// ---------------------------------------------------------------------------
// customer-grid / Documents.tsx
// Wave-8 I12c (contract C5) — the Documents section of the record drawer:
// upload, list, download, delete, per customer.
//
// Shape notes that are not obvious from the contract:
//
//   - The bytes NEVER ride the payload. Metadata (`payload.docs[pid]`) renders
//     the list; the file itself is fetched one at a time via `doc_fetch`, whose
//     answer arrives on the NEXT render in `payload.docPayload`. The host bridge
//     has no request/response channel — events are a fire-and-forget log
//     (hostBridge.ts) — so a download is: emit, remember what we asked for,
//     recognise the answer, build a Blob, revoke the URL.
//
//   - The 5 MB cap is enforced BEFORE the read, on `file.size`. Reading a 400 MB
//     file into memory to discover it is too big is its own bug, and base64
//     inflates by 4/3 on top of that. The limit is also stated in the UI before
//     anyone picks a file, rather than only in the rejection.
//
//   - Delete is host-enforced (uploader or admin). `canDelete` only decides
//     whether the control RENDERS — a hidden button is courtesy, never the wall.
// ---------------------------------------------------------------------------

import { useEffect, useRef, useState } from "react";
import { MAX_DOC_BYTES } from "./types";
import type { CustomerDoc } from "./types";

function sizeText(bytes: number): string {
  if (!Number.isFinite(bytes) || bytes < 0) return "—";
  if (bytes < 1024) return `${bytes} B`;
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}

/** Uploaded-by / when, as one quiet line. `ts` is host-supplied; it is rendered,
 *  never computed from the browser clock. */
function byLine(doc: CustomerDoc): string {
  const parts: string[] = [];
  if (doc.by) parts.push(doc.by);
  if (doc.ts) parts.push(doc.ts.slice(0, 16).replace("T", " "));
  return parts.join(" · ");
}

export function Documents({
  pid,
  docs,
  docPayload,
  onAdd,
  onFetch,
  onDelete,
}: {
  pid: number;
  docs: CustomerDoc[];
  /** The host's answer to the most recent `doc_fetch`, if one has arrived. */
  docPayload?: { pid: number; docId: string; name: string; mime: string; data_b64: string };
  onAdd: (file: { name: string; mime: string; size: number; data_b64: string }) => void;
  onFetch: (docId: string) => void;
  onDelete: (docId: string) => void;
}) {
  const fileRef = useRef<HTMLInputElement>(null);
  const [error, setError] = useState<string | null>(null);
  const [busy, setBusy] = useState(false);
  /** What we asked the host for, so an arriving docPayload is only consumed when
   *  it is the answer to OUR request (and only once). */
  const wantRef = useRef<string | null>(null);
  const [confirmId, setConfirmId] = useState<string | null>(null);

  // The download half: recognise our answer, save it, forget it.
  useEffect(() => {
    if (!docPayload || wantRef.current == null) return;
    if (docPayload.pid !== pid || docPayload.docId !== wantRef.current) return;
    wantRef.current = null;
    try {
      const bin = atob(docPayload.data_b64);
      const bytes = new Uint8Array(bin.length);
      for (let i = 0; i < bin.length; i += 1) bytes[i] = bin.charCodeAt(i);
      const url = URL.createObjectURL(
        new Blob([bytes], { type: docPayload.mime || "application/octet-stream" })
      );
      const a = document.createElement("a");
      a.href = url;
      a.download = docPayload.name || "document";
      document.body.appendChild(a);
      a.click();
      a.remove();
      // Revoked on the next tick: revoking synchronously can beat the click in
      // some browsers and the download silently does nothing.
      window.setTimeout(() => URL.revokeObjectURL(url), 0);
    } catch {
      setError("That file could not be decoded. Nothing was downloaded.");
    }
  }, [docPayload, pid]);

  const pick = (file: File | undefined) => {
    setError(null);
    if (!file) return;
    // Checked on file.size, BEFORE reading — see the header note.
    if (file.size > MAX_DOC_BYTES) {
      setError(
        `${file.name} is ${sizeText(file.size)}. The limit is ${sizeText(MAX_DOC_BYTES)}, so it was not uploaded.`
      );
      if (fileRef.current) fileRef.current.value = "";
      return;
    }
    setBusy(true);
    const reader = new FileReader();
    reader.onerror = () => {
      setBusy(false);
      setError(`${file.name} could not be read.`);
    };
    reader.onload = () => {
      setBusy(false);
      const result = String(reader.result ?? "");
      const comma = result.indexOf(",");
      if (comma < 0) {
        setError(`${file.name} could not be encoded.`);
        return;
      }
      onAdd({
        name: file.name,
        mime: file.type || "application/octet-stream",
        size: file.size,
        data_b64: result.slice(comma + 1),
      });
      if (fileRef.current) fileRef.current.value = "";
    };
    reader.readAsDataURL(file);
  };

  return (
    <section className="cg-detail-section cg-docs">
      <div className="cg-detail-section-title">Documents</div>

      {docs.length === 0 ? (
        <div className="cg-docs-empty">No documents on this customer yet.</div>
      ) : (
        <ul className="cg-docs-list">
          {docs.map((d) => (
            <li key={d.id} className="cg-docs-row">
              <button
                type="button"
                className="cg-link-btn cg-docs-name"
                title={`Download ${d.name}`}
                onClick={() => {
                  setError(null);
                  wantRef.current = d.id;
                  onFetch(d.id);
                }}
              >
                {d.name}
              </button>
              <span className="cg-docs-meta">
                {sizeText(d.size)}
                {byLine(d) ? ` · ${byLine(d)}` : ""}
              </span>
              {d.canDelete !== false && (
                <button
                  type="button"
                  className={"cg-docs-del" + (confirmId === d.id ? " is-armed" : "")}
                  aria-label={confirmId === d.id ? `Delete ${d.name}?` : `Delete ${d.name}`}
                  onClick={() => {
                    if (confirmId !== d.id) {
                      setConfirmId(d.id);
                      return;
                    }
                    setConfirmId(null);
                    onDelete(d.id);
                  }}
                >
                  {confirmId === d.id ? "Delete?" : "×"}
                </button>
              )}
            </li>
          ))}
        </ul>
      )}

      <div className="cg-docs-add">
        <input
          ref={fileRef}
          type="file"
          className="cg-docs-file"
          aria-label="Add a document"
          disabled={busy}
          onChange={(e) => pick(e.target.files?.[0])}
        />
        <span className="cg-docs-cap">Up to {sizeText(MAX_DOC_BYTES)} per file.</span>
      </div>
      {error && <div className="cg-docs-error">{error}</div>}
    </section>
  );
}
