"use client";

import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { del, get, post, put } from "@/lib/api";
import { Notice, Spinner, Toggle } from "@/components/ui";
import { DataGrid, type ColDef } from "@/components/DataGrid";
import { useSaveItem } from "@/components/SaveBus";
import { CheckIcon, CloseIcon, TrashIcon } from "@/components/icons";

type Source = {
  id: number;
  label: string;
  host: string;
  weight: number;
  categories: string[];
  allowImages: boolean;
  enabled: boolean;
  _new?: boolean;
};

type CategoryOption = { code: string; label: string; level: string; parentCode: string | null };

/** Inline, Apple-style two-step delete confirmation rendered inside the grid cell. */
function DeleteCell(props: any) {
  const [confirming, setConfirming] = useState(false);
  const [busy, setBusy] = useState(false);

  if (confirming) {
    return (
      <span className="flex items-center gap-1">
        <span className="mr-1 text-xs text-slate-500">Delete?</span>
        <button
          type="button"
          className="rounded-sm p-1 text-ok hover:bg-green-50 disabled:opacity-50"
          aria-label="Confirm delete"
          disabled={busy}
          onClick={async () => {
            setBusy(true);
            try {
              await props.context.onDelete(props.data);
            } finally {
              setBusy(false);
            }
          }}
        >
          <CheckIcon className="h-4 w-4" />
        </button>
        <button
          type="button"
          className="rounded-sm p-1 text-slate-500 hover:bg-slate-100"
          aria-label="Cancel delete"
          onClick={() => setConfirming(false)}
        >
          <CloseIcon className="h-4 w-4" />
        </button>
      </span>
    );
  }

  return (
    <button
      type="button"
      className="rounded-sm p-1 text-slate-400 hover:bg-red-50 hover:text-danger"
      aria-label={`Delete ${props.data?.label || "source"}`}
      onClick={() => setConfirming(true)}
    >
      <TrashIcon className="h-4 w-4" />
    </button>
  );
}

/** Categories cell: selected category-tree nodes as chips + an Edit button (opens the picker). */
function CategoriesCell(p: any) {
  const codes: string[] = p.data?.categories || [];
  const labelFor: (c: string) => string = p.context?.labelFor || ((c: string) => c);
  return (
    <div className="flex flex-wrap items-center gap-1 py-1">
      {codes.length === 0 ? (
        <span className="text-xs italic text-slate-400">all categories</span>
      ) : (
        codes.map((c) => (
          <span key={c} className="rounded-sm bg-blue-50 px-1.5 py-0.5 text-xs text-blue-700">{labelFor(c)}</span>
        ))
      )}
      <button
        type="button"
        onClick={() => p.context?.onEditCategories?.(p.data)}
        className="ml-1 rounded-sm px-1.5 py-0.5 text-xs font-medium text-brand hover:bg-slate-100"
      >
        Edit
      </button>
    </div>
  );
}

