'use client';

import { useEffect, useMemo, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { formatDistanceToNow } from 'date-fns';
import { ChevronDown } from 'lucide-react';

import { apiFetch } from '@/lib/api/client';
import { Button } from '@/components/ui/button';
import { STAGE_LABELS, formatEnum, formatSource, type PipelineStage } from '@/lib/constants';
import { cn } from '@/lib/utils';

interface AuditRow {
  id: string;
  action: string;
  entityType: string;
  entityId: string | null;
  fieldChanged: string | null;
  oldValue: unknown;
  newValue: unknown;
  metadata: Record<string, unknown> | null;
  createdAt: string;
  actor: { id: string; email: string; name: string | null } | null;
}

const ACTION_VERBS: Record<string, { past: string }> = {
  create: { past: 'created' },
  update: { past: 'updated' },
  delete: { past: 'deleted' },
  archive: { past: 'archived' },
  restore: { past: 'restored' },
  merge: { past: 'merged' },
  revert: { past: 'reverted' },
};

function actionVerb(action: string): string {
  return ACTION_VERBS[action]?.past ?? action;
}

function formatField(field: string | null): string | null {
  if (!field) return null;
  return field
    .replace(/_/g, ' ')
    .replace(/([a-z])([A-Z])/g, '$1 $2')
    .toLowerCase();
}

function formatValueForField(field: string | null, value: unknown): string {
  if (value === null || value === undefined) return '';
  if (field) {
    const f = field.replace(/_/g, '').toLowerCase();
    if (typeof value === 'string') {
      if (f === 'pipelinestage' || f === 'stage') {
        return STAGE_LABELS[value as PipelineStage] ?? formatEnum(value);
      }
      if (f === 'source') return formatSource(value) ?? value;
      if (f === 'leadcategory' || f === 'category' || f === 'outcome') {
        return formatEnum(value);
      }
    }
  }
  if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
    return String(value);
  }
  return JSON.stringify(value);
}

/** Natural-sentence rendering of one audit row.
 *
 *  Behaviour ladder:
 *    1. Field changed with both old + new values → "set <field> to <new>"
 *       (the inline diff below already shows the old-value strikethrough,
 *       so we don't restate it here; result reads like a sentence rather
 *       than a diff label).
 *    2. Field changed with only a new value → "set <field> to <new>"
 *    3. Field changed with neither value (rare; legacy rows) → "<verb>
 *       the <field>"
 *    4. No field → "<verb> this record"
 *
 *  Truncation at 60 chars on the inline value keeps long body fields
 *  (notes, descriptions) from blowing out the row — the diff line
 *  below still renders the full value if the rep clicks through.
 */
function sentence(row: AuditRow, actor: string): string {
  const verb = actionVerb(row.action);
  const field = formatField(row.fieldChanged);
  if (!field) return `${actor} ${verb} this record`;

  const newFormatted =
    row.newValue !== null && row.newValue !== undefined
      ? formatValueForField(row.fieldChanged, row.newValue)
      : null;
  if (newFormatted) {
    const truncated = newFormatted.length > 60 ? newFormatted.slice(0, 60) + '…' : newFormatted;
    return `${actor} set ${field} to "${truncated}"`;
  }
  return `${actor} ${verb} the ${field}`;
}

interface Props {
  endpoint: string; // e.g. /api/v1/clients/{id}/activity
  emptyText?: string;
}

interface SessionGroup {
  actorKey: string;
  actorLabel: string;
  rows: AuditRow[];
}

/** Five-minute window: consecutive events from the same actor inside this
 *  window collapse into one session header. Outside the window a new
 *  group starts so a single bulk-edit run reads as one block. */
const SESSION_WINDOW_MS = 5 * 60_000;

