// ---------------------------------------------------------------------------
// SP28: Inbox "Ack orphans" lane (D7).
//
// Mirrors the existing payer-rejected lane's chrome (oxblood accent,
// the same "lit from above" gradient) but renders AckOrphanRow
// shapes with per-row inline actions:
//
//   - Dismiss — drop the orphan from the lane. The backend doesn't
//     have a "dismiss" endpoint yet (the operator would normally
//     just leave the orphan in place and run a manual match later),
//     so for the first cut this only optimistically removes the row
//     client-side and refetches. The affordance is present so the
//     operator has a "clear this from my queue" gesture even
//     without backend persistence.
//   - Link to… — inline dropdown of the top-3 candidate claims
//     returned by the backend's orphan endpoint. Picking one fires
//     `api.matchAckToClaim` (the same manual-match endpoint used
//     from the AckDrawer's MatchedClaimPanel).
//
// Live-tail: opens the `claim-acks` stream via `useTailStream` so a
// manual match made elsewhere (the AckDrawer panel or another tab)
// drops the corresponding row from this lane in real time. The
// subscription lives on the lane component (not the hook) per
// `cyclone-frontend-page` convention #3.
// ---------------------------------------------------------------------------

import { useState } from "react";
import { Link2, X } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { api, type AckOrphanRow } from "@/lib/api";
import { cn } from "@/lib/utils";
import { fmt } from "@/lib/format";
import { useTailStream } from "@/hooks/useTailStream";
import { useTailStore } from "@/store/tail-store";

function AckKindBadge({ kind }: { kind: AckOrphanRow["kind"] }) {
  // Same palette as the existing AcksPage AckCodeBadge — uppercase,
  // mono, color-coded by ack kind.
  const cfg =
    kind === "999"
      ? {
          text: "hsl(var(--tt-amber))",
          bg: "hsla(36, 75%, 58%, 0.10)",
          border: "hsla(36, 75%, 58%, 0.30)",
        }
      : kind === "277ca"
        ? {
            text: "hsl(var(--tt-ink-blue))",
            bg: "hsla(212, 80%, 65%, 0.10)",
            border: "hsla(212, 80%, 65%, 0.30)",
          }
        : {
            text: "hsl(var(--tt-muted))",
            bg: "hsla(220, 8%, 50%, 0.10)",
            border: "hsla(220, 8%, 50%, 0.30)",
          };
  return (
    <span
      className="inline-flex items-center rounded-sm border px-1.5 py-0.5 mono text-[9.5px] font-semibold uppercase tracking-[0.16em]"
      style={{
        color: cfg.text,
        backgroundColor: cfg.bg,
        borderColor: cfg.border,
      }}
      data-testid="ack-orphan-kind-badge"
      data-kind={kind}
    >
      {kind === "277ca" ? "277CA" : kind.toUpperCase()}
    </span>
  );
}