/** Multiselect picker over the full category tree (all levels selectable). */
function CategoryPickerModal({
  row,
  options,
  onClose,
  onApply,
}: {
  row: Source;
  options: CategoryOption[];
  onClose: () => void;
  onApply: (codes: string[]) => void;
}) {
  const [selected, setSelected] = useState<Set<string>>(new Set(row.categories));
  const [q, setQ] = useState("");

  // Order depth-first (main → parent → child) for a readable, indented list.
  const ordered = useMemo(() => {
    const byParent: Record<string, CategoryOption[]> = {};
    for (const o of options) (byParent[o.parentCode ?? "__root__"] ??= []).push(o);
    const out: (CategoryOption & { depth: number })[] = [];
    const walk = (list: CategoryOption[] | undefined, depth: number) => {
      for (const n of list ?? []) {
        out.push({ ...n, depth });
        walk(byParent[n.code], depth + 1);
      }
    };
    walk(byParent["__root__"], 0);
    return out;
  }, [options]);

  const needle = q.trim().toLowerCase();
  const visible = needle ? ordered.filter((o) => o.label.toLowerCase().includes(needle) || o.code.toLowerCase().includes(needle)) : ordered;
  const toggle = (code: string) =>
    setSelected((s) => {
      const n = new Set(s);
      n.has(code) ? n.delete(code) : n.add(code);
      return n;
    });

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30 p-4" onClick={onClose} role="presentation">
      <div className="card flex max-h-[80vh] w-full max-w-lg flex-col" onClick={(e) => e.stopPropagation()} role="dialog" aria-modal="true">
        <div className="mb-1 flex items-center justify-between">
          <h3 className="font-semibold text-slate-900">Categories — {row.label}</h3>
          <button type="button" onClick={onClose} aria-label="Close" className="rounded-sm p-1 text-slate-400 hover:bg-slate-100"><CloseIcon className="h-4 w-4" /></button>
        </div>
        <p className="mb-2 text-xs text-slate-500">
          Category-tree nodes this source is <em>preferred</em> for (all levels selectable). Leave empty = usable for every category.
        </p>
        <input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Search categories…" className="input mb-2" />
        <div className="flex-1 overflow-auto rounded-sm border border-slate-200">
          {visible.length === 0 ? (
            <p className="p-3 text-sm text-slate-400">No matching categories.</p>
          ) : (
            visible.map((o) => (
              <label key={o.code} className="flex cursor-pointer items-center gap-2 px-2 py-1 text-sm hover:bg-slate-50" style={{ paddingLeft: `${0.5 + o.depth * 1.1}rem` }}>
                <input type="checkbox" checked={selected.has(o.code)} onChange={() => toggle(o.code)} />
                <span className={o.level === "main" ? "font-semibold text-slate-800" : "text-slate-700"}>{o.label}</span>
                <span className="font-mono text-[10px] text-slate-400">{o.code}</span>
              </label>
            ))
          )}
        </div>
        <div className="mt-3 flex items-center justify-between">
          <button type="button" className="text-xs text-slate-500 hover:underline" onClick={() => setSelected(new Set())}>Clear all</button>
          <div className="flex gap-2">
            <button type="button" className="btn-secondary" onClick={onClose}>Cancel</button>
            <button type="button" className="btn-primary" onClick={() => onApply([...selected])}>Apply</button>
          </div>
        </div>
      </div>
    </div>
  );
}