function groupRows(rows: AuditRow[]): SessionGroup[] {
  const groups: SessionGroup[] = [];
  for (const row of rows) {
    const actorKey = row.actor?.id ?? '__system__';
    const actorLabel = row.actor?.name || row.actor?.email || 'System';
    const last = groups[groups.length - 1];
    if (
      last &&
      last.actorKey === actorKey &&
      last.rows.length > 0 &&
      Math.abs(new Date(last.rows[0]!.createdAt).getTime() - new Date(row.createdAt).getTime()) <=
        SESSION_WINDOW_MS
    ) {
      last.rows.push(row);
    } else {
      groups.push({ actorKey, actorLabel, rows: [row] });
    }
  }
  return groups;
}

export function EntityActivityFeed({ endpoint, emptyText = 'No activity yet.' }: Props) {
  const [now, setNow] = useState(() => Date.now());

  useEffect(() => {
    const t = setInterval(() => setNow(Date.now()), 60_000);
    return () => clearInterval(t);
  }, []);

  const { data, isLoading, error } = useQuery({
    queryKey: ['entity-activity', endpoint],
    queryFn: () => apiFetch<{ data: AuditRow[] }>(endpoint),
    staleTime: 30_000,
  });

  // Filter state — chips above the feed. Empty selection = no filter.
  const [actorFilter, setActorFilter] = useState<string | null>(null);
  const [actionFilter, setActionFilter] = useState<string | null>(null);

  const rows = data?.data ?? [];

  const actorOptions = useMemo(() => {
    const map = new Map<string, string>();
    for (const r of rows) {
      const id = r.actor?.id ?? '__system__';
      const label = r.actor?.name || r.actor?.email || 'System';
      map.set(id, label);
    }
    return Array.from(map.entries()).sort((a, b) => a[1].localeCompare(b[1]));
  }, [rows]);

  const actionOptions = useMemo(() => {
    const set = new Set<string>();
    for (const r of rows) set.add(r.action);
    return Array.from(set).sort();
  }, [rows]);

  const filteredRows = rows.filter((r) => {
    if (actorFilter && (r.actor?.id ?? '__system__') !== actorFilter) return false;
    if (actionFilter && r.action !== actionFilter) return false;
    return true;
  });

  const groups = useMemo(() => groupRows(filteredRows), [filteredRows]);

  if (isLoading) {
    return <div className="text-sm text-muted-foreground py-6">Loading activity…</div>;
  }
  if (error) {
    return (
      <div className="text-sm text-red-600 py-6">
        Failed to load activity: {error instanceof Error ? error.message : 'unknown error'}
      </div>
    );
  }
  if (rows.length === 0) {
    return <div className="text-sm text-muted-foreground py-6">{emptyText}</div>;
  }

  // Touch `now` so the closure participates in the minute re-render.
  void now;

  return (
    <div className="space-y-3">
      {(actorOptions.length > 1 || actionOptions.length > 1) && (
        <div className="flex flex-wrap items-center gap-1.5 text-xs">
          {actorOptions.length > 1 && (
            <FilterChipMenu
              label="Actor"
              value={actorFilter}
              onChange={setActorFilter}
              options={actorOptions.map(([id, label]) => ({ value: id, label }))}
            />
          )}
          {actionOptions.length > 1 && (
            <FilterChipMenu
              label="Action"
              value={actionFilter}
              onChange={setActionFilter}
              options={actionOptions.map((a) => ({ value: a, label: actionVerb(a) }))}
            />
          )}
          {(actorFilter || actionFilter) && (
            <button
              type="button"
              className="text-muted-foreground underline"
              onClick={() => {
                setActorFilter(null);
                setActionFilter(null);
              }}
            >
              Clear
            </button>
          )}
        </div>
      )}

      {filteredRows.length === 0 ? (
        <div className="text-sm text-muted-foreground py-6">
          No activity matches the current filters.
        </div>
      ) : (
        <ol className="relative ml-3 pl-6 space-y-4 py-2">
          {groups.map((group, gi) => (
            <SessionGroupItem
              key={`${group.actorKey}-${gi}`}
              group={group}
              isLast={gi === groups.length - 1}
            />
          ))}
        </ol>
      )}
    </div>
  );
}

