import { useQuery } from "@tanstack/react-query";
import { useState, type ReactNode } from "react";
import { listManifestFiles, listRevokedCertificates } from "../api/service";
import type { ProjectionRecord } from "../api/schemas";
import { CopyableValue } from "./CopyableValue";
import { CursorPagerControls } from "./CursorPagerControls";
import { DataTable, type Column } from "./DataTable";
import { EmptyBlock, LoadingBlock, Notice } from "./StateBlock";
import { useCursorPager } from "../lib/cursor";
import { formatInt, formatUtc } from "../lib/format";
import { asArray, asNumber, asRecord, asString } from "../lib/projection";

function MetaRow({ label, value, mono = false }: { label: string; value: ReactNode; mono?: boolean }) {
  return (
    <>
      <dt>{label}</dt>
      <dd className={mono ? "mono" : undefined}>{value ?? <span className="text-faint">—</span>}</dd>
    </>
  );
}

/** Format an IP address from a projection byte array (v4 dotted / v6 compressed hextets). */
function formatIpAddr(addr: unknown, afi: unknown): string | null {
  if (!Array.isArray(addr)) return null;
  const bytes = addr.filter((b): b is number => typeof b === "number");
  if (bytes.length === 0) return null;
  if (asString(afi) === "Ipv6" || bytes.length > 4) {
    const groups: number[] = [];
    for (let i = 0; i + 1 < bytes.length; i += 2) {
      groups.push((bytes[i] << 8) | bytes[i + 1]);
    }
    // RFC 5952-style compression of the longest run of zero groups.
    let bestStart = -1;
    let bestLen = 0;
    for (let i = 0; i < groups.length; ) {
      if (groups[i] !== 0) {
        i += 1;
        continue;
      }
      let j = i;
      while (j < groups.length && groups[j] === 0) j += 1;
      if (j - i > bestLen) {
        bestStart = i;
        bestLen = j - i;
      }
      i = j;
    }
    const hex = groups.map((g) => g.toString(16));
    if (bestLen >= 2) {
      const head = hex.slice(0, bestStart).join(":");
      const tail = hex.slice(bestStart + bestLen).join(":");
      return `${head}::${tail}`;
    }
    return hex.join(":");
  }
  return bytes.join(".");
}

/**
 * Flatten projection ipResources to display strings.
 * Real shape (serde of the rpki types):
 *   { families: [ { afi, choice: { AddressesOrRanges: [ {Prefix:{addr,afi,prefix_len}} | {Range:{min,max}} ] } } ] }
 */
function formatIpResources(resources: unknown): string[] {
  const out: string[] = [];
  for (const famRaw of asArray(asRecord(resources)?.families)) {
    const fam = asRecord(famRaw);
    const choice = asRecord(fam?.choice);
    if (!choice) continue;
    if ("Inherit" in choice) {
      out.push("inherit");
      continue;
    }
    for (const entryRaw of asArray(choice.AddressesOrRanges)) {
      const entry = asRecord(entryRaw);
      const prefix = asRecord(entry?.Prefix);
      if (prefix) {
        const base = formatIpAddr(prefix.addr, prefix.afi ?? fam?.afi);
        out.push(base ? `${base}/${asNumber(prefix.prefix_len) ?? "?"}` : JSON.stringify(entryRaw));
        continue;
      }
      const range = asRecord(entry?.Range);
      if (range) {
        const min = formatIpAddr(asRecord(range.min)?.addr ?? range.min, fam?.afi);
        const max = formatIpAddr(asRecord(range.max)?.addr ?? range.max, fam?.afi);
        out.push(min && max ? `${min} - ${max}` : JSON.stringify(entryRaw));
        continue;
      }
      out.push(JSON.stringify(entryRaw));
    }
  }
  return out;
}

/**
 * Flatten projection asResources to display strings.
 * Real shape: { asnum: { AsIdsOrRanges: [ {Id: n} | {Range: {min, max}} ] } } or { "inherit"/"Inherit": … }.
 */
function formatAsResources(resources: unknown): string[] {
  const rec = asRecord(resources);
  if (!rec) return [];
  if ("inherit" in rec || "Inherit" in rec) return ["inherit"];
  const out: string[] = [];
  for (const entryRaw of asArray(asRecord(rec.asnum)?.AsIdsOrRanges)) {
    const entry = asRecord(entryRaw);
    const id = asNumber(entry?.Id);
    if (id != null) {
      out.push(`AS${id}`);
      continue;
    }
    const range = asRecord(entry?.Range);
    if (range) {
      out.push(`AS${asNumber(range.min) ?? "?"}-AS${asNumber(range.max) ?? "?"}`);
      continue;
    }
    out.push(JSON.stringify(entryRaw));
  }
  return out;
}

