"use client";

import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { get, post, put } from "@/lib/api";
import { withBase } from "@/lib/basePath";
import { Badge, Notice, Spinner } from "@/components/ui";
import { DataGrid, type ColDef } from "@/components/DataGrid";
import { Modal } from "@/components/Modal";
import { StatusBadge, stepLabel } from "@/components/processingStatus";
import { InfoIcon, BanIcon, CheckIcon, CloseIcon } from "@/components/icons";
import { timeAgo } from "@/lib/format";

type QueueItem = {
  position: number;
  productId: string;
  sku: string | null;
  gtin: string | null;
  productName: string | null;
  variantName: string | null;
  status: string;
  pending: boolean;
  claimedAt: string | null;
  runCount: number;
  targeted: boolean;
};

type QueueData = {
  count: number;
  activeCount: number;
  pendingCount: number;
  ingestionEnabled: boolean;
  killSwitchActive: boolean;
  pendingError: string | null;
  items: QueueItem[];
};

function TargetedCell(p: any) {
  return p.data?.targeted ? (
    <span className="badge bg-blue-100 text-brand" title="Targeted enrichment job — only selected attributes are researched">Targeted</span>
  ) : (
    <span className="text-slate-300">—</span>
  );
}

function InfoCell(p: any) {
  return (
    <button
      type="button"
      className="rounded-sm p-1 text-slate-400 hover:bg-slate-100 hover:text-brand"
      aria-label="Processing history"
      onClick={() => p.context.onInfo(p.data.productId)}
    >
      <InfoIcon className="h-4 w-4" />
    </button>
  );
}

