// ---------------------------------------------------------------------------
// customer-grid / ImportDialog.tsx
//
// ⭐⭐ WAVE-29 T25 (owner item 6) — IMPORT A SPREADSHEET INTO A USER DATABASE.
//
// Three steps in one window, because each answers the question the last one raised:
//   1. pick a file      -> the server parses it and says which columns it found
//   2. map its columns  -> to fields, or to nothing; unmapped is the default
//   3. check + import   -> every value coerced FIRST, the problems named BEFORE anything writes
//
// ⛔ APPEND-ONLY IN V1, AND THE DIALOG SAYS SO. Nothing is matched, nothing is overwritten — every
// row becomes a new record. The only bulk-merge primitive in the product is upsert-BY-KEY shaped,
// and choosing a key on the user's behalf is how an import quietly rewrites rows somebody edited.
// The sentence is on screen rather than in a doc, because that is where the decision is made.
//
// ⛔ REUSE, NOT A SECOND COERCER. `clipboard.coerceClipboardValue` is THE client answer to "can
// this text be this field's value" (it is what a paste already goes through), and
// `POST /api/v1/uploads/tabular` is the parser that already exists — 5 MB / 20,000 rows, xlsx AND
// csv, and it never writes a row. A second copy of either is a second set of rules for one
// question ([[one-evaluator-per-question]]).
//
// ⚠ THE SERVER IS THE WALL, not this dialog. It refuses computed columns BY NAME and refuses the
// whole batch if it would pass the row cap. What happens here is the part a person needs before
// they press the button: which column goes where, and what will not survive the trip.
// ---------------------------------------------------------------------------

import { useMemo, useRef, useState } from "react";
// ⚠ From `apiContract`, which is where these two are DECLARED — not re-exported from
// `apiBridge`, which does not export them and is another session's fence this wave.
import { API_V1, CREDENTIALS } from "../apiContract";
import { isImportTarget } from "./types";
import type { Field } from "./types";
// ⭐ W29-T76 — the plan lives in a pure module so a test can EXECUTE it against the server's real
// payload. This file holds the WINDOW; `importPlan.ts` holds what would be written.
import { buildImportPlan } from "./importPlan";
import type { ImportMapping, ParsedUpload } from "./importPlan";
import "./ImportDialog.css";

/** The one header a JSON POST needs. `apiBridge` keeps its own module-local copy; this is a
 *  literal rather than an import because that constant is not exported and that file is not
 *  this session's to change. */
const JSON_HEADERS = { "Content-Type": "application/json" };

/** The upload door's answer (`routes_uploads.tabular`) — declared in `importPlan.ts`, beside the
 *  only code that reads its shape. ⛔ It is COLUMN-MAJOR; the copy that used to live here said
 *  `string[][]` and that one wrong word is why nothing ever imported. */
type Parsed = ParsedUpload;

export interface ImportDialogProps {
  /** The target database's fields — the mapping targets, before this file filters them. */
  fields: Field[];
  /** `ut_*` table key; the import door is per-table. */
  scope: string;
  /** How the database calls itself, for the sentence that says where rows are going. */
  tableName: string;
  /** Fired after a successful import so the host can re-read its rows. */
  onImported: (count: number) => void;
  onClose: () => void;
}

const NONE = "";

