"use client";

import { useCallback, useEffect, useMemo, useState } from "react";
import Link from "next/link";
import { CustomerPicker } from "@/components/CustomerPicker";
import { DiscardBatchCard } from "@/components/DiscardBatchCard";
import {
  confirmPolicyOcrBatch,
  discardPolicyOcrBatch,
  getPolicyOcrBatch,
  listCustomers,
  listPolicyOcrDocuments,
  policyOcrDocumentUrl,
  rejectPolicyOcrDocument,
  reviewPolicyOcrDocument,
} from "@/lib/api";
import { useCan } from "@/lib/abilities";
import { formatDate, formatMoney } from "@/lib/labels";
import type {
  CustomerListItem,
  PolicyOcrBatchDetail,
  PolicyOcrConfirmDocument,
  PolicyOcrCoverage,
  PolicyOcrCustomerSuggestion,
  PolicyOcrDocument,
  PolicyOcrReviewInput,
} from "@/lib/types";

/** Document and batch statuses share this map — the two enums have no
 *  overlapping members, and the header renders a batch status through it. */
const STATUS_LABEL: Record<string, string> = {
  PENDING_OCR: "Pendiente",
  OCR_FAILED: "Falló OCR",
  NEEDS_REVIEW: "Para revisar",
  MATCHED: "Listo",
  CONFIRMED: "Confirmado",
  POSTED: "Aplicado",
  REJECTED: "Rechazado",
  UPLOADED: "Recibido",
  PROCESSING: "Procesando…",
  READY_FOR_REVIEW: "Listo para revisar",
  COMPLETED: "Aplicado",
  FAILED: "Falló",
  DISCARDED: "Descartado",
};

const OPEN_FIRST = [
  "NEEDS_REVIEW",
  "MATCHED",
  "CONFIRMED",
  "PENDING_OCR",
  "OCR_FAILED",
  "REJECTED",
  "POSTED",
];

type EditMap = Record<string, PolicyOcrConfirmDocument | undefined>;