export default function QueuePanel() {
  const [data, setData] = useState<QueueData | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [updatedAt, setUpdatedAt] = useState<string | null>(null);
  const [selected, setSelected] = useState<string[]>([]);
  const [pickupBusy, setPickupBusy] = useState(false);
  const [pickupMsg, setPickupMsg] = useState<string | null>(null);
  const [historyId, setHistoryId] = useState<string | null>(null);

  // Pause the 4s auto-refresh while the operator is dragging or editing a row, so
  // a refresh doesn't clobber an in-progress reorder. Direct load(true) bypasses it.
  const pauseMouseRef = useRef(false);
  const pauseEditRef = useRef(false);

  const load = useCallback(async (force = false) => {
    if (!force && (pauseMouseRef.current || pauseEditRef.current)) return;
    try {
      const d = await get<QueueData>("queue");
      setData(d);
      setUpdatedAt(new Date().toISOString());
      setError(null);
    } catch (e) {
      setError((e as Error).message);
    }
  }, []);

  useEffect(() => {
    load(true);
    const t = setInterval(() => load(), 4000);
    return () => clearInterval(t);
  }, [load]);

  const saveOrder = useCallback(
    async (ids: string[]) => {
      try {
        await post("queue/order", { productIds: ids });
      } catch (e) {
        setError((e as Error).message);
      }
      await load(true);
    },
    [load],
  );

  // Pending rows' product ids in current display order (the order to persist).
  const pendingOrder = (api: any): string[] => {
    const ids: string[] = [];
    api.forEachNodeAfterFilterAndSort((n: any) => {
      if (n.data?.pending) ids.push(n.data.productId);
    });
    return ids;
  };

  const onRowDragEnd = useCallback(
    (e: any) => {
      pauseMouseRef.current = false;
      saveOrder(pendingOrder(e.api));
    },
    [saveOrder],
  );

  // Editing the "#": move the row to the typed position among the pending rows.
  const onCellValueChanged = useCallback(
    (e: any) => {
      if (e.colDef.field !== "position" || !e.data?.pending) return;
      const rows: any[] = [];
      e.api.forEachNodeAfterFilterAndSort((n: any) => rows.push(n.data));
      const pending = rows.filter((r) => r.pending);
      const activeCount = rows.length - pending.length;
      const ids = pending.map((r) => r.productId);
      const from = ids.indexOf(e.data.productId);
      const target = parseInt(String(e.newValue), 10);
      if (from < 0 || !Number.isFinite(target)) {
        load(true);
        return;
      }
      let to = target - 1 - activeCount; // global "#" → pending-relative index
      to = Math.max(0, Math.min(pending.length - 1, to));
      ids.splice(from, 1);
      ids.splice(to, 0, e.data.productId);
      saveOrder(ids);
    },
    [saveOrder, load],
  );

  const columnDefs = useMemo<ColDef[]>(
    () => [
      {
        field: "position",
        headerName: "#",
        width: 96,
        filter: false,
        sortable: false,
        // Pending products are reorderable: drag the handle, or double-click to type a position.
        rowDrag: (p: any) => !!p.data?.pending,
        editable: (p: any) => !!p.data?.pending,
        valueParser: (p: any) => {
          const n = parseInt(String(p.newValue), 10);
          return Number.isFinite(n) ? n : p.oldValue;
        },
        cellClass: (p: any) => (p.data?.pending ? "text-slate-700" : "text-slate-400"),
      },
      { field: "status", headerName: "AI status", width: 180, cellRenderer: (p: any) => <StatusBadge status={String(p.value)} /> },
      { field: "sku", headerName: "SKU", flex: 1, minWidth: 140, valueFormatter: (p: any) => p.value || "—" },
      { field: "gtin", headerName: "GTIN", width: 160, valueFormatter: (p: any) => p.value || "—" },
      {
        headerName: "Product name",
        colId: "productName",
        flex: 1,
        minWidth: 160,
        valueGetter: (p: any) => p.data.productName || "—",
      },
      { field: "variantName", headerName: "Variant", width: 140, valueFormatter: (p: any) => p.value || "—" },
      { headerName: "Job", colId: "targeted", width: 110, cellRenderer: TargetedCell, cellClass: "flex items-center", valueGetter: (p: any) => (p.data.targeted ? "Targeted" : "Full") },
      { headerName: "", colId: "info", width: 64, sortable: false, filter: false, cellRenderer: InfoCell, cellClass: "flex items-center justify-center", pinned: "right" },
    ],
    [],
  );

  async function forcePickup() {
    if (selected.length === 0) return;
    setPickupBusy(true);
    setPickupMsg(null);
    try {
      const r = await post<{ ok: boolean; detail?: string; error?: string }>("queue/pickup", { productIds: selected });
      setPickupMsg(r.detail || r.error || "Done.");
      setSelected([]);
      await load();
    } catch (e) {
      setPickupMsg((e as Error).message);
    } finally {
      setPickupBusy(false);
    }
  }

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

  const { items, activeCount, pendingCount } = data;

  return (
    <>
      <p className="mb-3 text-sm text-slate-600">
        Products being processed, in the order they run, followed by products still <strong>New</strong> in Ergonode.
        Set the processing order of pending products by <strong>dragging a row</strong> (the handle in the <strong>#</strong>{" "}
        column) or <strong>double-clicking the #</strong> to type a position. Tick one or more pending rows and{" "}
        <strong>Force pickup</strong> to process them now; click the ⓘ for a product&rsquo;s history.
      </p>

      {data.killSwitchActive ? (
        <div className="mb-4"><Notice kind="error">The kill-switch is active — processing is paused. Resume it (Services page or Dashboard).</Notice></div>
      ) : !data.ingestionEnabled ? (
        <div className="mb-4"><Notice kind="error">Ingestion is <strong>off</strong> — New products aren&rsquo;t auto-picked-up. Enable it under Services → Features, or use Force pickup.</Notice></div>
      ) : pendingCount > 0 ? (
        <div className="mb-4"><Notice kind="info">{pendingCount} product{pendingCount === 1 ? " is" : "s are"} New in Ergonode, awaiting the next poll. They process automatically when the poller + worker run, or use Force pickup.</Notice></div>
      ) : null}

      {data.pendingError && <div className="mb-4"><Notice kind="error">Could not read New products from Ergonode: {data.pendingError}</Notice></div>}

      <div className="mb-3 flex flex-wrap items-center gap-3">
        <button type="button" className="btn-primary" onClick={forcePickup} disabled={pickupBusy || selected.length === 0}>
          {pickupBusy ? "Picking up…" : `Force pickup${selected.length ? ` (${selected.length})` : ""}`}
        </button>
        <span className="text-xs text-slate-500" aria-live="polite">
          {activeCount} processing · {pendingCount} awaiting pickup · updated {timeAgo(updatedAt || undefined)}
        </span>
        {pickupMsg && <span className="text-sm text-ok" role="status">{pickupMsg}</span>}
      </div>

      <div
        onMouseDown={() => { pauseMouseRef.current = true; }}
        onMouseUp={() => { window.setTimeout(() => { pauseMouseRef.current = false; }, 250); }}
        onMouseLeave={() => { pauseMouseRef.current = false; }}
      >
        <DataGrid
          rowData={items}
          columnDefs={columnDefs}
          height={560}
          getRowId={(p: any) => String(p.data.productId)}
          context={{ onInfo: (id: string) => setHistoryId(id) }}
          rowSelection={{ mode: "multiRow", checkboxes: true, headerCheckbox: true, enableClickSelection: false }}
          isRowSelectable={(node: any) => !!node.data?.pending}
          onSelectionChanged={(e: any) => setSelected(e.api.getSelectedRows().map((r: any) => r.productId))}
          rowDragManaged
          onRowDragEnd={onRowDragEnd}
          onCellValueChanged={onCellValueChanged}
          onCellEditingStarted={() => { pauseEditRef.current = true; }}
          onCellEditingStopped={() => { pauseEditRef.current = false; }}
        />
      </div>
      {data.count < activeCount + pendingCount && (
        <p className="mt-2 text-xs text-slate-400">Showing the first {data.count} of {activeCount + pendingCount}.</p>
      )}

      <HistoryModal productId={historyId} onClose={() => setHistoryId(null)} />
    </>
  );
}