/** Render a list of URIs (one per line) or null when empty. */
function uriLines(uris: string[]): ReactNode {
  if (!uris.length) return null;
  return (
    <span>
      {uris.map((u) => (
        <span key={u} className="mono" style={{ display: "block", wordBreak: "break-all" }}>
          {u}
        </span>
      ))}
    </span>
  );
}

/**
 * Chip list that collapses to `collapsedCount` entries with a clickable
 * "+N more" toggle; expanding shows every item (plus a "Show less" toggle).
 */
export function ExpandableChipList({ items, collapsedCount = 12 }: { items: string[]; collapsedCount?: number }) {
  const [expanded, setExpanded] = useState(false);
  if (!items.length) return <>—</>;
  const collapsible = items.length > collapsedCount;
  const visible = expanded || !collapsible ? items : items.slice(0, collapsedCount);
  return (
    <span className="chip-list">
      {visible.map((item, i) => (
        <span className="chip" key={i}>{item}</span>
      ))}
      {collapsible ? (
        <button
          type="button"
          className="chip chip-more"
          onClick={() => setExpanded((prev) => !prev)}
          aria-expanded={expanded}
        >
          {expanded ? "Show less" : `+${items.length - collapsedCount} more`}
        </button>
      ) : null}
    </span>
  );
}

function ManifestFiles({ runId, objectInstanceId }: { runId: string; objectInstanceId: string }) {
  const pager = useCursorPager(`${runId}:${objectInstanceId}:mft`);
  const query = useQuery({
    queryKey: ["manifest-files", runId, objectInstanceId, pager.cursor],
    queryFn: () => listManifestFiles(runId, objectInstanceId, { limit: 100, cursor: pager.cursor }),
    placeholderData: (prev) => prev,
  });

  const columns: Column<{ fileName: string; hashHex: string }>[] = [
    { key: "file", header: "File", render: (row) => <span className="mono">{row.fileName}</span> },
    {
      key: "hash",
      header: "Hash",
      render: (row) => <CopyableValue value={row.hashHex} max={40} label="file hash" />,
    },
  ];

  if (query.isPending) return <LoadingBlock label="Loading manifest file list…" />;
  if (query.isError) return <Notice kind="error">Failed to load the manifest file list.</Notice>;
  return (
    <>
      <DataTable
        columns={columns}
        rows={query.data.items}
        rowKey={(row) => row.fileName}
        emptyTitle="Manifest has no file entries"
        caption="Manifest file list"
      />
      <CursorPagerControls
        page={pager.page}
        canPrev={pager.canPrev}
        hasNext={query.data.nextCursor != null}
        loading={query.isFetching}
        onPrev={pager.goPrev}
        onNext={() => pager.goNext(query.data.nextCursor ?? null)}
        itemCount={query.data.items.length}
      />
    </>
  );
}

function RevokedCerts({ runId, objectInstanceId }: { runId: string; objectInstanceId: string }) {
  const pager = useCursorPager(`${runId}:${objectInstanceId}:crl`);
  const query = useQuery({
    queryKey: ["revoked-certs", runId, objectInstanceId, pager.cursor],
    queryFn: () =>
      listRevokedCertificates(runId, objectInstanceId, { limit: 100, cursor: pager.cursor }),
    placeholderData: (prev) => prev,
  });

  const columns: Column<{ serialNumberHex?: string | null; serialNumber?: string | number | null; revocationDate?: string | null }>[] = [
    {
      key: "serial",
      header: "Serial number",
      render: (row) => (
        <CopyableValue
          value={row.serialNumberHex ?? (row.serialNumber != null ? String(row.serialNumber) : null)}
          max={44}
          label="serial number"
        />
      ),
    },
    { key: "date", header: "Revocation date", render: (row) => formatUtc(row.revocationDate) },
  ];

  if (query.isPending) return <LoadingBlock label="Loading revoked certificates…" />;
  if (query.isError) return <Notice kind="error">Failed to load the revocation list.</Notice>;
  const rows = query.data.items.map((row, index) => ({
    ...row,
    _key: `${row.serialNumberHex ?? row.serialNumber ?? "entry"}-${index}`,
  }));
  return (
    <>
      <DataTable
        columns={columns}
        rows={rows}
        rowKey={(row) => row._key}
        emptyTitle="No revoked certificates"
        caption="Revoked certificates"
      />
      <CursorPagerControls
        page={pager.page}
        canPrev={pager.canPrev}
        hasNext={query.data.nextCursor != null}
        loading={query.isFetching}
        onPrev={pager.goPrev}
        onNext={() => pager.goNext(query.data.nextCursor ?? null)}
        itemCount={query.data.items.length}
      />
    </>
  );
}