function OrphanRow({
  orphan,
  onDismiss,
  onLinked,
}: {
  orphan: AckOrphanRow;
  onDismiss: (id: number) => void;
  onLinked: (orphanId: number) => void;
}) {
  const [busy, setBusy] = useState<"dismiss" | "link" | null>(null);
  const [linkError, setLinkError] = useState<string | null>(null);
  const [showCandidates, setShowCandidates] = useState(false);
  const navigate = useNavigate();

  async function linkTo(claimId: string) {
    setBusy("link");
    setLinkError(null);
    try {
      await api.matchAckToClaim(orphan.kind, orphan.id, claimId);
      setShowCandidates(false);
      onLinked(orphan.id);
    } catch (err) {
      setLinkError(err instanceof Error ? err.message : String(err));
    } finally {
      setBusy(null);
    }
  }

  return (
    <div
      className="flex flex-col gap-2 px-3 py-2.5"
      style={{ borderBottom: "1px solid hsl(220 8% 14%)" }}
      data-testid="ack-orphan-row"
      data-orphan-id={orphan.id}
    >
      {/* Top row: PCN + kind badge + parsed-at. */}
      <div className="flex items-baseline justify-between gap-2 flex-wrap">
        <div className="flex items-center gap-2 min-w-0">
          <AckKindBadge kind={orphan.kind} />
          <button
            type="button"
            onClick={() => {
              // Drill into the AckDrawer — the ack_id is encoded as
              // ?ack=<id>. The Acks page mounts the AckDrawer so
              // navigating there mirrors the rest of the inbox's
              // cross-page drill behavior.
              navigate(`/acks?ack=${encodeURIComponent(String(orphan.id))}`);
            }}
            className="mono truncate text-[12px] text-foreground hover:text-[color:var(--tt-amber)] transition-colors"
            title={`${orphan.pcn} · source batch ${orphan.sourceBatchId}`}
            data-testid="ack-orphan-pcn"
          >
            {orphan.pcn || "—"}
          </button>
        </div>
        <span className="mono text-[10px] text-[color:var(--tt-muted)]">
          {orphan.parsedAt ? fmt.dateShort(orphan.parsedAt) : "—"}
        </span>
      </div>

      {/* Source batch id — small line under the PCN for context. */}
      <div className="mono text-[10px] text-[color:var(--tt-muted)] truncate">
        {orphan.sourceBatchId}
      </div>

      {/* Action row: Dismiss + Link to… */}
      <div className="flex items-center gap-1.5 flex-wrap">
        <button
          type="button"
          onClick={() => {
            setBusy("dismiss");
            onDismiss(orphan.id);
          }}
          disabled={busy !== null}
          data-testid="ack-orphan-dismiss"
          className={cn(
            "inline-flex items-center gap-1 rounded-sm border px-1.5 py-0.5 mono text-[10px] uppercase tracking-[0.14em] font-semibold transition-colors",
            "border-[color:var(--tt-muted)]/60 bg-transparent text-[color:var(--tt-muted)] hover:bg-[color:var(--tt-oxblood)]/10 hover:border-[color:var(--tt-oxblood)]/60 hover:text-[color:var(--tt-oxblood)] disabled:opacity-50",
          )}
        >
          <X className="h-3 w-3" strokeWidth={1.75} aria-hidden />
          Dismiss
        </button>
        <button
          type="button"
          onClick={() => setShowCandidates((v) => !v)}
          disabled={busy !== null}
          data-testid="ack-orphan-link-toggle"
          className={cn(
            "inline-flex items-center gap-1 rounded-sm border px-1.5 py-0.5 mono text-[10px] uppercase tracking-[0.14em] font-semibold transition-colors",
            showCandidates
              ? "border-[color:var(--tt-amber)] bg-[color:var(--tt-amber)]/10 text-[color:var(--tt-amber)]"
              : "border-[color:var(--tt-amber)]/60 bg-transparent text-[color:var(--tt-amber)] hover:bg-[color:var(--tt-amber)]/10 disabled:opacity-50",
          )}
        >
          <Link2 className="h-3 w-3" strokeWidth={1.75} aria-hidden />
          {showCandidates ? "Cancel" : "Link to…"}
        </button>
        {orphan.candidates.length > 0 ? (
          <span className="ml-auto mono text-[10px] text-[color:var(--tt-muted)]">
            {orphan.candidates.length} candidate
            {orphan.candidates.length === 1 ? "" : "s"}
          </span>
        ) : null}
      </div>

      {/* Candidate dropdown — same compact-row treatment as the
          candidates lane. Picking one fires `matchAckToClaim` and
          refetches (the optimistic removal lands on success). */}
      {showCandidates ? (
        <div
          className="flex flex-col gap-1 rounded-sm border p-1.5"
          style={{
            background: "hsla(36, 75%, 58%, 0.04)",
            borderColor: "hsla(36, 75%, 58%, 0.30)",
          }}
          data-testid="ack-orphan-candidate-list"
        >
          {orphan.candidates.length === 0 ? (
            <p className="mono text-[10.5px] text-[color:var(--tt-muted)] px-1.5 py-1">
              No candidate claims scored.
            </p>
          ) : (
            orphan.candidates.map((c) => (
              <button
                key={c.claimId}
                type="button"
                disabled={busy !== null}
                onClick={() => void linkTo(c.claimId)}
                className={cn(
                  "flex items-center gap-2 rounded-sm px-1.5 py-1 text-left transition-colors hover:bg-[color:var(--tt-amber)]/10 disabled:opacity-50",
                )}
                data-testid="ack-orphan-candidate-option"
                data-claim-id={c.claimId}
              >
                <span className="mono text-[11px] text-foreground truncate">
                  {c.claimId}
                </span>
                <span className="ml-auto mono text-[10px] text-[color:var(--tt-muted)]">
                  score {c.score.toFixed(2)}
                </span>
              </button>
            ))
          )}
        </div>
      ) : null}

      {linkError ? (
        <p
          className="mono text-[10.5px] text-[color:var(--tt-oxblood)]"
          data-testid="ack-orphan-link-error"
        >
          {linkError}
        </p>
      ) : null}
    </div>
  );
}