export function PolicyOcrReview({ id }: { id: string }) {
  const canReview = useCan("policy:ocr-review");
  const [batch, setBatch] = useState<PolicyOcrBatchDetail | null>(null);
  const [docs, setDocs] = useState<PolicyOcrDocument[]>([]);
  const [edits, setEdits] = useState<EditMap>({});
  const [customerIndex, setCustomerIndex] = useState<Record<string, CustomerListItem>>({});
  const [error, setError] = useState<string | null>(null);
  const [loading, setLoading] = useState(true);
  const [submitting, setSubmitting] = useState(false);
  const [discarding, setDiscarding] = useState(false);

  const load = useCallback(async () => {
    try {
      const [b, d, c] = await Promise.all([
        getPolicyOcrBatch(id),
        listPolicyOcrDocuments(id),
        canReview ? listCustomers({ pageSize: 200 }).then((r) => r.items) : Promise.resolve([]),
      ]);
      setBatch(b);
      setDocs(d);
      setCustomerIndex(Object.fromEntries(c.map((x) => [x.id, x])));
      setError(null);
    } catch (e) {
      setError((e as Error)?.message ?? "No se pudo cargar el lote.");
    } finally {
      setLoading(false);
    }
  }, [id, canReview]);

  useEffect(() => {
    void load();
  }, [load]);

  const processing = batch?.status === "PROCESSING" || batch?.status === "UPLOADED";
  useEffect(() => {
    if (!processing) return;
    const t = setInterval(() => void load(), 4000);
    return () => clearInterval(t);
  }, [processing, load]);

  const sorted = useMemo(
    () =>
      [...docs].sort(
        (a, b) =>
          OPEN_FIRST.indexOf(a.status) - OPEN_FIRST.indexOf(b.status) ||
          a.pageNumber - b.pageNumber,
      ),
    [docs],
  );

  const readyCount = Object.values(edits).filter(Boolean).length;

  function setEdit(docId: string, edit: PolicyOcrConfirmDocument) {
    setEdits((prev) => ({ ...prev, [docId]: edit }));
  }

  async function onConfirm() {
    if (!batch) return;
    const payload: PolicyOcrConfirmDocument[] = [];
    for (const d of docs) {
      const edit = edits[d.id];
      if (!edit) continue;
      if (!edit.policyId && !edit.customerId) {
        setError(`Página ${d.pageNumber}: falta cliente o póliza destino.`);
        return;
      }
      payload.push(edit);
    }
    if (!payload.length) {
      setError("No hay documentos revisados. Guarde cada página antes de aplicar.");
      return;
    }
    setSubmitting(true);
    setError(null);
    try {
      await confirmPolicyOcrBatch(batch.id, { documents: payload });
      setEdits({});
      await load();
    } catch (e) {
      setError((e as Error)?.message ?? "No se pudo aplicar el lote.");
    } finally {
      setSubmitting(false);
    }
  }

  async function onDiscard() {
    if (!batch) return;
    setDiscarding(true);
    setError(null);
    try {
      await discardPolicyOcrBatch(batch.id);
      setEdits({});
      await load();
    } catch (e) {
      setError((e as Error)?.message ?? "No se pudo descartar el lote.");
    } finally {
      setDiscarding(false);
    }
  }

  if (loading) return <div className="state-box">Cargando…</div>;
  if (!batch) return <div className="state-box state-error">{error ?? "No encontrado."}</div>;

  const appliedCount = docs.filter((d) => d.status === "POSTED").length;
  // Discarding is only offered while the batch can still be abandoned whole:
  // nothing applied yet, and not already discarded.
  const canDiscard =
    canReview &&
    batch.status !== "DISCARDED" &&
    batch.status !== "COMPLETED" &&
    appliedCount === 0;

  return (
    <div className="stack">
      <header className="page-head">
        <div>
          <h1 className="page-title">
            Pólizas — {batch.provider}
            {batch.label ? ` · ${batch.label}` : ""}
          </h1>
          <p className="page-sub">
            {formatDate(batch.createdAt)} · {docs.length} página(s) ·{" "}
            {STATUS_LABEL[batch.status] ?? batch.status}
          </p>
        </div>
        {/* Back to the capture screen this batch was uploaded from, not to
            the policy list — same as the statement review screen, which
            returns to /recibos. */}
        <Link className="btn btn-ghost" href="/polizas/captura">
          Volver a captura
        </Link>
      </header>

      {processing && <div className="state-box">Procesando…</div>}

      {error && <div className="state-box state-error">{error}</div>}

      {canReview && readyCount > 0 && (
        <section className="card" style={{ padding: 16 }}>
          <h2 className="section-title" style={{ marginTop: 0 }}>
            Aplicar lote
          </h2>
          <p className="page-sub" style={{ marginBottom: 12 }}>
            {readyCount} página(s) revisada(s). Se creará o actualizará la póliza
            y, si marcó la casilla, se registrará la prima en el estado de
            cuenta.
          </p>
          <button
            type="button"
            className="btn btn-primary"
            disabled={submitting}
            onClick={onConfirm}
          >
            {submitting ? "Aplicando…" : "Aplicar"}
          </button>
        </section>
      )}

      {canDiscard && (
        <DiscardBatchCard
          busy={discarding}
          onDiscard={onDiscard}
          pageCount={docs.length}
          what="póliza"
        />
      )}

      <section className="stack">
        {sorted.map((doc) => (
          <DocumentRow
            key={doc.id}
            doc={doc}
            customerIndex={customerIndex}
            canReview={canReview}
            onSave={async (edit) => {
              await reviewPolicyOcrDocument(doc.id, edit.reviewInput);
              setEdit(doc.id, edit.confirmInput);
              await load();
            }}
            onReject={async () => {
              await rejectPolicyOcrDocument(doc.id);
              setEdits((prev) => {
                const { [doc.id]: _, ...rest } = prev;
                return rest;
              });
              await load();
            }}
          />
        ))}
      </section>
    </div>
  );
}

interface RowSaved {
  reviewInput: PolicyOcrReviewInput;
  confirmInput: PolicyOcrConfirmDocument;
}