export default function ImportDialog({
  fields,
  scope,
  tableName,
  onImported,
  onClose,
}: ImportDialogProps): React.ReactElement {
  const [parsed, setParsed] = useState<Parsed | null>(null);
  const [mapping, setMapping] = useState<ImportMapping>({});
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState("");
  const fileRef = useRef<HTMLInputElement | null>(null);

  const targets = useMemo(() => fields.filter(isImportTarget), [fields]);

  /**
   * The parse. `FormData` and NO `Content-Type` header: the browser has to set the multipart
   * boundary itself, and spelling the header by hand is the classic way to make a working upload
   * 400 with a message about a missing part.
   */
  async function pick(file: File): Promise<void> {
    setBusy(true);
    setError("");
    try {
      const form = new FormData();
      form.append("file", file);
      const res = await fetch(`${API_V1}/uploads/tabular`, {
        method: "POST",
        credentials: CREDENTIALS,
        body: form,
      });
      const body = (await res.json().catch(() => null)) as
        | (Parsed & { error?: { message?: string } })
        | null;
      if (!res.ok || !body) {
        setError(
          body?.error?.message ??
            `That file could not be read (the server answered ${res.status}).`
        );
        return;
      }
      setParsed(body);
      // ⭐ AUTO-MAP BY NAME, case- and space-insensitively, because the overwhelmingly common
      // case is a sheet exported from this very grid. Every guess is VISIBLE in a picker the
      // user can change — a silent auto-map is only dangerous when it is also invisible.
      const byName = new Map(
        targets.map((f) => [f.label.trim().toLowerCase(), f.key] as const)
      );
      const guess: ImportMapping = {};
      (body.columns ?? []).forEach((name, i) => {
        const hit = byName.get(String(name).trim().toLowerCase());
        if (hit) guess[i] = hit;
      });
      setMapping(guess);
    } catch {
      setError("That file could not be read.");
    } finally {
      setBusy(false);
    }
  }

  /**
   * What would be written, and what cannot be. The work is `importPlan.buildImportPlan` — a pure
   * module, so the node suite executes it against the server's real payload rather than grepping
   * this file for the shape of a loop.
   */
  const plan = useMemo(
    () => buildImportPlan(parsed, mapping, targets),
    [parsed, mapping, targets]
  );

  async function run(): Promise<void> {
    setBusy(true);
    setError("");
    try {
      const res = await fetch(
        `${API_V1}/tables/${encodeURIComponent(scope)}/rows/import`,
        {
          method: "POST",
          credentials: CREDENTIALS,
          headers: JSON_HEADERS,
          body: JSON.stringify({ rows: plan.rows }),
        }
      );
      const body = (await res.json().catch(() => null)) as
        | { imported?: number; error?: { message?: string } }
        | null;
      if (!res.ok) {
        // The server's sentence beats anything invented here: it names the column, or the cap.
        setError(body?.error?.message ?? `The server answered ${res.status}.`);
        return;
      }
      onImported(body?.imported ?? plan.rows.length);
      onClose();
    } catch {
      setError("Nothing was imported — the request did not complete.");
    } finally {
      setBusy(false);
    }
  }

  const mappedCount = Object.values(mapping).filter((k) => k !== NONE).length;

  return (
    <div className="cg-import-scrim" role="presentation" onClick={onClose}>
      <div
        className="cg-import"
        role="dialog"
        aria-modal="true"
        aria-label={`Import records into ${tableName}`}
        onClick={(event) => event.stopPropagation()}
      >
        <div className="cg-import-head">
          <h2>Import records</h2>
          <button type="button" className="cg-import-x" aria-label="Close" onClick={onClose}>
            ×
          </button>
        </div>

        {!parsed ? (
          <div className="cg-import-body">
            <p className="cg-import-lede">
              Add records to <strong>{tableName}</strong> from a spreadsheet. Excel
              (<code>.xlsx</code>) or <code>.csv</code>, up to 5&nbsp;MB.
            </p>
            <p className="cg-import-note">
              Every row becomes a <strong>new record</strong>. Nothing already in this database is
              matched or overwritten.
            </p>
            <input
              ref={fileRef}
              type="file"
              accept=".xlsx,.csv"
              className="cg-import-file"
              onChange={(event) => {
                const file = event.currentTarget.files?.[0];
                if (file) void pick(file);
              }}
            />
          </div>
        ) : (
          <div className="cg-import-body">
            <p className="cg-import-lede">
              {parsed.rows.toLocaleString()} row{parsed.rows === 1 ? "" : "s"} read
              {parsed.truncated ? " (the first 20,000 — the rest were not read)" : ""}. Choose
              where each column goes.
            </p>
            <div className="cg-import-map" role="list">
              {parsed.columns.map((name, i) => (
                <label className="cg-import-row" role="listitem" key={`${name}:${i}`}>
                  <span className="cg-import-col" title={name}>
                    {name || `Column ${i + 1}`}
                  </span>
                  <select
                    value={mapping[i] ?? NONE}
                    onChange={(event) => {
                      const next = event.target.value;
                      setMapping((cur) => ({ ...cur, [i]: next }));
                    }}
                  >
                    <option value={NONE}>Don&rsquo;t import</option>
                    {targets.map((f) => (
                      <option key={f.key} value={f.key}>
                        {f.label}
                      </option>
                    ))}
                  </select>
                </label>
              ))}
            </div>
            {plan.problems.length > 0 && (
              <div className="cg-import-problems" role="alert">
                <strong>
                  {plan.problems.length.toLocaleString()} value
                  {plan.problems.length === 1 ? "" : "s"} cannot go in the column
                  {plan.problems.length === 1 ? "" : "s"} chosen for them
                </strong>
                <ul>
                  {plan.problems.slice(0, 5).map((p, i) => (
                    <li key={i}>
                      Row {p.row}, {p.column}: {p.value ? `“${p.value}”` : "(blank)"} is not a
                      valid {p.field}
                    </li>
                  ))}
                </ul>
                {plan.problems.length > 5 && (
                  <p>…and {(plan.problems.length - 5).toLocaleString()} more.</p>
                )}
                <p>
                  Fix them in the file, or point that column somewhere else — nothing has been
                  imported yet.
                </p>
              </div>
            )}
          </div>
        )}

        {error && (
          <p className="cg-import-error" role="alert">
            {error}
          </p>
        )}

        <div className="cg-import-foot">
          {parsed && (
            <span className="cg-import-count">
              {mappedCount === 0
                ? "No columns chosen yet"
                : `${plan.rows.length.toLocaleString()} record${
                    plan.rows.length === 1 ? "" : "s"
                  } from ${mappedCount} column${mappedCount === 1 ? "" : "s"}`}
            </span>
          )}
          <button type="button" className="cg-btn" onClick={onClose}>
            Cancel
          </button>
          {parsed && (
            <button
              type="button"
              className="cg-btn cg-btn--primary"
              /* ⛔ DISABLED WHILE ANY VALUE IS REFUSED. The alternative — import the good rows and
                 report the rest — leaves somebody reconciling a spreadsheet against a table by
                 hand, and neither existing write door demonstrates a partial policy. Refusing
                 whole is recoverable in one edit; a half-import is not. */
              disabled={busy || plan.rows.length === 0 || plan.problems.length > 0}
              onClick={() => void run()}
            >
              {busy ? "Importing…" : `Import ${plan.rows.length.toLocaleString()} record${
                plan.rows.length === 1 ? "" : "s"
              }`}
            </button>
          )}
        </div>
      </div>
    </div>
  );
}