// ---- per-product history + progress modal ---------------------------------

type HistoryEvent = { step: string; detail: string; outcome: string | null; attribute?: string | null; occurredAt: string };
type AttrScores = { aqs: number; brc: number; tsq: number; fpv: number; csc: number; vfq: number };
type HistoryAttribute = {
  code: string;
  label: string;
  requirement: string;
  initial: string | null;
  state: string;
  value: string | null;
  scores: AttrScores | null;
  log: { detail: string; outcome: string | null; occurredAt: string }[];
};
type HistoryImage = {
  id: number;
  type: string;
  position: number;
  filename: string | null;
  accepted: boolean;
  segmented: boolean;
  aqs: number;
  sourceUrl: string | null;
  rejectionReason: string | null;
  hasFile: boolean;
};
type HistoryVideo = {
  id: number;
  mode: string;
  filename: string | null;
  accepted: boolean;
  durationSeconds: number;
  mimeType: string;
  modelId: string | null;
  costChf: number;
  seedImage: string | null;
  rejectionReason: string | null;
  hasFile: boolean;
};
type HistoryData = {
  productId: string;
  currentStatus: string;
  progress: { key: string; state: string }[];
  finalState: string;
  attributes: HistoryAttribute[];
  images: HistoryImage[];
  videos: HistoryVideo[];
  events: HistoryEvent[];
};