interface DocumentRowProps {
  doc: PolicyOcrDocument;
  customerIndex: Record<string, CustomerListItem>;
  canReview: boolean;
  onSave: (saved: RowSaved) => Promise<void>;
  onReject: () => Promise<void>;
}

function DocumentRow({ doc, customerIndex, canReview, onSave, onReject }: DocumentRowProps) {
  const [v, setV] = useState({
    policyNumber: doc.extractedPolicyNumber ?? "",
    insuredName: doc.extractedInsuredName ?? "",
    additionalInsured: doc.extractedAdditionalInsured ?? "",
    agentName: doc.extractedAgentName ?? "",
    legalAddress: doc.extractedLegalAddress ?? "",
    zip: doc.extractedZip ?? "",
    policyFrom: doc.extractedPolicyFrom?.slice(0, 10) ?? "",
    policyTo: doc.extractedPolicyTo?.slice(0, 10) ?? "",
    policyDate: doc.extractedPolicyDate?.slice(0, 10) ?? "",
    currency: doc.extractedCurrency ?? "USD",
    netPremium: doc.extractedNetPremium ?? "",
    total: doc.extractedTotal ?? "",
    premiumPayment: doc.extractedPremiumPayment ?? "",
    coveragePeriodDays: doc.extractedCoveragePeriodDays?.toString() ?? "",
    postPremium: doc.extractedNetPremium != null && Number(doc.extractedNetPremium) > 0,
  });
  const [customerId, setCustomerId] = useState(
    doc.matchedCustomer?.id ?? doc.matchedPolicy?.customerId ?? "",
  );
  const [customerName, setCustomerName] = useState(
    doc.matchedCustomer?.name ?? doc.matchedPolicy?.customer.name ?? "",
  );
  const [policyId, setPolicyId] = useState(doc.matchedPolicy?.id ?? "");
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState<string | null>(null);

  function set<K extends keyof typeof v>(k: K, val: (typeof v)[K]) {
    setV((p) => ({ ...p, [k]: val }));
  }

  async function save() {
    setBusy(true);
    setErr(null);
    try {
      const numOrUndef = (s: string) => (s.trim() === "" ? undefined : Number(s));
      const trimOrUndef = (s: string) => (s.trim() === "" ? undefined : s.trim());
      const currency = v.currency || undefined;
      const reviewInput: PolicyOcrReviewInput = {
        policyNumber: trimOrUndef(v.policyNumber),
        insuredName: trimOrUndef(v.insuredName),
        additionalInsured: trimOrUndef(v.additionalInsured),
        agentName: trimOrUndef(v.agentName),
        legalAddress: trimOrUndef(v.legalAddress),
        zip: trimOrUndef(v.zip),
        policyFrom: v.policyFrom || undefined,
        policyTo: v.policyTo || undefined,
        policyDate: v.policyDate || undefined,
        currency,
        netPremium: numOrUndef(v.netPremium),
        total: numOrUndef(v.total),
        premiumPayment: trimOrUndef(v.premiumPayment),
        coveragePeriodDays: numOrUndef(v.coveragePeriodDays),
        matchedPolicyId: policyId || undefined,
        matchedCustomerId: !policyId && customerId ? customerId : undefined,
        forceConfirm: true,
      };
      const confirmInput: PolicyOcrConfirmDocument = {
        documentId: doc.id,
        policyId: policyId || undefined,
        customerId: !policyId && customerId ? customerId : undefined,
        policyNumber: reviewInput.policyNumber,
        insuredName: reviewInput.insuredName,
        additionalInsured: reviewInput.additionalInsured,
        agentName: reviewInput.agentName,
        legalAddress: reviewInput.legalAddress,
        zip: reviewInput.zip,
        policyFrom: reviewInput.policyFrom,
        policyTo: reviewInput.policyTo,
        policyDate: reviewInput.policyDate,
        currency: (currency as "MXN" | "USD" | "EUR" | undefined) ?? undefined,
        netPremium: reviewInput.netPremium,
        total: reviewInput.total,
        premiumPayment: reviewInput.premiumPayment,
        coveragePeriodDays: reviewInput.coveragePeriodDays,
        coveragesJson: (doc.extractedCoveragesJson ?? undefined) as
          | PolicyOcrCoverage[]
          | undefined,
        postPremium: v.postPremium,
      };
      await onSave({ reviewInput, confirmInput });
    } catch (e) {
      setErr((e as Error)?.message ?? "No se pudo guardar.");
    } finally {
      setBusy(false);
    }
  }

  const locked = doc.status === "POSTED" || doc.status === "REJECTED";
  const matchedExisting = !!doc.matchedPolicy;
  const candidates = doc.matchCandidates ?? [];
  const suggestions: PolicyOcrCustomerSuggestion[] = doc.customerSuggestions ?? [];

  return (
    <article className="card" style={{ padding: 16 }}>
      <header className="row" style={{ gap: 12, alignItems: "center" }}>
        <span className="tag">{STATUS_LABEL[doc.status] ?? doc.status}</span>
        <span className="page-sub">Página {doc.pageNumber}</span>
        {/* No hand-rolled separators or margins here: `.row` is a flex
            container and its gap does the spacing. A literal "· " would leave
            a dot floating in that gap. */}
        {doc.extractedPolicyNumber && <strong>{doc.extractedPolicyNumber}</strong>}
        {doc.extractedInsuredName && (
          <span className="page-sub">{doc.extractedInsuredName}</span>
        )}
        {/* Read-only: the parser names the type, the confirm step resolves it
            to a policy_types row. Reassigning it is the policy screen's job,
            where the full picker already lives. */}
        {doc.extractedPolicyTypeName && (
          <span className="tag">{doc.extractedPolicyTypeName}</span>
        )}
        {doc.provider && <span className="tag">{doc.provider}</span>}
      </header>

      <div className="doc-detail">
        {/*
         * Embed the source PDF the office uploaded. One PDF = one parsed
         * policy, so the browser's PDF viewer handles multi-page navigation
         * natively; we don't need to render individual pages on the server.
         */}
        <iframe
          src={policyOcrDocumentUrl(doc.id)}
          title={`Póliza ${doc.extractedPolicyNumber ?? doc.pageNumber}`}
          style={{
            width: "100%",
            height: 720,
            border: "1px solid var(--border, #ddd)",
            borderRadius: 6,
            background: "#fff",
          }}
        />

        <div className="stack" style={{ flex: 1, minWidth: 0 }}>
          {doc.matchNote && <p className="page-sub">{doc.matchNote}</p>}

          {matchedExisting ? (
            <div className="state-box">
              Coincide con la póliza{" "}
              <strong>{doc.matchedPolicy?.policyNumber}</strong> del cliente{" "}
              <strong>{doc.matchedPolicy?.customer.name}</strong>.
            </div>
          ) : candidates.length > 1 ? (
            <div className="state-box state-warn">
              {candidates.length} pólizas comparten este número. Elija
              manualmente abajo.
            </div>
          ) : (
            <div className="state-box">
              No se encontró una póliza con este número. Se creará una nueva
              bajo el cliente que elija abajo.
            </div>
          )}

          <fieldset className="form-grid" disabled={locked || !canReview}>
            <Field label="Número de póliza">
              <input
                className="input"
                value={v.policyNumber}
                onChange={(e) => set("policyNumber", e.target.value)}
              />
            </Field>
            <Field label="Asegurado">
              <input
                className="input"
                value={v.insuredName}
                onChange={(e) => set("insuredName", e.target.value)}
              />
            </Field>
            <Field label="Asegurado adicional">
              <input
                className="input"
                value={v.additionalInsured}
                onChange={(e) => set("additionalInsured", e.target.value)}
              />
            </Field>
            <Field label="Agente">
              <input
                className="input"
                value={v.agentName}
                onChange={(e) => set("agentName", e.target.value)}
              />
            </Field>
            <Field label="Desde">
              <input
                className="input"
                type="date"
                value={v.policyFrom}
                onChange={(e) => set("policyFrom", e.target.value)}
              />
            </Field>
            <Field label="Hasta">
              <input
                className="input"
                type="date"
                value={v.policyTo}
                onChange={(e) => set("policyTo", e.target.value)}
              />
            </Field>
            <Field label="Fecha de firma">
              <input
                className="input"
                type="date"
                value={v.policyDate}
                onChange={(e) => set("policyDate", e.target.value)}
              />
            </Field>
            {/* ANA sells 3- and 4-day tourist policies; left blank the
                póliza keeps the 365-day default. */}
            <Field label="Días de vigencia">
              <input
                className="input"
                type="number"
                min={1}
                value={v.coveragePeriodDays}
                onChange={(e) => set("coveragePeriodDays", e.target.value)}
              />
            </Field>
            <Field label="Moneda">
              <select
                className="input select"
                value={v.currency}
                onChange={(e) => set("currency", e.target.value)}
              >
                <option value="MXN">MXN</option>
                <option value="USD">USD</option>
                <option value="EUR">EUR</option>
              </select>
            </Field>
            <Field label="Prima neta">
              <input
                className="input"
                type="number"
                step="0.01"
                value={v.netPremium}
                onChange={(e) => set("netPremium", e.target.value)}
              />
            </Field>
            <Field label="Total">
              <input
                className="input"
                type="number"
                step="0.01"
                value={v.total}
                onChange={(e) => set("total", e.target.value)}
              />
            </Field>
            <Field label="Pago de prima">
              <input
                className="input"
                value={v.premiumPayment}
                onChange={(e) => set("premiumPayment", e.target.value)}
              />
            </Field>
            <Field label="Dirección">
              <input
                className="input"
                value={v.legalAddress}
                onChange={(e) => set("legalAddress", e.target.value)}
              />
            </Field>
            <Field label="C.P.">
              <input
                className="input"
                value={v.zip}
                onChange={(e) => set("zip", e.target.value)}
              />
            </Field>
          </fieldset>

          {doc.extractedCoveragesJson && doc.extractedCoveragesJson.length > 0 && (
            <details>
              <summary>
                Coberturas ({doc.extractedCoveragesJson.length}) ·{" "}
                {formatMoney(
                  doc.extractedCoveragesJson
                    .map((c) => Number(c.insuredAmount ?? 0))
                    .reduce((a, b) => a + b, 0)
                    .toString(),
                  v.currency,
                )}
              </summary>
              <table className="tx-table" style={{ marginTop: 8 }}>
                <thead>
                  <tr>
                    <th>Riesgo</th>
                    <th className="num">Suma</th>
                    <th className="num">Prima</th>
                    <th>Deducible</th>
                    <th>Participación</th>
                  </tr>
                </thead>
                <tbody>
                  {doc.extractedCoveragesJson.map((c, i) => (
                    <tr key={i}>
                      <td>{c.risk}</td>
                      <td className="num">
                        {formatMoney(c.insuredAmount?.toString() ?? null, v.currency)}
                      </td>
                      {/* ANA's add-on sections print what the coverage COST
                          where the others print what it pays. Kept in its own
                          column so the two are never added together. */}
                      <td className="num">
                        {c.premium == null
                          ? "—"
                          : formatMoney(c.premium.toString(), v.currency)}
                      </td>
                      <td>{c.deductible ?? "—"}</td>
                      <td>{c.lossParticipation ?? "—"}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </details>
          )}

          {/*
           * Vehicles and named drivers are read-only here: they are written
           * as their own Vehicle / InsuredDriver rows on confirm, and the
           * policy screen is where they get edited. Showing them is what
           * lets a reviewer catch a misread VIN before it is applied.
           */}
          {doc.extractedVehiclesJson && doc.extractedVehiclesJson.length > 0 && (
            <details>
              <summary>Unidades ({doc.extractedVehiclesJson.length})</summary>
              <table className="tx-table" style={{ marginTop: 8 }}>
                <thead>
                  <tr>
                    <th>Tipo</th>
                    <th>Año</th>
                    <th>Marca</th>
                    <th>Carrocería</th>
                    <th>Serie</th>
                    <th>Placas</th>
                  </tr>
                </thead>
                <tbody>
                  {doc.extractedVehiclesJson.map((veh, i) => (
                    <tr key={i}>
                      <td>{veh.item}</td>
                      <td>{veh.modelYear ?? "—"}</td>
                      <td>{veh.make ?? "—"}</td>
                      <td>{veh.bodyType ?? "—"}</td>
                      <td>{veh.vinNumber ?? "—"}</td>
                      <td>{veh.licensePlate ?? "—"}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </details>
          )}

          {doc.extractedDriversJson && doc.extractedDriversJson.length > 0 && (
            <details>
              <summary>Conductores ({doc.extractedDriversJson.length})</summary>
              <table className="tx-table" style={{ marginTop: 8 }}>
                <thead>
                  <tr>
                    <th>Nombre</th>
                    <th>Licencia</th>
                    <th>Teléfono</th>
                  </tr>
                </thead>
                <tbody>
                  {doc.extractedDriversJson.map((d, i) => (
                    <tr key={i}>
                      <td>{d.fullName}</td>
                      <td>{d.licenseNumber ?? "—"}</td>
                      <td>{d.phone ?? "—"}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </details>
          )}

          {candidates.length > 1 && (
            <Field label="Póliza destino">
              <select
                className="input select"
                value={policyId}
                onChange={(e) => {
                  setPolicyId(e.target.value);
                  const found = candidates.find((c) => c.policyId === e.target.value);
                  if (found) {
                    setCustomerId(found.customerId);
                    setCustomerName(customerIndex[found.customerId]?.name ?? found.customerName);
                  }
                }}
              >
                <option value="">— elegir póliza —</option>
                {candidates.map((c) => (
                  <option key={c.policyId} value={c.policyId}>
                    {c.policyNumber} · {c.customerName}
                  </option>
                ))}
              </select>
            </Field>
          )}

          {!policyId && (
            <Field label={matchedExisting ? "Cliente" : "Cliente (póliza nueva)"}>
              <CustomerPicker
                value={customerId}
                valueName={customerName}
                onPick={(id, name) => {
                  setCustomerId(id);
                  setCustomerName(name);
                }}
              />
            </Field>
          )}

          {/*
           * Name suggestions, never a preselection. The office writes
           * customers surname-first and carriers print them given-name-first,
           * so without this the reviewer retypes a name the parser already
           * read. One click fills the picker above; nothing is chosen until
           * they click. Kept outside the <Field> label — a label must not
           * wrap other interactive controls.
           */}
          {!policyId && !customerId && !locked && canReview && suggestions.length > 0 && (
            <div className="row" style={{ gap: 8, flexWrap: "wrap" }}>
              <span className="page-sub">Sugerencias por nombre:</span>
              {suggestions.map((s) => (
                <button
                  key={s.customerId}
                  type="button"
                  className="btn btn-ghost btn-sm"
                  onClick={() => {
                    setCustomerId(s.customerId);
                    setCustomerName(s.customerName);
                  }}
                >
                  {s.customerName}
                  {s.tier === "PARTIAL" && <span className="page-sub"> · parcial</span>}
                </button>
              ))}
            </div>
          )}

          <label className="field">
            <input
              type="checkbox"
              checked={v.postPremium}
              onChange={(e) => set("postPremium", e.target.checked)}
              disabled={!v.netPremium || Number(v.netPremium) <= 0}
            />{" "}
            Registrar prima en el estado de cuenta
          </label>

          {err && <div className="state-box state-error">{err}</div>}

          {!locked && canReview && (
            <div className="row" style={{ gap: 8 }}>
              <button type="button" className="btn btn-primary" disabled={busy} onClick={save}>
                {busy ? "Guardando…" : "Guardar revisión"}
              </button>
              <button
                type="button"
                className="btn btn-ghost"
                onClick={() => void onReject()}
              >
                Rechazar
              </button>
            </div>
          )}
        </div>
      </div>
    </article>
  );
}

function Field({ label, children }: { label: string; children: React.ReactNode }) {
  return (
    <label className="field">
      <span className="field-label">{label}</span>
      {children}
    </label>
  );
}