function RoaView({ projection }: { projection: Record<string, unknown> }) {
  const roa = asRecord(projection.roa);
  const families = asArray(roa?.ipAddressFamilies);
  return (
    <>
      <dl className="meta-grid" style={{ marginBottom: 16 }}>
        <MetaRow label="AS ID" value={asNumber(roa?.asId) ?? asString(roa?.asId)} mono />
      </dl>
      {families.map((family, i) => {
        const fam = asRecord(family);
        const addresses = asArray(fam?.addresses);
        return (
          <div key={i}>
            <h3 className="text-small" style={{ margin: "12px 0 6px" }}>
              Address family {asString(fam?.afi) ?? `#${i + 1}`}
            </h3>
            <DataTable
              columns={[
                {
                  key: "prefix",
                  header: "Prefix",
                  render: (row: Record<string, unknown>) => (
                    <span className="mono">{asString(row.prefix) ?? "—"}</span>
                  ),
                },
                {
                  key: "maxLength",
                  header: "maxLength",
                  numeric: true,
                  render: (row: Record<string, unknown>) => asNumber(row.maxLength) ?? "—",
                },
              ]}
              rows={addresses.map((a) => asRecord(a) ?? {})}
              rowKey={(row) => `${asString(row.prefix)}-${asNumber(row.maxLength)}`}
              emptyTitle="No prefixes"
            />
          </div>
        );
      })}
    </>
  );
}

function AspaView({ projection }: { projection: Record<string, unknown> }) {
  const aspa = asRecord(projection.aspa);
  const providers = asArray(aspa?.providerAsIds).map((p) => asNumber(p) ?? asString(p));
  return (
    <dl className="meta-grid">
      <MetaRow label="Customer AS" value={asNumber(aspa?.customerAsId) ?? asString(aspa?.customerAsId)} mono />
      <MetaRow
        label="Provider ASes"
        value={
          providers.length ? (
            <span className="chip-list">
              {providers.map((p, i) => (
                <span className="chip" key={i}>AS{String(p)}</span>
              ))}
            </span>
          ) : (
            "—"
          )
        }
      />
    </dl>
  );
}

export function CertificateView({ projection }: { projection: Record<string, unknown> }) {
  // Real API shape: the certificate payload lives in `resourceCertificate`
  // with extensions nested under `extensions`; keep the older
  // certificate/cer + flat-field variants working as fallbacks.
  const cert =
    asRecord(projection.certificate) ??
    asRecord(projection.cer) ??
    asRecord(projection.resourceCertificate) ??
    projection;
  const validity = asRecord(cert.validity);
  const ext = asRecord(cert.extensions);
  const legacyResources = asRecord(cert.resources);

  const siaDescriptions = asArray(asRecord(ext?.subjectInfoAccess)?.accessDescriptions);
  const siaUris = siaDescriptions
    .map((d) => asString(asRecord(d)?.accessLocation))
    .filter((v): v is string => Boolean(v));
  const aiaUris = asArray(ext?.caIssuersUris)
    .map((u) => asString(u))
    .filter((v): v is string => Boolean(v));
  const crlDpUris = asArray(ext?.crlDistributionPointsUris)
    .map((u) => asString(u))
    .filter((v): v is string => Boolean(v));

  const extIp = formatIpResources(ext?.ipResources);
  const ipBlocks = extIp.length
    ? extIp
    : asArray(legacyResources?.ipResources ?? legacyResources?.ipv4 ?? legacyResources?.addresses).map((b) =>
        typeof b === "string" ? b : JSON.stringify(b),
      );
  const extAs = formatAsResources(ext?.asResources);
  const asBlocks = extAs.length
    ? extAs
    : asArray(legacyResources?.asResources ?? legacyResources?.asns).map((b) =>
        typeof b === "string" || typeof b === "number" ? String(b) : JSON.stringify(b),
      );

  return (
    <dl className="meta-grid">
      <MetaRow label="Serial number" value={asString(cert.serialNumberHex ?? cert.serialNumber)} mono />
      <MetaRow label="Issuer" value={asString(cert.issuer)} mono />
      <MetaRow label="Subject" value={asString(cert.subject)} mono />
      <MetaRow
        label="Validity"
        value={`${formatUtc(asString(validity?.notBefore)) ?? "—"} → ${formatUtc(asString(validity?.notAfter)) ?? "—"}`}
        mono
      />
      <MetaRow
        label="SKI"
        value={<CopyableValue value={asString(cert.ski) ?? asString(ext?.subjectKeyIdentifier)} max={56} label="SKI" />}
      />
      <MetaRow
        label="AKI"
        value={<CopyableValue value={asString(cert.aki) ?? asString(ext?.authorityKeyIdentifier)} max={56} label="AKI" />}
      />
      <MetaRow
        label="SIA"
        value={asString(cert.sia) ? <CopyableValue value={asString(cert.sia)} max={72} label="SIA" /> : uriLines(siaUris)}
      />
      <MetaRow
        label="AIA"
        value={asString(cert.aia) ? <CopyableValue value={asString(cert.aia)} max={72} label="AIA" /> : uriLines(aiaUris)}
      />
      <MetaRow
        label="CRL DP"
        value={
          asString(cert.crlDistributionPoint ?? cert.crldp) ? (
            <CopyableValue value={asString(cert.crlDistributionPoint ?? cert.crldp)} max={72} label="CRL distribution point" />
          ) : (
            uriLines(crlDpUris)
          )
        }
      />
      <MetaRow
        label="IP resources"
        value={<ExpandableChipList items={ipBlocks} />}
      />
      <MetaRow
        label="AS resources"
        value={<ExpandableChipList items={asBlocks} />}
      />
      <MetaRow label="Signature algorithm" value={asString(cert.signatureAlgorithm)} mono />
    </dl>
  );
}

