import { Alert, Anchor, Badge, Card, Group, SimpleGrid, Stack, Text } from "@mantine/core";
import { useQuery } from "@tanstack/react-query";
import { FilePlus2, FileX2, History } from "lucide-react";

import { openFileInNewTab } from "@/services/files.service";
import { api } from "@/services/api";
import type {
  Company,
  CompanyChangeRequest,
  CompanyRevision,
  CompanyRevisionChange,
  DocumentChangeIntent,
  LicenseChangeIntent,
} from "@/types/customer";
import { DiffRow, FIELD_LABELS, currentValue } from "./ChangeRequestReview";
import { formatDate, humanize } from "./format";

interface DocDiff {
  key: string;
  label: string;
  fromFile: { id: string; name: string } | null;
  toFile: { id: string; name: string } | null;
}

interface FieldDiff {
  key: string;
  label: string;
  from: string;
  to: string;
}

interface TimelineEntry {
  id: string;
  kind: "approved" | "rejected" | "changes_requested" | "revision";
  at: string;
  note?: string | null;
  summary?: string;
  /** Who filed the change (the customer, or staff editing during onboarding). */
  requestedBy?: string | null;
  /** When they filed it — the "asked" half of the ask/decide pair below. */
  requestedAt?: string | null;
  /** Who decided (approved / rejected / sent it back to marketing). */
  decidedBy?: string | null;
  fieldDiffs: FieldDiff[];
  docDiffs: DocDiff[];
}

const KIND_BADGE: Record<TimelineEntry["kind"], { label: string; color: string }> = {
  approved: { label: "Approved", color: "edr-green" },
  rejected: { label: "Rejected", color: "red" },
  // Sending a request back is what "reverted to marketing" means here: the
  // request stays open and marketing owns the follow-up with the customer.
  changes_requested: { label: "Sent back to marketing", color: "yellow" },
  revision: { label: "Recorded", color: "blue" },
};

/** "Requested by X" / "Reviewed by X", with the id-less case reading sanely. */
function actorLine(verb: string, who?: string | null, when?: string | null) {
  if (!who && !when) return null;
  return `${verb}${who ? ` by ${who}` : ""}${when ? ` · ${formatDate(when)}` : ""}`;
}

/**
 * Pair adjacent remove-then-add intents into one before/after doc diff — a
 * "replace" is always staged as `[{op:'remove'}, {op:'add'}]` pushed together
 * (see `replaceProfileLicenseFile` and friends), and later merges only ever
 * append after that pair, so adjacency survives. A remove or add with no
 * adjacent partner stands alone.
 */
function pairIntents(
  intents: (LicenseChangeIntent | DocumentChangeIntent)[],
  labelFor: (intent: LicenseChangeIntent | DocumentChangeIntent) => string,
): DocDiff[] {
  const diffs: DocDiff[] = [];
  let i = 0;
  while (i < intents.length) {
    const current = intents[i];
    const next = intents[i + 1];
    if (current.op === "remove" && next?.op === "add") {
      diffs.push({
        key: `${current.fileId}-${next.fileId}`,
        label: labelFor(next),
        fromFile: { id: current.fileId, name: current.fileName ?? "Document" },
        toFile: { id: next.fileId, name: next.fileName ?? "Document" },
      });
      i += 2;
      continue;
    }
    diffs.push({
      key: `${current.fileId}-${i}`,
      label: labelFor(current),
      fromFile:
        current.op === "remove"
          ? { id: current.fileId, name: current.fileName ?? "Document" }
          : null,
      toFile:
        current.op === "add"
          ? { id: current.fileId, name: current.fileName ?? "Document" }
          : null,
    });
    i += 1;
  }
  return diffs;
}

/**
 * Historical field diffs on a change request only ever recorded the proposed
 * ("to") value — there is no stored "before" snapshot — so `from` reads the
 * CURRENT company value. That's exact for the most recent entry; for an older
 * one it can drift if the field changed again since. A real limitation of the
 * data model, not something this view can reconstruct.
 */
function fromChangeRequest(
  r: CompanyChangeRequest,
  company: Company,
): TimelineEntry {
  const proposedKeys = Object.keys(r.snapshot ?? {}).filter(
    (k) => k !== "faydaIdentity",
  );
  const fieldDiffs: FieldDiff[] = proposedKeys.map((key) => ({
    key,
    label: FIELD_LABELS[key] ?? humanize(key),
    from: currentValue(company, key),
    to:
      r.snapshot[key] === null || r.snapshot[key] === undefined || r.snapshot[key] === ""
        ? "—"
        : String(r.snapshot[key]),
  }));

  const docDiffs: DocDiff[] = [
    ...pairIntents(r.licenseChanges, () => "Business license"),
    ...pairIntents(r.documentChanges, (c) =>
      humanize((c as DocumentChangeIntent).code),
    ),
    ...r.documentFileIds.map((fileId, i) => ({
      key: fileId,
      label: "Document",
      fromFile: null,
      toFile: { id: fileId, name: `Document ${i + 1}` },
    })),
  ];

  return {
    id: r.id,
    kind: r.status as TimelineEntry["kind"],
    at: r.reviewedAt ?? r.updatedAt,
    note: r.note,
    requestedBy: r.submittedByName,
    requestedAt: r.submittedAt ?? r.createdAt,
    decidedBy: r.reviewedByName,
    fieldDiffs,
    docDiffs,
  };
}

