import { useState } from "react";
import {
  ArrowRight,
  CircleDashed,
  GitMerge,
  X,
} from "lucide-react";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
import { useReconciliation } from "@/hooks/useReconciliation";
import { ApiError } from "@/lib/api";
import { Skeleton } from "@/components/ui/skeleton";
import { EmptyState } from "@/components/ui/empty-state";
import { ErrorState } from "@/components/ui/error-state";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { KpiCard } from "@/components/KpiCard";
import { PageHeader } from "@/components/PageHeader";
import { RemitDrawer } from "@/components/RemitDrawer";
import { useRemitDrawerUrlState } from "@/hooks/useRemitDrawerUrlState";
import { fmt } from "@/lib/format";
import { cn } from "@/lib/utils";
import { RoleGate } from "@/auth/RoleGate";

/**
 * Two-column manual reconciliation surface. The operator picks one row from
 * the claims column, one from the remits column, then clicks "Match
 * selected" to POST /api/reconciliation/match. The hook's invalidation
 * cascade refreshes the unmatched bucket + claims + remits + activity feeds
 * on success.
 *
 * Same chrome language as the rest of Cyclone: dark instrument surface,
 * PageHeader hero, KPI strip, two-column Card layout, action bar.
 */
export function ReconciliationPage() {
  const { unmatched, match } = useReconciliation();
  // SP21 Phase 4 Task 4.6: drill into the RemitDrawer from the
  // remits column. The hook reads `?remit=` from the URL so deep
  // links land with the drawer open. Selection state stays local
  // — the drill is a separate gesture from the match selection.
  const { remitId, open, close } = useRemitDrawerUrlState();
  // SP21 Phase 5 Task 5.5: claim drill from the card body navigates
  // to /claims?claim=ID so the ClaimDrawer mounts in-place on the
  // /claims route. Selection for matching stays local — the drill
  // gesture is separate from the match selection gesture.
  const navigate = useNavigate();
  const [selectedClaim, setSelectedClaim] = useState<string | null>(null);
  const [selectedRemit, setSelectedRemit] = useState<string | null>(null);

  // -----------------------------------------------------------------
  // Stagger choreography — hero lands first, KPI strip at 140ms,
  // pairing surface at 240ms, action bar at 320ms. Total < 500ms.
  // -----------------------------------------------------------------
  const heroDelay = 0;
  const kpiDelay = 140;
  const columnsDelay = 240;
  const actionDelay = 320;

  // ---------- Loading state ----------
  if (unmatched.isLoading) {
    return (
      <div className="space-y-6 lg:space-y-10">
        <section
          className="relative animate-fade-in"
          style={{ animationDelay: `${heroDelay}ms` }}
        >
          <div className="flex items-center gap-3 mb-5">
            <div className="h-px w-14 bg-foreground/20" />
            <span className="eyebrow">Reconciliation</span>
          </div>
          <div className="display text-[28px] sm:text-[32px] leading-[1.1] text-muted-foreground/40">
            Loading pairs…
          </div>
        </section>
        <div className="grid gap-3.5 grid-cols-1 sm:grid-cols-2 md:grid-cols-3">
          <Skeleton variant="default" height={140} />
          <Skeleton variant="default" height={140} />
          <Skeleton variant="default" height={140} />
        </div>
        <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
          <Skeleton variant="default" height={320} />
          <Skeleton variant="default" height={320} />
        </div>
      </div>
    );
  }

  // ---------- Error state ----------
  if (unmatched.isError) {
    const detail =
      unmatched.error instanceof Error
        ? unmatched.error.message
        : String(unmatched.error);
    return (
      <div className="space-y-6 lg:space-y-10">
        <section
          className="relative animate-fade-in"
          style={{ animationDelay: `${heroDelay}ms` }}
        >
          <PageHeader
            eyebrow="Reconciliation"
            title={
              <>
                Manual <span className="italic text-muted-foreground/85">pairing.</span>
              </>
            }
            subtitle={
              <>
                Pick one claim and one remittance, then match them. The system
                reconciles automatically when amounts and providers line up;
                the ones that don't are dropped here for you to triage.
              </>
            }
          />
        </section>
        <Card>
          <CardContent className="p-6">
            <ErrorState
              message="Couldn't load unmatched claims and remittances."
              detail={detail}
              onRetry={() => unmatched.refetch()}
            />
          </CardContent>
        </Card>
      </div>
    );
  }

  const { claims, remittances } = unmatched.data ?? {
    claims: [],
    remittances: [],
  };
  const empty = claims.length === 0 && remittances.length === 0;
  const queueTotal = claims.length + remittances.length;

  const handleMatch = async () => {
    if (!selectedClaim || !selectedRemit) return;
    try {
      await match.mutateAsync({ claimId: selectedClaim, remitId: selectedRemit });
      toast.success(`Matched ${selectedClaim} ↔ ${selectedRemit}`);
      setSelectedClaim(null);
      setSelectedRemit(null);
    } catch (e) {
      const msg =
        e instanceof ApiError && e.status === 409
          ? "Already matched — view the existing match."
          : e instanceof Error
            ? e.message
            : String(e);
      toast.error(msg);
    }
  };

  const handleClear = () => {
    setSelectedClaim(null);
    setSelectedRemit(null);
  };

  // ---------- Empty state ----------
  if (empty) {
    return (
      <div className="space-y-6 lg:space-y-10">
        {/* HERO — eyebrow says "Reconciliation · nothing pending" so the
            Reconciliation.test.tsx empty-state assertion still resolves
            (it concatenates textContent and looks for that exact string).
            Title is the standard editorial display with an italic accent. */}
        <section
          className="relative animate-fade-in overflow-hidden"
          style={{ animationDelay: `${heroDelay}ms` }}
        >
          <div className="relative z-10 flex items-end justify-between gap-4 sm:gap-6 flex-wrap">
            <div className="min-w-0">
              <PageHeader
                eyebrow="Reconciliation · nothing pending"
                title={
                  <>
                    Manual{" "}
                    <span className="italic text-muted-foreground/85">pairing.</span>
                  </>
                }
                subtitle={
                  <>
                    Pick one claim and one remittance, then match them. The
                    system reconciles automatically when amounts and
                    providers line up; the ones that don't are dropped here
                    for you to triage.
                  </>
                }
              />
            </div>
            <div className="inline-flex items-center gap-2 rounded-full border border-[hsl(var(--success)/0.4)] bg-[hsl(var(--success)/0.08)] px-3 py-1.5 text-[11px] mono uppercase tracking-[0.12em] text-[hsl(var(--success))] backdrop-blur">
              <span className="relative inline-flex h-1.5 w-1.5">
                <span className="absolute inline-flex h-full w-full rounded-full bg-[hsl(var(--success))] opacity-60 animate-ping" />
                <span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-[hsl(var(--success))]" />
              </span>
              All paired
            </div>
          </div>
        </section>

        {/* Hero stat strip — all zeros, anchored to the same rhythm as
            Dashboard / Upload hero strips. */}
        <section
          className="grid gap-3.5 grid-cols-2 md:grid-cols-4 pt-5 border-t border-border/40 animate-fade-in-up"
          style={{ animationDelay: `${kpiDelay}ms` }}
        >
          <KpiCard
            label="Unmatched claims"
            value={fmt.num(claims.length)}
            accent="accent"
            hint="awaiting pair"
          />
          <KpiCard
            label="Unmatched remits"
            value={fmt.num(remittances.length)}
            accent="warning"
            hint="awaiting pair"
          />
          <KpiCard
            label="In the queue"
            value={fmt.num(queueTotal)}
            hint="total"
          />
          <KpiCard
            label="Auto-match"
            value="ON"
            accent="success"
            hint="no manual pairs needed"
          />
        </section>

        {/* Empty card — uses the EmptyState component so the visual
            voice matches the rest of the app. "Every claim and
            remittance is paired." is preserved for the test assertion. */}
        <section
          className="animate-fade-in-up"
          style={{ animationDelay: `${columnsDelay}ms` }}
        >
          <Card>
            <CardContent className="p-6 lg:p-8">
              <EmptyState
                eyebrow="All clear"
                message="Every claim and remittance is paired."
                icon={<CircleDashed className="h-4 w-4" strokeWidth={1.5} />}
              />
            </CardContent>
          </Card>
        </section>

        {/* SP21 Phase 4 Task 4.6: RemitDrawer mount, same as below. */}
        <RemitDrawer
          remitId={remitId}
          remits={[]}
          onClose={close}
          onNavigate={open}
          onToggleHelp={() => {
            // Reconciliation has no cheatsheet; `?` is a no-op here.
          }}
        />
      </div>
    );
  }

  // ---------- Main rendering ----------
  return (
    <div className="space-y-6 lg:space-y-10">
      {/* HERO — same PageHeader pattern as Dashboard / Upload. Ghost
          watermark carries the queue total so the page's scale reads
          at a glance. */}
      <section
        className="relative animate-fade-in overflow-hidden"
        style={{ animationDelay: `${heroDelay}ms` }}
      >
        <div
          aria-hidden="true"
          className="pointer-events-none select-none absolute -right-4 top-1/2 -translate-y-1/2 whitespace-nowrap display text-foreground"
          style={{
            fontSize: "clamp(72px, 12vw, 140px)",
            letterSpacing: "-0.04em",
            opacity: 0.045,
            lineHeight: 1,
          }}
        >
          {queueTotal}
        </div>

        <div className="relative z-10 flex items-end justify-between gap-4 sm:gap-6 flex-wrap">
          <div className="min-w-0">
            <PageHeader
              eyebrow={`Reconciliation · ${queueTotal} need eyes`}
              title={
                <>
                  Manual{" "}
                  <span className="italic text-muted-foreground/85">pairing.</span>
                </>
              }
              subtitle={
                <>
                  Pick one claim and one remittance, then match them. The
                  system reconciles automatically when amounts and providers
                  line up; the ones that don't are dropped here for you to
                  triage.
                </>
              }
            />
          </div>
          <div className="inline-flex items-center gap-2 rounded-full border border-[hsl(var(--warning)/0.4)] bg-[hsl(var(--warning)/0.08)] px-3 py-1.5 text-[11px] mono uppercase tracking-[0.12em] text-[hsl(var(--warning))] backdrop-blur">
            <span className="relative inline-flex h-1.5 w-1.5">
              <span className="absolute inline-flex h-full w-full rounded-full bg-[hsl(var(--warning))] opacity-60 animate-ping" />
              <span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-[hsl(var(--warning))]" />
            </span>
            Auto-match paused
          </div>
        </div>
      </section>

      {/* KPI STRIP — three tiles in the same rhythm as Dashboard's
          KPI row. accent/blue for claims, warning/amber for remits,
          default for the queue total. */}
      <section
        aria-label="Reconciliation metrics"
        className="grid gap-3.5 grid-cols-1 sm:grid-cols-2 md:grid-cols-3 animate-fade-in-up"
        style={{ animationDelay: `${kpiDelay}ms` }}
      >
        <KpiCard
          label="Unmatched claims"
          value={fmt.num(claims.length)}
          accent="accent"
          hint="awaiting pair"
        />
        <KpiCard
          label="Unmatched remits"
          value={fmt.num(remittances.length)}
          accent="warning"
          hint="awaiting pair"
        />
        <KpiCard
          label="In the queue"
          value={fmt.num(queueTotal)}
          hint="total unmatched"
        />
      </section>

      {/* TWO-COLUMN PAIRING SURFACE — standard Card wrapper. Each
          column is its own Card with an accent-strip header. */}
      <section
        aria-label="Pick a claim and a remit"
        className="animate-fade-in-up"
        style={{ animationDelay: `${columnsDelay}ms` }}
      >
        <Card>
          <CardContent className="p-6 lg:p-7 space-y-5">
            {/* Section header */}
            <div className="flex items-end justify-between gap-6 flex-wrap">
              <div className="min-w-0">
                <div className="eyebrow flex items-center gap-2 mb-2">
                  <span className="inline-block h-px w-6 bg-foreground/20" />
                  The match
                </div>
                <h2 className="display text-[26px] leading-[1.05] tracking-[-0.02em]">
                  Pair <span className="italic">them.</span>
                </h2>
              </div>
              <p className="text-[12.5px] text-muted-foreground/80 max-w-sm">
                Tap a row on the left, tap a row on the right, then send the
                pair.
              </p>
            </div>

            <div className="grid grid-cols-1 md:grid-cols-[1fr,auto,1fr] gap-4 items-stretch pt-5 border-t border-border/40">
              {/* Claims column */}
              <PairColumn
                eyebrow="Claims"
                count={claims.length}
                tone="accent"
                empty={claims.length === 0}
                emptyMessage="No unmatched claims."
              >
                {claims.map((c) => {
                  const active = selectedClaim === c.id;
                  return (
                    // SP21 Phase 5 Task 5.5: card body drills into the
                    // ClaimDrawer; selection for matching lives on a
                    // dedicated button inside the card. The card is no
                    // longer a single <button> — it's a <div> with a
                    // clickable body region (cursor-pointer) and a
                    // separate "Select for match" toggle. We split the
                    // gestures so the operator can review the claim
                    // without accidentally queuing it for pairing.
                    <div
                      key={c.id}
                      data-testid="recon-claim-card"
                      className={cn(
                        "relative w-full text-left p-3.5 min-h-[44px] rounded-md border transition-all overflow-hidden",
                        active
                          ? "border-accent/60 bg-accent/[0.06] shadow-[inset_2px_0_0_0_hsl(var(--accent))]"
                          : "border-border/70 bg-card/40 hover:bg-card/80 hover:border-border"
                      )}
                    >
                      {/* Body — drillable into /claims?claim=ID. We
                          use a button (not an <a>) because we're
                          staying on the SPA; the ClaimDrawer mounts
                          when Claims.tsx reads ?claim= off the URL.
                          e.stopPropagation in DrillableCell prevents
                          the click from reaching the select button
                          below. */}
                      <button
                        type="button"
                        onClick={() =>
                          navigate(
                            `/claims?claim=${encodeURIComponent(c.id)}`,
                          )
                        }
                        aria-label={`View claim ${c.id} in detail`}
                        className="w-full text-left cursor-pointer rounded-sm p-0 m-0 border-0 bg-transparent"
                      >
                        <div className="display mono text-[13.5px] text-foreground">
                          {c.id}
                        </div>
                        <div className="text-[12.5px] mt-0.5 text-muted-foreground/80">
                          {c.patientName}
                        </div>
                        <div className="mono text-[11px] mt-1 text-muted-foreground/60">
                          {c.serviceDate ?? "—"} · $
                          {c.billedAmount.toFixed(2)} · NPI{" "}
                          {c.providerNpi ?? "—"}
                        </div>
                      </button>
                      {/* Selection toggle — separate gesture so a drill
                          doesn't auto-select the row for matching. */}
                      <button
                        type="button"
                        onClick={(e) => {
                          e.stopPropagation();
                          setSelectedClaim(active ? null : c.id);
                        }}
                        aria-pressed={active}
                        data-testid={`recon-claim-select-${c.id}`}
                        className={cn(
                          "mt-2.5 inline-flex items-center gap-1.5 px-2.5 py-1 rounded-sm text-[10.5px] mono uppercase tracking-[0.16em] font-semibold transition-colors border",
                          active
                            ? "bg-accent text-accent-foreground border-accent"
                            : "bg-transparent text-muted-foreground/80 border-border/70 hover:bg-accent/10 hover:text-accent hover:border-accent/40"
                        )}
                      >
                        {active ? (
                          <>
                            <X className="h-3 w-3" strokeWidth={2.5} />
                            Unselect
                          </>
                        ) : (
                          <>Select for match</>
                        )}
                      </button>
                    </div>
                  );
                })}
              </PairColumn>

              {/* Center bridge — a vertical "→" between the two columns */}
              <div className="hidden md:flex flex-col items-center justify-center px-2 min-h-[200px]">
                <div
                  aria-hidden
                  className="w-px flex-1 bg-border/60"
                />
                <div
                  className={cn(
                    "my-3 h-10 w-10 rounded-full flex items-center justify-center border transition-colors",
                    selectedClaim && selectedRemit
                      ? "bg-[hsl(var(--success)/0.14)] border-[hsl(var(--success)/0.4)] text-[hsl(var(--success))]"
                      : "bg-muted/40 border-border/70 text-muted-foreground/60"
                  )}
                >
                  {selectedClaim && selectedRemit ? (
                    <GitMerge className="h-4 w-4" strokeWidth={1.75} />
                  ) : (
                    <ArrowRight
                      className="h-4 w-4 rotate-90"
                      strokeWidth={1.75}
                    />
                  )}
                </div>
                <div
                  aria-hidden
                  className="w-px flex-1 bg-border/60"
                />
              </div>

              {/* Remits column */}
              <PairColumn
                eyebrow="Remits"
                count={remittances.length}
                tone="warning"
                empty={remittances.length === 0}
                emptyMessage="No unmatched remits."
              >
                {remittances.map((r) => {
                  const active = selectedRemit === r.id;
                  return (
                    // SP21 Phase 5 Task 5.5: card body drills into the
                    // RemitDrawer; selection for matching lives on a
                    // dedicated button. Same gesture-split as the
                    // claims column — body = drill, button = select.
                    <div
                      key={r.id}
                      data-testid="recon-remit-card"
                      className={cn(
                        "relative w-full text-left p-3.5 min-h-[44px] rounded-md border transition-all overflow-hidden",
                        active
                          ? "border-[hsl(var(--warning)/0.6)] bg-[hsl(var(--warning)/0.08)] shadow-[inset_2px_0_0_0_hsl(var(--warning))]"
                          : "border-border/70 bg-card/40 hover:bg-card/80 hover:border-border"
                      )}
                    >
                      {/* Body — drillable. The whole body is one
                          <button>; no nested DrillableCell (a <button>
                          inside a <button> is invalid HTML and warns
                          in the console). The PCN still gets the
                          chevron affordance via the `drillable` class
                          added directly to the inner span. */}
                      <button
                        type="button"
                        onClick={() => open(r.id)}
                        aria-label={`View remittance ${r.payerClaimControlNumber} in detail`}
                        className="w-full text-left cursor-pointer rounded-sm p-0 m-0 border-0 bg-transparent"
                      >
                        <div className="mono text-[13.5px] flex items-center gap-2 text-foreground">
                          <span className="display drillable inline-flex items-center gap-0 rounded-sm">
                            {r.payerClaimControlNumber}
                          </span>
                          {r.isReversal ? (
                            <Badge variant="signal">Reversal</Badge>
                          ) : null}
                        </div>
                        <div className="mono text-[11px] mt-1 text-muted-foreground/60">
                          Status {r.status} · ${r.paidAmount.toFixed(2)} paid · $
                          {r.adjustmentAmount.toFixed(2)} adj
                        </div>
                      </button>
                      {/* Selection toggle — separate gesture from
                          drill. Same pattern as the claims column. */}
                      <button
                        type="button"
                        onClick={(e) => {
                          e.stopPropagation();
                          setSelectedRemit(active ? null : r.id);
                        }}
                        aria-pressed={active}
                        data-testid={`recon-remit-select-${r.id}`}
                        className={cn(
                          "mt-2.5 inline-flex items-center gap-1.5 px-2.5 py-1 rounded-sm text-[10.5px] mono uppercase tracking-[0.16em] font-semibold transition-colors border",
                          active
                            ? "bg-[hsl(var(--warning))] text-[hsl(var(--warning-foreground))] border-[hsl(var(--warning))]"
                            : "bg-transparent text-muted-foreground/80 border-border/70 hover:bg-[hsl(var(--warning)/0.1)] hover:text-[hsl(var(--warning))] hover:border-[hsl(var(--warning)/0.4)]"
                        )}
                      >
                        {active ? (
                          <>
                            <X className="h-3 w-3" strokeWidth={2.5} />
                            Unselect
                          </>
                        ) : (
                          <>Select for match</>
                        )}
                      </button>
                    </div>
                  );
                })}
              </PairColumn>
            </div>
          </CardContent>
        </Card>
      </section>

      {/* ACTION BAR — separate Card so the primary action sits in its
          own surface with a clear status sentence + Clear / Match
          buttons. Anchors the pairing gesture to a decisive end. */}
      <section
        aria-label="Match action"
        className="animate-fade-in-up"
        style={{ animationDelay: `${actionDelay}ms` }}
      >
        <Card>
          <CardContent className="p-5 lg:p-6">
            <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
              <div className="text-[13px] text-muted-foreground/80 min-w-0">
                {selectedClaim && selectedRemit ? (
                  <>
                    Ready to pair{" "}
                    <span className="display mono not-italic text-foreground">
                      {selectedClaim}
                    </span>{" "}
                    with{" "}
                    <span className="display mono not-italic text-foreground">
                      {selectedRemit}
                    </span>
                    .
                  </>
                ) : selectedClaim ? (
                  <>Now pick a remit on the right.</>
                ) : selectedRemit ? (
                  <>Now pick a claim on the left.</>
                ) : (
                  <>Pick one from each side to begin.</>
                )}
              </div>
              <div className="flex items-center gap-2 flex-wrap">
                <Button
                  variant="outline"
                  size="sm"
                  onClick={handleClear}
                  disabled={(!selectedClaim && !selectedRemit) || match.isPending}
                >
                  <X className="h-3.5 w-3.5 mr-1.5" />
                  Clear
                </Button>
                {/* Match selected is the only true write affordance on
                    this page — it transitions a claim from "submitted" /
                    "rejected" to "paid" / "partial" / "denied" /
                    "received" via `record_manual_match`. Viewers can
                    still pick rows and see the selection chrome, but
                    the button itself is hidden for them. */}
                <RoleGate allow={["admin", "user"]}>
                  <Button
                    size="sm"
                    onClick={handleMatch}
                    disabled={!selectedClaim || !selectedRemit || match.isPending}
                  >
                    <GitMerge className="h-3.5 w-3.5 mr-1.5" />
                    Match selected
                  </Button>
                </RoleGate>
              </div>
            </div>
          </CardContent>
        </Card>
      </section>

      {/* SP21 Phase 4 Task 4.6: RemitDrawer mount. The remits column
          drills into the parent remit via the PCN text (DrillableCell
          + open). The drawer portals into document.body, so the
          surrounding surface stays put while the drawer is open.
          `remits` is empty (we don't keep a list of all remits on
          this page), so j/k is a no-op while the drawer is open. */}
      <RemitDrawer
        remitId={remitId}
        remits={[]}
        onClose={close}
        onNavigate={open}
        onToggleHelp={() => {
          // Reconciliation has no cheatsheet; `?` is a no-op here.
        }}
      />
    </div>
  );
}