function SessionGroupItem({ group, isLast }: { group: SessionGroup; isLast: boolean }) {
  const [expanded, setExpanded] = useState(group.rows.length <= 3);
  const first = group.rows[0]!;
  const created = new Date(first.createdAt);
  const ago = formatDistanceToNow(created, { addSuffix: true });

  // Vertical connector — runs from below this bubble down to the next item,
  // omitted on the last item so the line never trails past the last bubble.
  const connector = !isLast ? (
    <span
      aria-hidden
      className="absolute left-[-26px] top-3 bottom-[-1rem] w-px bg-muted-foreground/20"
    />
  ) : null;

  if (group.rows.length === 1) {
    return (
      <li className="relative">
        {connector}
        <span className="absolute left-[-31px] top-1 h-2.5 w-2.5 rounded-full bg-primary/70 ring-2 ring-background" />
        <RowBody row={first} actor={group.actorLabel} ago={ago} />
      </li>
    );
  }

  return (
    <li className="relative">
      {connector}
      <span className="absolute left-[-31px] top-1 h-2.5 w-2.5 rounded-full bg-primary/70 ring-2 ring-background" />
      <button
        type="button"
        onClick={() => setExpanded((v) => !v)}
        className="text-sm hover:underline flex items-center gap-1"
      >
        <span className="font-medium">{group.actorLabel}</span>
        <span className="text-muted-foreground">
          made {group.rows.length} changes in this session
        </span>
        <ChevronDown
          className={cn(
            'h-3.5 w-3.5 text-muted-foreground transition-transform',
            expanded && 'rotate-180',
          )}
        />
      </button>
      <div className="text-xs text-muted-foreground" title={created.toISOString()}>
        {ago}
      </div>
      {expanded && (
        <ol className="mt-2 space-y-2 border-l border-muted-foreground/15 pl-3">
          {group.rows.map((r) => (
            <li key={r.id} className="text-sm">
              <RowBody row={r} actor={group.actorLabel} ago={null} compact />
            </li>
          ))}
        </ol>
      )}
    </li>
  );
}

function RowBody({
  row,
  actor,
  ago,
  compact = false,
}: {
  row: AuditRow;
  actor: string;
  ago: string | null;
  compact?: boolean;
}) {
  return (
    <>
      <div className="text-sm">{sentence(row, actor)}</div>
      {ago !== null && (
        <div className="text-xs text-muted-foreground" title={row.createdAt}>
          {ago}
        </div>
      )}
      {row.fieldChanged && (row.oldValue !== null || row.newValue !== null) ? (
        <div className={cn('text-xs space-x-1', compact ? 'mt-0.5' : 'mt-1')}>
          {row.oldValue !== null && row.oldValue !== undefined ? (
            <span className="line-through text-muted-foreground">
              {formatValueForField(row.fieldChanged, row.oldValue).slice(0, 80)}
            </span>
          ) : null}
          {row.newValue !== null && row.newValue !== undefined ? (
            <span className="text-foreground">
              → {formatValueForField(row.fieldChanged, row.newValue).slice(0, 80)}
            </span>
          ) : null}
        </div>
      ) : null}
    </>
  );
}

function FilterChipMenu({
  label,
  value,
  onChange,
  options,
}: {
  label: string;
  value: string | null;
  onChange: (v: string | null) => void;
  options: { value: string; label: string }[];
}) {
  // Lightweight native <select>-style chip — keeps the activity feed
  // self-contained without pulling in a popover or radix dropdown for
  // what is essentially "two filter chips".
  return (
    <label className="inline-flex items-center gap-1 rounded-full border px-2 py-0.5 hover:bg-muted/50 cursor-pointer">
      <span className="text-muted-foreground">{label}:</span>
      <select
        className="bg-transparent outline-none text-foreground"
        value={value ?? ''}
        onChange={(e) => onChange(e.target.value || null)}
      >
        <option value="">All</option>
        {options.map((o) => (
          <option key={o.value} value={o.value}>
            {o.label}
          </option>
        ))}
      </select>
    </label>
  );
}

// Keep Button reference so unused-import lint doesn't fire if Button gets
// reintroduced later. Currently the feed uses raw <button> for the chip
// menu (smaller footprint).
void Button;