export function HistoryModal({ productId, onClose }: { productId: string | null; onClose: () => void }) {
  const [data, setData] = useState<HistoryData | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [tab, setTab] = useState<"attributes" | "gallery" | "log">("attributes");

  const fetchData = useCallback(async (id: string) => {
    try {
      setData(await get<HistoryData>(`queue/${id}/history`));
      setError(null);
    } catch (e) {
      setError((e as Error).message);
    }
  }, []);

  useEffect(() => {
    if (!productId) {
      setData(null);
      return;
    }
    setData(null);
    setError(null);
    fetchData(productId);
    const t = setInterval(() => fetchData(productId), 4000); // live progress
    return () => clearInterval(t);
  }, [productId, fetchData]);

  const skip = useCallback(
    async (code: string) => {
      if (!productId) return;
      try {
        await put("attributes/research-flags", { flags: { [code]: false } });
        await fetchData(productId);
      } catch (e) {
        setError((e as Error).message);
      }
    },
    [productId, fetchData],
  );

  return (
    <Modal open={!!productId} onClose={onClose} title="Product data aggregation" width="max-w-5xl">
      {error && <Notice kind="error">{error}</Notice>}
      {!error && !data && <Spinner label="Loading…" />}
      {data && (
        <>
          <ProgressTracker progress={data.progress} finalState={data.finalState} />

          <div className="mt-6 flex gap-1 border-b border-slate-200">
            {([
              ["attributes", "Attribute overview"],
              ["gallery", "Gallery"],
              ["log", "Log"],
            ] as const).map(([id, label]) => (
              <button
                key={id}
                type="button"
                onClick={() => setTab(id)}
                className={`-mb-px border-b-2 px-3 py-2 text-sm font-medium ${tab === id ? "border-brand text-brand" : "border-transparent text-slate-500 hover:text-slate-700"}`}
              >
                {label}
              </button>
            ))}
          </div>

          <div className="mt-4">
            {tab === "attributes" ? (
              <AttributeOverview attributes={data.attributes} onSkip={skip} />
            ) : tab === "gallery" ? (
              <GalleryView productId={data.productId} images={data.images} videos={data.videos ?? []} />
            ) : (
              <LogList events={data.events} />
            )}
          </div>
        </>
      )}
    </Modal>
  );
}

const STATE_STYLES: Record<string, string> = {
  new: "bg-slate-100 text-slate-600",
  queued: "bg-slate-100 text-slate-600",
  researching: "bg-blue-100 text-blue-700",
  "not found": "bg-amber-100 text-amber-700",
  done: "bg-emerald-100 text-emerald-700",
};

function AttrStateBadge(p: { value?: string }) {
  const state = p.value ?? "new";
  const label = state === "researching" ? "researching…" : state;
  return <span className={`inline-block rounded-sm px-1.5 py-0.5 text-xs font-medium ${STATE_STYLES[state] ?? "bg-slate-100 text-slate-600"}`}>{label}</span>;
}

function SkipCell(p: { data?: HistoryAttribute; context?: { onSkip: (code: string) => void } }) {
  if (!p.data) return null;
  const code = p.data.code;
  return (
    <button
      type="button"
      onClick={() => p.context?.onSkip(code)}
      aria-label={`Skip ${p.data.label} (exclude from research)`}
      title="Skip — exclude this attribute from research"
      className="rounded-sm p-1 text-slate-400 hover:bg-amber-50 hover:text-amber-600"
    >
      <BanIcon className="h-4 w-4" />
    </button>
  );
}

const REQUIREMENT_STYLES: Record<string, string> = {
  Required: "bg-emerald-100 text-emerald-700",
  Optional: "bg-blue-100 text-blue-700",
  Skip: "bg-slate-100 text-slate-500",
};

function RequirementChip(p: { value?: string }) {
  const r = p.value || "—";
  return <span className={`inline-block rounded-sm px-2 py-0.5 text-xs font-medium ${REQUIREMENT_STYLES[r] ?? "bg-slate-100 text-slate-600"}`}>{r}</span>;
}