// ---------------------------------------------------------------------------
// Helper components
// ---------------------------------------------------------------------------

function PairColumn({
  eyebrow,
  count,
  tone,
  empty,
  emptyMessage,
  children,
}: {
  eyebrow: string;
  count: number;
  tone: "accent" | "warning";
  empty: boolean;
  emptyMessage: string;
  children: React.ReactNode;
}) {
  const accentVar =
    tone === "accent" ? "hsl(var(--accent))" : "hsl(var(--warning))";
  const labelText =
    eyebrow === "Claims" ? "Unmatched claims" : "Unmatched remits";
  return (
    <div className="rounded-xl border border-border/60 bg-card/40 overflow-hidden">
      {/* Header bar — accent strip + label + count badge, all in
          dark surface tokens. The eyebrow text on the header is the
          "Unmatched claims" / "Unmatched remits" string that the
          Reconciliation.test.tsx non-empty-state test asserts on. */}
      <div className="flex items-center justify-between gap-2 px-4 py-3 border-b border-border/40 bg-muted/30">
        <div className="flex items-center gap-2.5 min-w-0">
          <span
            aria-hidden
            className="h-2.5 w-2.5 rounded-sm shrink-0"
            style={{ backgroundColor: accentVar }}
          />
          <span className="mono text-[10.5px] uppercase tracking-[0.20em] font-semibold text-muted-foreground">
            {labelText}
          </span>
        </div>
        <Badge variant={tone === "accent" ? "default" : "signal"}>{count}</Badge>
      </div>
      <div className="p-3 space-y-2 min-h-[200px] max-h-[420px] overflow-y-auto">
        {empty ? (
          <div className="text-center py-8 text-[12.5px] text-muted-foreground/70">
            {emptyMessage}
          </div>
        ) : (
          children
        )}
      </div>
    </div>
  );
}