'use client';

import { useQuery } from '@tanstack/react-query';
import { formatDistanceToNow } from 'date-fns';
import Link from 'next/link';
import { useParams } from 'next/navigation';

import { apiFetch } from '@/lib/api/client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { CardSkeleton } from '@/components/shared/loading-skeleton';
import { usePermissions } from '@/hooks/use-permissions';
import { WidgetErrorBoundary } from './widget-error-boundary';
import {
  STAGE_LABELS,
  PIPELINE_STAGES,
  LEGACY_STAGE_REMAP,
  formatSource,
  type PipelineStage,
} from '@/lib/constants';

interface ActivityItem {
  id: string;
  action: string;
  entityType: string;
  entityId: string | null;
  /** Server-resolved human label (client name, yacht name, …) when the
   *  underlying entity still exists. Falls back to the id prefix in the UI. */
  label: string | null;
  userId: string | null;
  /** Server-resolved actor display name (from user_profiles). When null,
   *  the actor row no longer exists - render falls back to a "Unknown
   *  user" sentinel rather than the raw UUID prefix. */
  actorName: string | null;
  fieldChanged: string | null;
  /** For user-FK diff rows (assignedTo, ownerId, etc.) the service
   *  already replaces these with display names. Non-user-FK rows pass
   *  through verbatim. */
  oldValue: unknown;
  newValue: unknown;
  metadata: Record<string, unknown> | null;
  createdAt: string;
}

/** camelCase / snake_case field name → "Title Case" so the audit log
 *  reads naturally ("fullName" → "Full Name", "phone_number" → "Phone
 *  Number"). Single-word fields stay capitalized. */
function humanizeFieldName(name: string): string {
  return name
    .replace(/_/g, ' ')
    .replace(/([a-z])([A-Z])/g, '$1 $2')
    .replace(/\b\w/g, (c) => c.toUpperCase());
}

/** Entity type alias map for the feed labels. Most types humanize fine
 *  via `humanizeFieldName`, but a few read awkwardly ("Residential
 *  Client" is clearer than the raw enum, notes flatten to their parent). */
const ENTITY_TYPE_LABELS: Record<string, string> = {
  residential_client: 'Residential client',
  residential_interest: 'Residential interest',
  berth_tenancy: 'Berth tenancy',
  berth_maintenance_log: 'Berth maintenance',
  berth_recommendation: 'Berth recommendation',
  client_note: 'Client note',
  yacht_note: 'Yacht note',
  company_note: 'Company note',
  interest_note: 'Interest note',
  interest_qualification: 'Interest qualification',
  document_send: 'Document send',
  document_folder: 'Document folder',
  document_template: 'Document template',
  documentTemplate: 'Document template',
  form_template: 'Form template',
  report_template: 'Report template',
  email_account: 'Email account',
  email_message: 'Email message',
  user_email_change: 'Email change',
  custom_field_definition: 'Custom field',
  custom_field_values: 'Custom field',
  expense_export: 'Expense export',
  gdpr_export: 'GDPR export',
  qualification_criterion: 'Qualification criterion',
  website_submission: 'Website submission',
  webhook_inbound: 'Inbound webhook',
  webhook_delivery: 'Webhook delivery',
  audit_log: 'Audit log',
  portal_user: 'Portal user',
  portal_session: 'Portal session',
  portal_auth_token: 'Portal token',
  client_contact: 'Client contact',
  clientContact: 'Client contact',
  clientAddress: 'Client address',
  companyAddress: 'Company address',
  clientRelationship: 'Client relationship',
  company_membership: 'Company membership',
  crm_invite: 'CRM invite',
  queue_job: 'Queue job',
  super_admin: 'Super admin',
};
function humanizeEntityType(type: string): string {
  return ENTITY_TYPE_LABELS[type] ?? humanizeFieldName(type);
}

/** Map enum-typed field values to their canonical human labels. The audit
 *  log stores raw enum strings (`deposit_10pct`, `lost_other_marina`); the
 *  feed should read like `10% Deposit`, not the wire value. */