function AttributeOverview({ attributes, onSkip }: { attributes: HistoryAttribute[]; onSkip: (code: string) => void }) {
  const columnDefs = useMemo<ColDef[]>(
    () => [
      {
        headerName: "Attribute",
        field: "label",
        flex: 1,
        minWidth: 190,
        cellClass: "flex items-center font-medium text-slate-800",
      },
      { headerName: "Requirement", field: "requirement", width: 130, cellRenderer: RequirementChip, cellClass: "flex items-center" },
      { headerName: "State", field: "state", width: 130, cellRenderer: AttrStateBadge, cellClass: "flex items-center" },
      { headerName: "Initial value", field: "initial", flex: 1, minWidth: 150, valueFormatter: (p: any) => p.value || "—", tooltipField: "initial" },
      { headerName: "Sourced value", field: "value", flex: 1, minWidth: 150, valueFormatter: (p: any) => p.value || "—", tooltipField: "value" },
      {
        headerName: "Score",
        colId: "score",
        width: 90,
        valueGetter: (p: any) => (p.data.scores ? p.data.scores.aqs : null),
        valueFormatter: (p: any) => (p.value != null ? Number(p.value).toFixed(2) : "—"),
        tooltipValueGetter: (p: any) =>
          p.data.scores ? `BRC ${p.data.scores.brc} · TSQ ${p.data.scores.tsq} · FPV ${p.data.scores.fpv} · CSC ${p.data.scores.csc} · VFQ ${p.data.scores.vfq}` : "",
      },
      { headerName: "", colId: "skip", width: 60, sortable: false, filter: false, pinned: "right", cellRenderer: SkipCell, cellClass: "flex items-center justify-center" },
    ],
    [],
  );

  if (!attributes.length) {
    return (
      <Notice kind="info">
        No research-enabled attributes to show for this product yet. Enable attributes under <strong>Services → Attributes</strong>,
        or the product may not be readable from Ergonode.
      </Notice>
    );
  }
  return <DataGrid rowData={attributes} columnDefs={columnDefs} context={{ onSkip }} getRowId={(p: any) => String(p.data.code)} height={460} />;
}

function LogList({ events }: { events: HistoryEvent[] }) {
  if (events.length === 0) return <p className="text-sm text-slate-500">No recorded actions yet.</p>;
  return (
    <ol className="space-y-2">
      {events.map((e, i) => {
        const isError =
          ["failed", "rejected"].includes(e.step) ||
          ["failed", "rejected"].includes(e.outcome || "") ||
          /push failed|HTTP \d{3}|error/i.test(e.detail || "");
        return (
          <li key={i} className="flex items-start gap-3 border-t border-slate-100 pt-2 first:border-0 first:pt-0">
            <span className="w-40 shrink-0 text-xs tabular-nums text-slate-400">{new Date(e.occurredAt).toLocaleString()}</span>
            <span className="shrink-0"><StatusBadge status={e.step} /></span>
            <span className={isError ? "whitespace-pre-wrap wrap-break-word rounded-sm bg-red-50 px-2 py-1 font-mono text-xs text-danger" : "text-sm text-slate-700"}>
              {e.detail || "—"}
              {e.outcome && !isError ? <span className="ml-1 text-slate-400">({e.outcome})</span> : null}
            </span>
          </li>
        );
      })}
    </ol>
  );
}

function GalleryView({ productId, images, videos }: { productId: string; images: HistoryImage[]; videos: HistoryVideo[] }) {
  if (!images.length && !videos.length) {
    return (
      <Notice kind="info">
        No media yet. The image pipeline (sourcing → generation → segmentation → alt text) runs after research;
        sourced and generated images — and any product video (Veo) — appear here.
      </Notice>
    );
  }
  const accepted = images.filter((i) => i.accepted).length;
  const segmented = images.filter((i) => i.segmented).length;
  const acceptedVideos = videos.filter((v) => v.accepted).length;
  return (
    <div className="space-y-6">
      {videos.length > 0 && (
        <section>
          <p className="mb-3 text-sm text-slate-600">
            {acceptedVideos} generated video{acceptedVideos === 1 ? "" : "s"} (Vertex Veo).
          </p>
          <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
            {videos.map((vid) => (
              <VideoCard key={vid.id} productId={productId} vid={vid} />
            ))}
          </div>
        </section>
      )}
      {images.length > 0 && (
        <section>
          <p className="mb-3 text-sm text-slate-600">
            {accepted} accepted image{accepted === 1 ? "" : "s"} · {segmented} segmented. The tick/cross shows whether
            background segmentation was applied (mood images are not segmented).
          </p>
          <div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4">
            {images.map((img) => (
              <ImageCard key={img.id} productId={productId} img={img} />
            ))}
          </div>
        </section>
      )}
    </div>
  );
}