export default function SourcesPanel() {
  const [rows, setRows] = useState<Source[] | null>(null);
  const [dirty, setDirty] = useState<Record<string, true>>({});
  const [aiFallback, setAiFallback] = useState(false);
  const [aiDirty, setAiDirty] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [msg, setMsg] = useState<string | null>(null);
  const [categoryOptions, setCategoryOptions] = useState<CategoryOption[]>([]);
  const [pickerRow, setPickerRow] = useState<Source | null>(null);
  const tempId = useRef(-1);

  const load = useCallback(async () => {
    try {
      const [d, s, c] = await Promise.all([
        get<{ sources: Source[] }>("sources"),
        get<{ settings: Record<string, unknown> }>("settings"),
        get<{ categoryRules: CategoryOption[] }>("category-rules"),
      ]);
      setRows(d.sources);
      setAiFallback(!!s.settings["research.ai_fallback_beyond_trusted"]);
      setCategoryOptions(c.categoryRules.map((n) => ({ code: n.code, label: n.label, level: n.level, parentCode: n.parentCode })));
      setDirty({});
      setAiDirty(false);
    } catch (e) {
      setError((e as Error).message);
    }
  }, []);

  const labelFor = useCallback((code: string) => categoryOptions.find((o) => o.code === code)?.label ?? code, [categoryOptions]);
  useEffect(() => {
    load();
  }, [load]);

  const markDirty = useCallback((id: number) => setDirty((p) => ({ ...p, [String(id)]: true })), []);

  // Stable save handler for the floating Save button: POST new rows, PUT edited ones.
  const rowsRef = useRef<Source[] | null>(null);
  const dirtyRef = useRef<Record<string, true>>({});
  const aiRef = useRef<{ value: boolean; dirty: boolean }>({ value: false, dirty: false });
  rowsRef.current = rows;
  dirtyRef.current = dirty;
  aiRef.current = { value: aiFallback, dirty: aiDirty };

  const saveAll = useCallback(async () => {
    const current = rowsRef.current ?? [];
    const toSave = current.filter((r) => dirtyRef.current[String(r.id)]);
    for (const r of toSave) {
      if (r._new) {
        await post("sources", { label: r.label, host: r.host, weight: r.weight, categories: r.categories });
      } else {
        await put(`sources/${r.id}`, r);
      }
    }
    if (aiRef.current.dirty) {
      await put("settings", { settings: { "research.ai_fallback_beyond_trusted": aiRef.current.value } });
    }
    await load();
    setMsg(`Saved ${toSave.length} source${toSave.length === 1 ? "" : "s"}${aiRef.current.dirty ? " + AI-fallback setting" : ""}.`);
  }, [load]);

  useSaveItem("trusted-sources", Object.keys(dirty).length > 0 || aiDirty, saveAll);

  const onDelete = useCallback(async (row: Source) => {
    setMsg(null);
    if (row._new) {
      setRows((rs) => (rs ? rs.filter((r) => r.id !== row.id) : rs));
      setDirty((p) => {
        const n = { ...p };
        delete n[String(row.id)];
        return n;
      });
      return;
    }
    try {
      await del(`sources/${row.id}`);
      setRows((rs) => (rs ? rs.filter((r) => r.id !== row.id) : rs));
      setMsg(`Deleted “${row.label}”.`);
    } catch (e) {
      setError((e as Error).message);
    }
  }, []);

  function addRow() {
    const id = tempId.current--;
    setRows((rs) => [...(rs ?? []), { id, label: "New source", host: "", weight: 0.85, categories: [], allowImages: false, enabled: true, _new: true }]);
    markDirty(id);
  }

  if (error) return <Notice kind="error">Could not load sources: {error}</Notice>;
  if (!rows) return <Spinner label="Loading trusted sources…" />;

  const columnDefs: ColDef[] = [
    { field: "label", headerName: "Label", editable: true, flex: 1, minWidth: 150 },
    { field: "host", headerName: "Host", editable: true, flex: 1, minWidth: 150 },
    {
      field: "weight",
      headerName: "TSQ weight",
      editable: true,
      width: 130,
      cellDataType: "number",
      valueParser: (p: any) => {
        const n = parseFloat(p.newValue);
        return Number.isNaN(n) ? p.oldValue : Math.max(0, Math.min(1, n));
      },
    },
    {
      headerName: "Categories",
      colId: "categories",
      editable: false,
      flex: 1,
      minWidth: 240,
      autoHeight: true,
      cellRenderer: CategoriesCell,
      cellClass: "flex items-center",
    },
    { field: "enabled", headerName: "Enabled", editable: true, cellDataType: "boolean", width: 110 },
    { field: "allowImages", headerName: "Images", editable: true, cellDataType: "boolean", width: 100 },
    {
      headerName: "",
      colId: "actions",
      width: 120,
      editable: false,
      sortable: false,
      filter: false,
      cellRenderer: DeleteCell,
      cellClass: "flex items-center",
    },
  ];

  return (
    <>
      <p className="mb-3 text-sm text-slate-600">
        Whitelist used by AI Research (TSQ weight) and Image Sourcing. <strong>Double-click a cell to edit</strong>;
        changes are saved together with the floating <em>Save</em> button. Delete a row with the trash icon (it asks to
        confirm).
      </p>
      {msg && <div className="mb-4"><Notice kind="success">{msg}</Notice></div>}

      <div className="card mb-4">
        <Toggle
          id="ai-fallback"
          checked={aiFallback}
          onChange={(v) => { setAiFallback(v); setAiDirty(true); }}
          label="AI fallback beyond trusted sources"
          info="When a value can't reach the quality threshold with trusted sources, a last-resort retry lets the AI use any reputable source. The candidate scores lower (untrusted TSQ) but gives reviewers something to work with instead of an empty attribute. Saved with the floating Save button."
        />
      </div>

      <div className="mb-3">
        <button type="button" className="btn-secondary" onClick={addRow}>+ Add source</button>
      </div>

      <DataGrid
        rowData={rows}
        columnDefs={columnDefs}
        autoHeight
        getRowId={(p: any) => String(p.data.id)}
        context={{ onDelete, onEditCategories: setPickerRow, labelFor }}
        onCellValueChanged={(e: any) => markDirty(e.data.id)}
        stopEditingWhenCellsLoseFocus
      />

      {pickerRow && (
        <CategoryPickerModal
          row={pickerRow}
          options={categoryOptions}
          onClose={() => setPickerRow(null)}
          onApply={(codes) => {
            setRows((rs) => (rs ? rs.map((r) => (r.id === pickerRow.id ? { ...r, categories: codes } : r)) : rs));
            markDirty(pickerRow.id);
            setPickerRow(null);
          }}
        />
      )}
    </>
  );
}