function normalizeEnumValue(field: string, value: unknown): unknown {
  if (typeof value !== 'string') return value;
  const f = field.replace(/_/g, '').toLowerCase();
  if (f === 'pipelinestage' || f === 'stage') {
    // A2: map legacy 9-stage enum values to their 7-stage equivalents so
    // historical audit-log rows ("deposit_10pct", "contract_sent", ...)
    // render as the modern label rather than a humanized raw enum.
    const modern = (PIPELINE_STAGES as readonly string[]).includes(value)
      ? (value as PipelineStage)
      : LEGACY_STAGE_REMAP[value];
    if (modern) return STAGE_LABELS[modern];
    return humanizeFieldName(value);
  }
  if (f === 'source') {
    return formatSource(value) ?? value;
  }
  if (f === 'leadcategory' || f === 'category') {
    return humanizeFieldName(value);
  }
  if (f === 'outcome') {
    return humanizeFieldName(value);
  }
  return value;
}

/** Render a JSON-ish value as a short, single-line preview. Strings come
 *  through as-is; objects flatten to "k: v, k: v"; arrays compress to a
 *  count; nulls / empty render as em-dash. */
function shortValue(value: unknown, fieldContext?: string): string {
  if (fieldContext) value = normalizeEnumValue(fieldContext, value);
  if (value === null || value === undefined || value === '') return '-';
  if (typeof value === 'string') return value;
  if (typeof value === 'number' || typeof value === 'boolean') return String(value);
  if (Array.isArray(value)) return `${value.length} item${value.length === 1 ? '' : 's'}`;
  if (typeof value === 'object') {
    const entries = Object.entries(value as Record<string, unknown>);
    if (entries.length === 0) return '-';
    return entries
      .slice(0, 3)
      .map(
        ([k, v]) =>
          `${humanizeFieldName(k)}: ${typeof v === 'string' ? normalizeEnumValue(k, v) : JSON.stringify(v)}`,
      )
      .join(', ');
  }
  return String(value);
}

/** Build a "Field: old → new" diff string for the activity row's second
 *  line. Returns null when there's nothing useful to show.
 *
 *  Audit logs for updates store the per-field diff inside `oldValue` as
 *  `{ field: { old, new }, … }` (see entity-diff.ts), so that's the
 *  shape we pattern-match first. Falls back to a fieldChanged/old→new
 *  pair when those are present, and finally to a key-by-key compare of
 *  two flat objects in `oldValue` vs `newValue`. */
function buildDiffLine(item: ActivityItem): string | null {
  // Shape A: oldValue = { field: { old, new }, … }
  if (
    item.action === 'update' &&
    item.oldValue &&
    typeof item.oldValue === 'object' &&
    !Array.isArray(item.oldValue)
  ) {
    const diffMap = item.oldValue as Record<string, unknown>;
    const entries = Object.entries(diffMap).filter(([, v]) => {
      return v && typeof v === 'object' && 'old' in (v as object) && 'new' in (v as object);
    });
    if (entries.length > 0) {
      return entries
        .slice(0, 2)
        .map(([field, v]) => {
          const { old, new: nextValue } = v as { old: unknown; new: unknown };
          return `${humanizeFieldName(field)}: ${shortValue(old, field)} → ${shortValue(nextValue, field)}`;
        })
        .join(' · ');
    }
  }

  // Shape B: single-field change with explicit columns.
  if (item.fieldChanged) {
    const field = item.fieldChanged;
    return `${humanizeFieldName(field)}: ${shortValue(item.oldValue, field)} → ${shortValue(item.newValue, field)}`;
  }

  // Shape C: flat oldValue vs flat newValue.
  if (
    item.action === 'update' &&
    item.oldValue &&
    typeof item.oldValue === 'object' &&
    item.newValue &&
    typeof item.newValue === 'object'
  ) {
    const oldObj = item.oldValue as Record<string, unknown>;
    const newObj = item.newValue as Record<string, unknown>;
    const keys = Object.keys(oldObj).filter((k) => k in newObj);
    if (keys.length === 0) return null;
    return keys
      .slice(0, 2)
      .map(
        (k) => `${humanizeFieldName(k)}: ${shortValue(oldObj[k], k)} → ${shortValue(newObj[k], k)}`,
      )
      .join(' · ');
  }

  return null;
}