export function AckOrphansLane({
  items,
  loading,
  error,
  refetch,
}: {
  items: AckOrphanRow[];
  loading: boolean;
  error: Error | null;
  refetch: () => Promise<void>;
}) {
  // Live-tail: open the claim-acks stream. When a manual match
  // resolves anywhere in the app, the corresponding row lands in
  // the slice and we drop it from the orphan lane optimistically.
  useTailStream("claim-acks");
  const claimAckOrder = useTailStore((s) => s.claimAckOrder);
  const claimAcks = useTailStore((s) => s.claimAcks);

  // Set of (kind, ack_id) tuples that have just been linked — when
  // the live-tail store grows, any orphan whose (kind, id) is now
  // present drops out of the lane until the next refetch confirms.
  const [optimisticDismissed, setOptimisticDismissed] = useState<Set<string>>(
    () => new Set(),
  );

  function orphanKey(o: AckOrphanRow): string {
    return `${o.kind}:${o.id}`;
  }

  function dropLocal(id: number) {
    setOptimisticDismissed((prev) => {
      const orphan = items.find((o) => o.id === id);
      if (!orphan) return prev;
      const next = new Set(prev);
      next.add(orphanKey(orphan));
      return next;
    });
    // Refetch in the background so the lane converges with the
    // server. The optimistic drop is purely visual.
    void refetch();
  }

  // Live-tail: drop orphans whose (kind, id) just landed in the
  // claimAcks slice (i.e. a manual match fired elsewhere). We only
  // act when claimAckOrder grows — the store is the source of truth.
  const [prevOrderLen, setPrevOrderLen] = useState(claimAckOrder.length);
  if (claimAckOrder.length > prevOrderLen) {
    // Find newly-added links and dismiss orphans by (kind, ack_id).
    const newKeys = claimAckOrder
      .slice(prevOrderLen)
      .map((i) => claimAcks[i])
      .filter((row): row is NonNullable<typeof row> => Boolean(row))
      .map((row) => `${row.ackKind}:${row.ackId}`);
    if (newKeys.length > 0) {
      const matched = items.filter((o) => newKeys.includes(orphanKey(o)));
      if (matched.length > 0) {
        setOptimisticDismissed((prev) => {
          const next = new Set(prev);
          matched.forEach((o) => next.add(orphanKey(o)));
          return next;
        });
      }
    }
    setPrevOrderLen(claimAckOrder.length);
  }

  const visible = items.filter((o) => !optimisticDismissed.has(orphanKey(o)));

  return (
    <section
      className="flex-1 min-w-[280px] flex flex-col rounded-md overflow-hidden"
      style={{
        // Same "lit from above" gradient as the other lanes, with an
        // oxblood hairline border to signal "this needs operator
        // attention" without competing with the dashboard amber.
        background:
          "linear-gradient(180deg, hsl(220 18% 11.5%) 0%, hsl(220 18% 9.5%) 55%, hsl(220 18% 8.5%) 100%)",
        border: "1px solid hsl(220 8% 18%)",
        boxShadow:
          "inset 0 1px 0 0 hsl(0 0% 100% / 0.05), 0 1px 2px 0 hsl(0 0% 0% / 0.4)",
      }}
      data-lane="ack-orphans"
    >
      <header
        className="px-3 py-2.5 flex items-baseline justify-between gap-2"
        style={{
          borderBottom: "1px solid hsl(220 8% 16%)",
          background:
            "linear-gradient(180deg, hsl(220 16% 11%) 0%, hsl(220 18% 9%) 100%)",
        }}
      >
        <div className="flex items-center gap-2">
          <span
            aria-hidden
            className="inline-block h-2 w-2 rounded-full"
            style={{
              background: "var(--tt-oxblood)",
              boxShadow: "0 0 8px var(--tt-oxblood)",
            }}
          />
          <h2
            className="mono uppercase"
            style={{
              color: "var(--tt-ink)",
              fontSize: 11,
              letterSpacing: "0.18em",
              fontWeight: 600,
            }}
          >
            ACK ORPHANS
          </h2>
        </div>
        <span
          className="mono tabular-nums"
          style={{
            color: "var(--tt-amber)",
            fontSize: 18,
            fontWeight: 600,
            lineHeight: 1,
          }}
          data-testid="ack-orphans-count"
        >
          {visible.length}
        </span>
      </header>

      {error ? (
        <p
          className="px-4 py-6 mono text-center"
          style={{ color: "var(--tt-oxblood)", fontSize: 11 }}
          data-testid="ack-orphans-error"
        >
          {error.message}
        </p>
      ) : loading ? (
        <p
          className="px-4 py-6 mono text-center"
          style={{ color: "var(--tt-muted)", fontSize: 11 }}
          data-testid="ack-orphans-loading"
        >
          loading orphans…
        </p>
      ) : visible.length === 0 ? (
        <p
          className="px-4 py-6 mono text-center"
          style={{ color: "var(--tt-muted)", fontSize: 11, letterSpacing: "0.06em" }}
          data-testid="ack-orphans-empty"
        >
          No orphan acks.
        </p>
      ) : (
        <div className="w-full">
          {visible.map((orphan) => (
            <OrphanRow
              key={orphanKey(orphan)}
              orphan={orphan}
              onDismiss={dropLocal}
              onLinked={() => dropLocal(orphan.id)}
            />
          ))}
        </div>
      )}
    </section>
  );
}