/**
 * Structured rendering of a parsed object projection, per object type.
 */
export function ProjectionView({
  record,
  runId,
  objectInstanceId,
}: {
  record: ProjectionRecord | null;
  runId: string;
  objectInstanceId: string;
}) {
  if (!record || !record.projection) {
    return (
      <EmptyBlock
        title="Parsed projection unavailable"
        hint="The query service needs --repo-bytes-db to build parsed projections for this object."
      />
    );
  }
  if (record.parseStatus === "error") {
    return <Notice kind="error">Projection failed: {record.errorSummary ?? "unknown parse error"}</Notice>;
  }

  const projection = record.projection;
  const type = (record.objectType ?? "").toLowerCase();
  // The query service nests the typed payload one level down:
  //   projection = { input, object: { roa | manifest | crl | certificate | aspa, … }, schemaVersion, tool }
  // Accept a flat payload too, so older fixtures / shape variants keep working.
  const typed = asRecord(projection.object) ?? projection;

  if (type === "roa") return <RoaView projection={typed} />;
  if (type === "asa" || type === "aspa") return <AspaView projection={typed} />;

  if (type === "mft") {
    const manifest = asRecord(typed.manifest);
    return (
      <>
        <dl className="meta-grid" style={{ marginBottom: 16 }}>
          <MetaRow label="Manifest number" value={asString(manifest?.manifestNumberHex ?? manifest?.manifestNumber)} mono />
          <MetaRow label="thisUpdate" value={formatUtc(asString(manifest?.thisUpdate))} />
          <MetaRow label="nextUpdate" value={formatUtc(asString(manifest?.nextUpdate))} />
          <MetaRow label="File hash algorithm" value={asString(manifest?.fileHashAlg)} mono />
          <MetaRow label="File count" value={formatInt(asNumber(manifest?.fileCount))} />
        </dl>
        <ManifestFiles runId={runId} objectInstanceId={objectInstanceId} />
      </>
    );
  }

  if (type === "crl") {
    // Real API shape: CRL fields sit directly on the typed payload
    // ({ issuer, thisUpdate, nextUpdate, extensions: { crlNumberHex, authorityKeyIdentifier }, … }),
    // there is no `crl` wrapper; keep `typed.crl` as a legacy fallback.
    const crl = asRecord(typed.crl) ?? typed;
    const crlExt = asRecord(crl.extensions);
    return (
      <>
        <dl className="meta-grid" style={{ marginBottom: 16 }}>
          <MetaRow label="Issuer" value={asString(crl.issuer)} mono />
          <MetaRow label="thisUpdate" value={formatUtc(asString(crl.thisUpdate))} />
          <MetaRow label="nextUpdate" value={formatUtc(asString(crl.nextUpdate))} />
          <MetaRow
            label="CRL number"
            value={
              asString(crl.crlNumberHex ?? crlExt?.crlNumberHex) ??
              (asNumber(crl.crlNumber ?? crlExt?.crlNumber) != null
                ? String(asNumber(crl.crlNumber ?? crlExt?.crlNumber))
                : null)
            }
            mono
          />
          <MetaRow
            label="AKI"
            value={
              <CopyableValue
                value={asString(crl.aki) ?? asString(crlExt?.authorityKeyIdentifier)}
                max={56}
                label="AKI"
              />
            }
          />
        </dl>
        <RevokedCerts runId={runId} objectInstanceId={objectInstanceId} />
      </>
    );
  }

  if (type === "cer" || type === "ee" || type === "router_cert") {
    return <CertificateView projection={typed} />;
  }

  return (
    <pre className="json-block" data-testid="projection-json">
      {JSON.stringify(typed, null, 2)}
    </pre>
  );
}