const ACTION_VARIANTS: Record<string, 'default' | 'secondary' | 'destructive' | 'outline'> = {
  create: 'default',
  update: 'secondary',
  delete: 'destructive',
  archive: 'outline',
  restore: 'secondary',
};

function ActionBadge({ action }: { action: string }) {
  const variant = ACTION_VARIANTS[action] ?? 'outline';
  return (
    <Badge variant={variant} className="shrink-0 capitalize text-xs">
      {action}
    </Badge>
  );
}

function ActivityFeedInner() {
  const params = useParams<{ portSlug: string }>();
  const portSlug = params?.portSlug ?? '';
  const { can } = usePermissions();
  const canViewAuditLog = can('admin', 'view_audit_log');

  const { data, isLoading } = useQuery<ActivityItem[]>({
    queryKey: ['dashboard', 'activity'],
    queryFn: () => apiFetch<ActivityItem[]>('/api/v1/dashboard/activity'),
    staleTime: 30_000,
    retry: 2,
  });

  if (isLoading) {
    return <CardSkeleton />;
  }

  // A1: permission_denied rows on the activity feed render as a bare
  // action badge with no entity name (they target `admin.X` with empty
  // entityId). They're noise for the rep - keep them in the audit log
  // page but hide them from the dashboard feed.
  const items = (data ?? []).filter((i) => i.action !== 'permission_denied');

  return (
    <Card>
      <CardHeader className="flex flex-row items-center justify-between gap-2 space-y-0">
        <CardTitle className="text-base">Recent Activity</CardTitle>
        {canViewAuditLog && portSlug ? (
          <Link
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            href={`/${portSlug}/admin/audit` as any}
            className="text-xs font-medium text-primary hover:underline"
          >
            See all
          </Link>
        ) : null}
      </CardHeader>
      <CardContent>
        {items.length === 0 ? (
          <p className="text-sm text-muted-foreground">
            No recent activity yet - your team&apos;s actions (interests created, stages changed,
            invoices sent) will appear here.
          </p>
        ) : (
          <div className="max-h-80 overflow-y-auto space-y-3 pr-1">
            {items.map((item) => {
              const diffLine = buildDiffLine(item);
              return (
                <div
                  key={item.id}
                  className="flex items-start gap-3 text-sm border-b border-border pb-3 last:border-0 last:pb-0"
                >
                  <ActionBadge action={item.action} />
                  <div className="min-w-0 flex-1">
                    <p className="truncate text-foreground">
                      {item.label ? (
                        <>
                          <span className="font-medium">{item.label}</span>
                          {/* M-NEW-2: explicit middle-dot separator. The
                              prior `ml-1.5` was getting collapsed under
                              `truncate` so the label + type rendered as
                              "Test Person 1interest" with no visible
                              space between them. */}
                          <span className="text-muted-foreground/60 mx-1.5">·</span>
                          <span className="text-muted-foreground text-xs capitalize">
                            {humanizeEntityType(item.entityType)}
                          </span>
                        </>
                      ) : (
                        // No resolvable label - either the entity was
                        // deleted or the type isn't in the server-side
                        // resolver yet. Either way we never expose a
                        // UUID fragment: it reads as noise to the rep
                        // and leaks an internal identifier.
                        <span className="font-medium capitalize">
                          {humanizeEntityType(item.entityType)}
                          {item.entityId ? (
                            <span className="ml-1 text-muted-foreground text-xs font-normal">
                              (removed)
                            </span>
                          ) : null}
                        </span>
                      )}
                    </p>
                    {diffLine ? (
                      <p className="truncate text-xs text-muted-foreground mt-0.5" title={diffLine}>
                        {diffLine}
                      </p>
                    ) : null}
                    <p className="text-[11px] text-muted-foreground/80 mt-0.5">
                      {formatDistanceToNow(new Date(item.createdAt), { addSuffix: true })}
                    </p>
                  </div>
                </div>
              );
            })}
          </div>
        )}
      </CardContent>
    </Card>
  );
}

export function ActivityFeed() {
  return (
    <WidgetErrorBoundary>
      <ActivityFeedInner />
    </WidgetErrorBoundary>
  );
}