function fromRevision(rev: CompanyRevision): TimelineEntry {
  const isDocChange = (c: CompanyRevisionChange) => c.field.startsWith("document:");
  const fieldDiffs: FieldDiff[] = rev.changes
    .filter((c) => !isDocChange(c))
    .map((c) => ({ key: c.field, label: c.label, from: c.from ?? "—", to: c.to ?? "—" }));
  const docDiffs: DocDiff[] = rev.changes
    .filter(isDocChange)
    .map((c) => ({
      key: c.field,
      label: c.label,
      fromFile: c.fromFileId ? { id: c.fromFileId, name: c.from ?? "Document" } : null,
      toFile: c.toFileId ? { id: c.toFileId, name: c.to ?? "Document" } : null,
    }));

  return {
    id: rev.id,
    kind: "revision",
    at: rev.createdAt,
    summary: rev.summary,
    requestedBy: rev.actorName,
    fieldDiffs,
    docDiffs,
  };
}

/**
 * One combined, chronological timeline of everything that's happened to a
 * company's record: onboarding-phase edits (no approval gate, from
 * `CompanyRevision`) and post-approval settings changes (reviewed via
 * `CompanyChangeRequest`) used to live in two separate, differently-shaped
 * lists — merged here into one sorted feed so "what changed and when" has a
 * single answer instead of two places to check.
 */
export function CompanyTimeline({ company }: { company: Company }) {
  const changeRequestsQuery = useQuery(
    api.customers.changeRequests.queryOptions({ input: { id: company.id } }),
  );
  const revisionsQuery = useQuery(
    api.customers.revisions.queryOptions({ input: { id: company.id } }),
  );

  const entries: TimelineEntry[] = [
    ...(changeRequestsQuery.data ?? [])
      .filter((r) => r.status !== "pending")
      .map((r) => fromChangeRequest(r, company)),
    ...(revisionsQuery.data ?? []).map(fromRevision),
  ].sort((a, b) => new Date(b.at).getTime() - new Date(a.at).getTime());

  const openFile = (file: { id: string; name: string }) =>
    openFileInNewTab(file.id, file.name);

  if (entries.length === 0) {
    return (
      <Card withBorder>
        <Stack align="center" gap={6} py="xl">
          <History size={24} className="text-edr-muted" />
          <Text size="sm" c="dimmed">
            No changes recorded yet.
          </Text>
        </Stack>
      </Card>
    );
  }

  return (
    <Stack gap="md">
      {entries.map((entry) => {
        const badge = KIND_BADGE[entry.kind];
        const requestedLine = actorLine(
          entry.kind === "revision" ? "Edited" : "Requested",
          entry.requestedBy,
          entry.requestedAt,
        );
        const decidedLine = actorLine(
          entry.kind === "changes_requested"
            ? "Sent back to marketing"
            : entry.kind === "rejected"
              ? "Rejected"
              : "Approved",
          entry.decidedBy,
          entry.kind === "revision" ? null : entry.at,
        );
        return (
          <Card key={entry.id} withBorder>
            <Stack gap="sm">
              <Group justify="space-between" wrap="nowrap" align="flex-start">
                <Group gap="sm">
                  <Badge color={badge.color} variant="light" radius="md">
                    {badge.label}
                  </Badge>
                  {entry.summary && (
                    <Text size="sm" c="edr-text" tt="capitalize">
                      {entry.summary}
                    </Text>
                  )}
                </Group>
                <Text size="xs" c="dimmed">
                  {formatDate(entry.at)}
                </Text>
              </Group>

              {/* Who asked, and who decided. Without this the feed said what
                  changed and when, but never named a person — the first thing
                  anyone auditing a returned request needs. */}
              {(requestedLine || decidedLine) && (
                <Stack gap={2}>
                  {requestedLine && (
                    <Text size="xs" c="dimmed">
                      {requestedLine}
                    </Text>
                  )}
                  {decidedLine && (
                    <Text size="xs" c="dimmed">
                      {decidedLine}
                    </Text>
                  )}
                </Stack>
              )}

              {entry.note && (
                <Alert color="yellow" variant="light">
                  <Text size="sm">
                    <strong>
                      {entry.kind === "changes_requested"
                        ? "What was asked for:"
                        : "Note:"}
                    </strong>{" "}
                    {entry.note}
                  </Text>
                </Alert>
              )}

              {entry.fieldDiffs.length > 0 && (
                <SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="lg">
                  {entry.fieldDiffs.map((f) => (
                    <DiffRow key={f.key} label={f.label} from={f.from} to={f.to} />
                  ))}
                </SimpleGrid>
              )}

              {entry.docDiffs.length > 0 && (
                <Stack gap={8}>
                  {entry.docDiffs.map((d) => (
                    <Group key={d.key} gap={8} wrap="nowrap">
                      <Text size="xs" fw={600} c="edr-muted" tt="uppercase">
                        {d.label}
                      </Text>
                      {d.fromFile && (
                        <Group gap={4} wrap="nowrap">
                          <FileX2 size={14} className="text-edr-muted" />
                          <Anchor
                            component="button"
                            type="button"
                            size="sm"
                            td="line-through"
                            onClick={() => openFile(d.fromFile!)}
                          >
                            {d.fromFile.name}
                          </Anchor>
                        </Group>
                      )}
                      {d.fromFile && d.toFile && (
                        <Text size="sm" c="edr-muted">
                          →
                        </Text>
                      )}
                      {d.toFile && (
                        <Group gap={4} wrap="nowrap">
                          <FilePlus2 size={14} className="text-edr-muted" />
                          <Anchor
                            component="button"
                            type="button"
                            size="sm"
                            onClick={() => openFile(d.toFile!)}
                          >
                            {d.toFile.name}
                          </Anchor>
                        </Group>
                      )}
                    </Group>
                  ))}
                </Stack>
              )}

              {entry.fieldDiffs.length === 0 && entry.docDiffs.length === 0 && (
                <Text size="sm" c="dimmed">
                  No details recorded for this entry.
                </Text>
              )}
            </Stack>
          </Card>
        );
      })}
    </Stack>
  );
}