function VideoCard({ productId, vid }: { productId: string; vid: HistoryVideo }) {
  const modeLabel = vid.mode === "studio_3d" ? "Studio 3D turntable" : vid.mode === "lifestyle" ? "Lifestyle" : vid.mode;
  return (
    <div className="overflow-hidden rounded-lg border border-slate-200">
      <div className="relative flex aspect-video items-center justify-center bg-slate-900">
        {vid.hasFile ? (
          <video src={withBase(`/api/backend/queue/${productId}/video/${vid.id}`)} className="h-full w-full object-contain" controls preload="metadata" />
        ) : (
          <div className="p-3 text-center text-xs text-slate-400">{vid.rejectionReason ?? "no video"}</div>
        )}
      </div>
      <div className="flex items-center justify-between gap-1 px-2 py-1.5 text-xs">
        <span className="text-slate-600">
          {modeLabel}
          {vid.durationSeconds ? ` · ${vid.durationSeconds}s` : ""}
        </span>
        {vid.accepted ? <Badge kind="ok">ok</Badge> : <Badge kind="danger">rejected</Badge>}
      </div>
    </div>
  );
}

function ImageCard({ productId, img }: { productId: string; img: HistoryImage }) {
  const segTitle = img.segmented ? "Background segmented" : img.type === "mood" ? "Mood image — not segmented" : "Not segmented";
  return (
    <div className="overflow-hidden rounded-lg border border-slate-200">
      <div className="relative flex aspect-square items-center justify-center bg-slate-50">
        {img.hasFile ? (
          // eslint-disable-next-line @next/next/no-img-element
          <img src={withBase(`/api/backend/queue/${productId}/image/${img.id}`)} alt={img.filename ?? ""} className="h-full w-full object-contain" loading="lazy" />
        ) : (
          <div className="p-3 text-center text-xs text-slate-400">{img.rejectionReason ?? "no image"}</div>
        )}
        <span
          title={segTitle}
          className={`absolute right-1 top-1 flex items-center gap-0.5 rounded-sm px-1 py-0.5 text-[10px] font-medium ${img.segmented ? "bg-emerald-100 text-emerald-700" : "bg-slate-200 text-slate-500"}`}
        >
          {img.segmented ? <CheckIcon className="h-3 w-3" /> : <CloseIcon className="h-3 w-3" />} seg
        </span>
      </div>
      <div className="flex items-center justify-between gap-1 px-2 py-1.5 text-xs">
        <span className="capitalize text-slate-600">{img.type}{img.position ? ` #${img.position}` : ""}</span>
        {img.accepted ? <Badge kind="ok">ok</Badge> : <Badge kind="danger">rejected</Badge>}
      </div>
    </div>
  );
}

function ProgressTracker({ progress, finalState }: { progress: { key: string; state: string }[]; finalState: string }) {
  const nodes = [
    ...progress,
    { key: "final", state: finalState === "pending" ? "pending" : finalState },
  ];
  const dot = (state: string) => {
    if (state === "done") return "bg-ok text-white";
    if (state === "current") return "bg-brand text-white ring-4 ring-blue-100";
    if (state === "review" || state === "requeued") return "bg-warn text-white";
    if (state === "failed" || state === "rejected") return "bg-danger text-white";
    return "bg-slate-200 text-slate-500";
  };
  const label = (key: string) => (key === "final" ? (finalState === "pending" ? "Done" : stepLabel(finalState)) : stepLabel(key));

  return (
    <div className="flex items-start justify-between gap-1">
      {nodes.map((n, i) => (
        <div key={n.key} className="flex flex-1 flex-col items-center text-center">
          <div className="flex w-full items-center">
            <span className={`mx-auto flex h-7 w-7 items-center justify-center rounded-full text-xs font-semibold ${dot(n.state)}`}>
              {n.state === "done" ? "✓" : i + 1}
            </span>
          </div>
          <span className="mt-1 text-[11px] leading-tight text-slate-600">{label(n.key)}</span>
        </div>
      ))}
    </div>
  );
}
