"use client";

import * as React from "react";
import { CircleDotIcon, CircleXIcon, DownloadIcon, UploadIcon } from "lucide-react";

import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";

/* ------------------------------------- Types -------------------------------------- */

export type GridRow = {
  id: string;
  /** Row header: names the line for screen readers ("Amoxil 500 mg"). */
  header: string;
  sub?: string;
  /** Committed values (from the API or the saved draft), as strings. */
  values: Record<string, string>;
};

export type GridColumn = {
  key: string;
  header: string;
  /** read: plain text · number/text/date: an input · derived: from the API, "Updating…" while refreshing. */
  kind: "read" | "number" | "text" | "date" | "derived";
  align?: "left" | "right";
  width?: string;
  /** Shown after numbers in read cells and as the input suffix ("tablets"). */
  unit?: (row: GridRow) => string;
  placeholder?: string;
  /** Return a message naming the problem and the fix, or undefined. Gets the row with unsaved edits applied. */
  validate?: (value: string, row: Record<string, string>) => string | undefined;
  /** For read and derived cells. */
  render?: (row: Record<string, string>, gridRow: GridRow) => React.ReactNode;
};


/**
 * Errors for every editable cell of rows that have edits (a change can make a blank neighbour required),
 * keyed "rowIndex:columnKey". Exported so a wizard step can block Next on the same rules the grid shows.
 * Pass `all` to check every row, edited or not (e.g. before committing).
 */
export function gridErrors(columns: GridColumn[], rows: GridRow[], draft: Record<string, Record<string, string>>, all = false) {
  const errors = new Map<string, string>();
  const editable = columns.filter((c) => c.kind === "number" || c.kind === "text" || c.kind === "date");
  rows.forEach((row, r) => {
    if (!all && !draft[row.id]) return;
    const m = { ...row.values, ...(draft[row.id] ?? {}) };
    for (const c of editable) {
      const msg = c.validate?.(m[c.key] ?? "", m);
      if (msg) errors.set(`${r}:${c.key}`, msg);
    }
  });
  return errors;
}

/* ------------------------------------ Component ------------------------------------ */

/**
 * Editable grid (spec 02-data-display §1.6): a native table for entering many lines in one sitting.
 * Enter commits and moves down the column; ↑/↓ commit and move; Esc reverts; Ctrl/Cmd+S saves the draft.
 * Number inputs never change on arrow keys. Unsaved cells carry a dot and "changed"; errors show on the cell,
 * under it while focused, and in a summary that links to each cell. Derived cells wait for the API.
 */
export function EditableGrid({
  caption,
  columns,
  rows,
  draft,
  updating,
  onDraftChange,
  onSaveDraft,
  onDiscard,
  saving,
  csv,
  footer,
  rowLabel = "Item",
}: {
  caption: string;
  /** Heading of the first (row header) column: "Item", "Employee". */
  rowLabel?: string;
  columns: GridColumn[];
  rows: GridRow[];
  /** Unsaved edits: rowId → key → value. */
  draft: Record<string, Record<string, string>>;
  /** Row ids whose derived cells are being refreshed from the API. */
  updating?: Set<string>;
  /** Set a cell's unsaved value; undefined reverts it to the committed value. */
  onDraftChange: (rowId: string, key: string, value: string | undefined) => void;
  onSaveDraft?: () => void;
  onDiscard?: () => void;
  saving?: boolean;
  /** Stocktake-style CSV: uploaded values land in the draft for review. */
  csv?: { template: string; filename: string; onUpload: (text: string) => { applied: number; skipped: string[] } };
  footer?: React.ReactNode;
}) {
  const id = React.useId();
  const [editing, setEditingState] = React.useState<{ cell: string; text: string } | null>(null);
  // Mirror in a ref so commit (on blur, Enter, arrows) always sees the latest typed text, even before a re-render.
  const editingRef = React.useRef(editing);
  const setEditing = (v: { cell: string; text: string } | null) => {
    editingRef.current = v;
    setEditingState(v);
  };
  const [focused, setFocused] = React.useState<string | null>(null);
  const [csvNote, setCsvNote] = React.useState<string | null>(null);
  const inputs = React.useRef(new Map<string, HTMLInputElement>());
  const fileRef = React.useRef<HTMLInputElement>(null);
  const cellKey = (r: number, c: string) => `${r}:${c}`;

  const merged = (row: GridRow) => ({ ...row.values, ...(draft[row.id] ?? {}) });

  const errors = gridErrors(columns, rows, draft);
  const changedCount = Object.values(draft).reduce((s, d) => s + Object.keys(d).length, 0);
  const errorRows = new Set([...errors.keys()].map((k) => Number(k.split(":")[0])));

  const commit = (r: number, col: GridColumn) => {
    const k = cellKey(r, col.key);
    const cur = editingRef.current;
    if (!cur || cur.cell !== k) return;
    const row = rows[r];
    const text = col.kind === "number" ? cur.text.replace(/[^\d.]/g, "") : cur.text;
    if (text !== (merged(row)[col.key] ?? "")) onDraftChange(row.id, col.key, text === (row.values[col.key] ?? "") ? undefined : text);
    setEditing(null);
  };

  const move = (r: number, col: string) => {
    const el = inputs.current.get(cellKey(r, col));
    if (el) {
      el.focus();
      el.select();
    }
  };

  const goTo = (k: string) => {
    const el = inputs.current.get(k);
    el?.scrollIntoView({ block: "center" });
    el?.focus();
  };

  return (
    <div
      className="space-y-3"
      onKeyDown={(e) => {
        if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
          e.preventDefault();
          (document.activeElement as HTMLElement | null)?.blur(); // commit the focused cell first
          setTimeout(() => onSaveDraft?.(), 0);
        }
      }}
    >
      {/* Toolbar */}
      <div className="flex flex-wrap items-center gap-2">
        <span className={cn("text-sm", changedCount ? "font-medium text-foreground" : "text-muted-foreground")} aria-live="polite">
          {changedCount ? `${changedCount} unsaved ${changedCount === 1 ? "change" : "changes"}` : "No unsaved changes"}
        </span>
        {changedCount ? (
          <>
            <Button size="sm" variant="secondary" onClick={onSaveDraft} disabled={saving}>
              {saving ? "Saving…" : "Save draft"}
            </Button>
            <Button size="sm" variant="ghost" onClick={onDiscard} disabled={saving}>
              Discard changes
            </Button>
          </>
        ) : null}
        {csv ? (
          <div className="ml-auto flex items-center gap-1">
            <Button
              size="sm"
              variant="ghost"
              onClick={() => {
                const url = URL.createObjectURL(new Blob([csv.template], { type: "text/csv" }));
                const a = document.createElement("a");
                a.href = url;
                a.download = csv.filename;
                a.click();
                URL.revokeObjectURL(url);
              }}
            >
              <DownloadIcon data-icon="inline-start" />
              Download template
            </Button>
            <Button size="sm" variant="outline" onClick={() => fileRef.current?.click()}>
              <UploadIcon data-icon="inline-start" />
              Upload CSV
            </Button>
            <input
              ref={fileRef}
              type="file"
              accept=".csv,text/csv"
              className="hidden"
              onChange={async (e) => {
                const f = e.target.files?.[0];
                e.target.value = "";
                if (!f) return;
                const r = csv.onUpload(await f.text());
                setCsvNote(`${r.applied} ${r.applied === 1 ? "value" : "values"} added as unsaved changes for review.${r.skipped.length ? ` Skipped: ${r.skipped.join("; ")}.` : ""}`);
              }}
            />
          </div>
        ) : null}
      </div>
      {csvNote ? (
        <p className="rounded-lg border border-status-info-border bg-status-info-bg px-3 py-2 text-sm text-status-info-text" role="status">
          {csvNote}
        </p>
      ) : null}

      {/* Error summary */}
      {errors.size ? (
        <div role="region" aria-label="Lines that need attention" className="space-y-1 rounded-lg border border-status-error-border bg-status-error-bg px-3 py-2 text-sm text-status-error-text">
          <p className="font-semibold">
            {errorRows.size} {errorRows.size === 1 ? "line needs" : "lines need"} attention
          </p>
          <ul className="space-y-0.5">
            {[...errors.entries()].map(([k, msg]) => {
              const [r, c] = k.split(":");
              const col = columns.find((x) => x.key === c)!;
              return (
                <li key={k}>
                  <button type="button" className="cursor-pointer text-left underline underline-offset-2" onClick={() => goTo(k)}>
                    {rows[Number(r)].header}, {col.header.toLowerCase()}: {msg}
                  </button>
                </li>
              );
            })}
          </ul>
        </div>
      ) : null}

      {/* Grid: native table, no zebra (inputs must sit on white) */}
      <div className="overflow-x-auto rounded-lg border border-border bg-card">
        <table className="w-full border-collapse text-sm">
          <caption className="sr-only">{caption}</caption>
          <thead>
            <tr className="border-b border-[var(--color-border-panel)] bg-[var(--color-bg-surface-subtle)]">
              <th scope="col" id={`${id}-h-item`} className="px-3 py-2 text-left font-medium text-muted-foreground">
                {rowLabel}
              </th>
              {columns.map((c) => (
                <th key={c.key} scope="col" id={`${id}-h-${c.key}`} style={{ width: c.width }} className={cn("px-3 py-2 font-medium text-muted-foreground", (c.align ?? (c.kind === "number" || c.kind === "derived" ? "right" : "left")) === "right" ? "text-right" : "text-left")}>
                  {c.header}
                </th>
              ))}
            </tr>
          </thead>
          <tbody>
            {rows.map((row, r) => {
              const m = merged(row);
              return (
                <tr key={row.id} className="border-b border-border bg-card last:border-b-0">
                  <th scope="row" className="px-3 py-1.5 text-left align-top font-normal">
                    <span id={`${id}-r-${row.id}`} className="block font-medium">
                      {row.header}
                    </span>
                    {row.sub ? <span className="block text-xs text-muted-foreground">{row.sub}</span> : null}
                  </th>
                  {columns.map((c) => {
                    const right = (c.align ?? (c.kind === "number" || c.kind === "derived" ? "right" : "left")) === "right";
                    if (c.kind === "read" || c.kind === "derived") {
                      return (
                        <td key={c.key} className={cn("px-3 py-1.5 align-top tabular-nums", right && "text-right")}>
                          {c.kind === "derived" && updating?.has(row.id) ? <span className="text-xs text-muted-foreground">Updating…</span> : (c.render?.(m, row) ?? m[c.key])}
                        </td>
                      );
                    }
                    const k = cellKey(r, c.key);
                    const err = errors.get(k);
                    const changed = draft[row.id]?.[c.key] !== undefined;
                    const value = editing?.cell === k ? editing.text : (m[c.key] ?? "");
                    const unit = c.unit?.(row);
                    return (
                      <td key={c.key} className="px-2 py-1.5 align-top">
                        <div className="relative flex items-center gap-1.5">
                          {changed && !err ? (
                            <span className="absolute -left-1.5 text-muted-foreground" title="Changed, not saved">
                              <CircleDotIcon className="size-3" aria-hidden />
                            </span>
                          ) : null}
                          <div className={cn("flex h-8 w-full items-center rounded-sm border bg-card focus-within:ring-2 focus-within:ring-ring/50", err ? "border-[var(--color-border-input-error)]" : "border-input focus-within:border-ring")}>
                            <input
                              ref={(el) => {
                                if (el) inputs.current.set(k, el);
                                else inputs.current.delete(k);
                              }}
                              // type=text + inputMode: number inputs must never step on arrow keys (spec).
                              type={c.kind === "date" ? "date" : "text"}
                              inputMode={c.kind === "number" ? "decimal" : undefined}
                              value={value}
                              placeholder={c.placeholder}
                              aria-labelledby={`${id}-h-${c.key} ${id}-r-${row.id}`}
                              aria-invalid={!!err || undefined}
                              aria-describedby={err ? `${id}-e-${k}` : changed ? `${id}-c-${k}` : undefined}
                              className={cn("h-full min-w-0 flex-1 bg-transparent px-2 outline-none", right && "text-right tabular-nums")}
                              onFocus={(e) => {
                                setFocused(k);
                                setEditing({ cell: k, text: m[c.key] ?? "" });
                                if (c.kind !== "date") e.currentTarget.select();
                              }}
                              onChange={(e) => setEditing({ cell: k, text: e.target.value })}
                              onBlur={() => {
                                commit(r, c);
                                setFocused((f) => (f === k ? null : f));
                              }}
                              onKeyDown={(e) => {
                                if (e.key === "Enter" || e.key === "ArrowDown" || e.key === "ArrowUp") {
                                  if (c.kind === "date" && e.key !== "Enter") return; // let the date input use arrows
                                  e.preventDefault();
                                  const next = e.key === "ArrowUp" ? r - 1 : r + 1;
                                  commit(r, c);
                                  if (next >= 0 && next < rows.length) setTimeout(() => move(next, c.key), 0);
                                } else if (e.key === "Escape") {
                                  e.preventDefault();
                                  setEditing({ cell: k, text: m[c.key] ?? "" });
                                }
                              }}
                            />
                            {unit ? <span className="pr-2 text-xs text-muted-foreground">{unit}</span> : null}
                            {err ? <CircleXIcon className="mr-1.5 size-4 shrink-0 text-status-error-text" aria-hidden /> : null}
                          </div>
                        </div>
                        {changed ? (
                          <span id={`${id}-c-${k}`} className="sr-only">
                            changed
                          </span>
                        ) : null}
                        {err ? (
                          <p id={`${id}-e-${k}`} className={cn("mt-1 text-xs text-status-error-text", focused === k ? "block" : "sr-only")}>
                            {err}
                          </p>
                        ) : null}
                      </td>
                    );
                  })}
                </tr>
              );
            })}
          </tbody>
          {footer ? <tfoot>{footer}</tfoot> : null}
        </table>
      </div>
    </div>
  );
}
