/**
 * Modal that lets the user add products to the PO via a single unified
 * interface that covers search, paste, and file upload.
 *
 * The three previously-separate tabs are replaced by one input surface:
 *   - Type a query → live-search `/api/products/search` and show results
 *   - Paste multi-line or tab-separated data → onPaste intercepts, parses,
 *     and switches to a column-mapping preview
 *   - Click "Upload file" OR drop a .xlsx/.csv onto the dialog → parse and
 *     show the same column-mapping preview
 *
 * The "mode" of the interface is derived from state (presence of a parsed
 * table) rather than stored — no state machine, no drift.
 *
 * Paste/upload funnel through the same pipeline: parse → auto-detect
 * columns → preview w/ inline remapping → resolve identifiers → (optional
 * review dialog for ambiguous matches) → onAdd. Search results resolve
 * directly since each row is a known pid.
 */

import { useCallback, useMemo, useRef, useState } from "react";
import { useDropzone } from "react-dropzone";
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";
import {
  Loader2,
  Upload,
  FileSpreadsheet,
  AlertCircle,
  Search as SearchIcon,
  Check,
  X,
} from "lucide-react";
import { toast } from "sonner";
import { apiClient } from "@/utils/apiClient";
import type { SearchProduct } from "@/components/product-editor/types";
import {
  parsePastedTable,
  parseWorkbookFirstSheet,
  autoDetectColumns,
  applyMapping,
  type ParsedTable,
  type DetectedMapping,
  type ColumnRole,
} from "./parseSpreadsheet";
import { resolveIdentifiers } from "./resolveIdentifiers";
import type { ResolveResult } from "./types";
import { ReviewMatchesDialog } from "./ReviewMatchesDialog";
import { cn } from "@/lib/utils";

export interface AddProductsResult {
  /** Resolved (pid, qty) pairs ready to be hydrated by the parent. */
  items: Array<{ pid: number; qty: number }>;
}

interface AddProductsDialogProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  /** Pids that are already on the PO; used to dim/disable in search results. */
  existingPids: Set<number>;
  /** Called when the user has finalized a list of pids to add. */
  onAdd: (result: AddProductsResult) => void;
}

interface QuickSearchResult {
  pid: number;
  title: string;
  sku: string;
  barcode: string;
  brand: string;
  line: string;
  regular_price: number;
  image_175: string | null;
}

const SEARCH_LIMIT = 100;

export function AddProductsDialog({
  open,
  onOpenChange,
  existingPids,
  onAdd,
}: AddProductsDialogProps) {
  // ----- Search state --------------------------------------------------------
  const [query, setQuery] = useState("");
  const [searchResults, setSearchResults] = useState<QuickSearchResult[]>([]);
  const [searchTotal, setSearchTotal] = useState(0);
  const [isSearching, setIsSearching] = useState(false);
  const [searched, setSearched] = useState(false);

  // ----- Parse state (from paste/upload) ------------------------------------
  const [table, setTable] = useState<ParsedTable | null>(null);
  const [mapping, setMapping] = useState<DetectedMapping | null>(null);
  const [filename, setFilename] = useState<string | null>(null);

  // ----- Review (ambiguous match) state --------------------------------------
  const [reviewOpen, setReviewOpen] = useState(false);
  const [reviewResult, setReviewResult] = useState<ResolveResult | null>(null);
  const [resolving, setResolving] = useState(false);

  const fileInputRef = useRef<HTMLInputElement>(null);

  // Mode is derived: if we have a parsed table, we're showing the preview;
  // otherwise we're in search mode.
  const inParseMode = table !== null;

  // Computed at the dialog level so the sticky footer's Add button shows an
  // accurate count without having to plumb state out of ParsedPreview.
  // applyMapping is pure, so ParsedPreview computing the same thing for its
  // own summary line / row highlighting can't drift from this value.
  const parseImportable = useMemo(() => {
    if (!table || !mapping || mapping.identifierIdx < 0) {
      return { count: 0, hasIdentifier: false };
    }
    return {
      count: applyMapping(table, mapping).length,
      hasIdentifier: true,
    };
  }, [table, mapping]);

  // ----- Reset everything (used by Start over button) -----------------------
  const resetAll = useCallback(() => {
    setTable(null);
    setMapping(null);
    setFilename(null);
    setQuery("");
    setSearchResults([]);
    setSearchTotal(0);
    setSearched(false);
  }, []);

  // ----- Close handler that also resets ------------------------------------
  const handleClose = useCallback(
    (nextOpen: boolean) => {
      if (!nextOpen) resetAll();
      onOpenChange(nextOpen);
    },
    [onOpenChange, resetAll]
  );

  // ----- Search --------------------------------------------------------------
  const runSearch = useCallback(async () => {
    if (!query.trim()) return;
    setIsSearching(true);
    setSearched(true);
    try {
      const res = await apiClient.get<{ results: QuickSearchResult[]; total: number }>(
        "/api/products/search",
        { params: { q: query } }
      );
      setSearchResults(res.data.results ?? []);
      setSearchTotal(res.data.total ?? 0);
    } catch {
      toast.error("Search failed");
    } finally {
      setIsSearching(false);
    }
  }, [query]);

  const handleSelectSearchResult = useCallback(
    async (result: QuickSearchResult) => {
      // Look up full details to pull MOQ (to default qty sensibly)
      try {
        const res = await apiClient.get<SearchProduct[]>("/api/import/search-products", {
          params: { pid: result.pid },
        });
        const full = (res.data ?? [])[0];
        const moq = full?.moq && full.moq > 0 ? full.moq : 1;
        onAdd({ items: [{ pid: Number(result.pid), qty: moq }] });
      } catch {
        // Fall back to qty=1 if the detail lookup fails — still adds the product
        onAdd({ items: [{ pid: Number(result.pid), qty: 1 }] });
      }
      handleClose(false);
    },
    [onAdd, handleClose]
  );

  const handleLoadAllSearchResults = useCallback(async () => {
    const pids = searchResults
      .map((r) => Number(r.pid))
      .filter((pid) => !existingPids.has(pid));
    if (pids.length === 0) return;

    try {
      const res = await apiClient.get<SearchProduct[]>("/api/import/search-products", {
        params: { pid: pids.join(",") },
      });
      const items = (res.data ?? []).map((p) => ({
        pid: Number(p.pid),
        qty: p.moq && p.moq > 0 ? p.moq : 1,
      }));
      onAdd({ items });
      handleClose(false);
    } catch {
      toast.error("Failed to load products");
    }
  }, [searchResults, existingPids, onAdd, handleClose]);

  // ----- Paste interception --------------------------------------------------
  // If the clipboard content looks like tabular data (contains newlines or
  // tabs), we hijack the paste and switch to parse mode instead of letting
  // the text land in the search input.
  const handlePaste = useCallback((e: React.ClipboardEvent<HTMLInputElement>) => {
    const text = e.clipboardData.getData("text");
    if (!text || (!text.includes("\n") && !text.includes("\t"))) {
      // Normal paste — let it through
      return;
    }
    e.preventDefault();
    const parsed = parsePastedTable(text);
    if (!parsed.headers.length || !parsed.rows.length) {
      toast.error("No data detected in pasted content");
      return;
    }
    setTable(parsed);
    setMapping(autoDetectColumns(parsed.headers, parsed.rows));
    setFilename(null);
    setQuery("");
  }, []);

  // ----- File handling -------------------------------------------------------
  const handleFile = useCallback(async (file: File) => {
    setFilename(file.name);
    try {
      const buffer = await file.arrayBuffer();
      const parsed = parseWorkbookFirstSheet(buffer);
      if (!parsed.headers.length || !parsed.rows.length) {
        toast.error("No data found in file");
        setFilename(null);
        return;
      }
      setTable(parsed);
      setMapping(autoDetectColumns(parsed.headers, parsed.rows));
    } catch (e) {
      console.error(e);
      toast.error("Could not parse file");
      setFilename(null);
    }
  }, []);

  const handleFileInputChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      const f = e.target.files?.[0];
      if (f) void handleFile(f);
      // Reset the input so selecting the same file again still fires onChange
      e.target.value = "";
    },
    [handleFile]
  );

  // Full-dialog dropzone — the user can drop a file anywhere on the modal
  const { getRootProps, getInputProps, isDragActive } = useDropzone({
    noClick: true,
    noKeyboard: true,
    maxFiles: 1,
    accept: {
      "application/vnd.ms-excel": [".xls"],
      "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [".xlsx"],
      "text/csv": [".csv"],
    },
    onDropAccepted: ([file]) => {
      void handleFile(file);
    },
    onDropRejected: (rejections) => {
      const msg = rejections[0]?.errors[0]?.message || "Invalid file";
      toast.error(msg);
    },
  });

  // ----- Parse preview → resolve → review → onAdd pipeline -------------------
  const handleResolveAndAdd = useCallback(async () => {
    if (!table || !mapping) return;
    const rows = applyMapping(table, mapping);
    if (rows.length === 0) {
      toast.error("No valid rows to import");
      return;
    }
    setResolving(true);
    try {
      const result = await resolveIdentifiers(rows);
      // Fast path: everything matched cleanly → skip the review dialog
      if (result.ambiguous.length === 0 && result.unmatched.length === 0) {
        onAdd({ items: result.matched.map((m) => ({ pid: m.pid, qty: m.qty })) });
        handleClose(false);
        return;
      }
      setReviewResult(result);
      setReviewOpen(true);
    } catch (e) {
      console.error(e);
      toast.error("Failed to look up products");
    } finally {
      setResolving(false);
    }
  }, [table, mapping, onAdd, handleClose]);

  const handleReviewConfirm = useCallback(
    (resolved: Array<{ pid: number; qty: number }>) => {
      setReviewOpen(false);
      setReviewResult(null);
      if (resolved.length === 0) {
        toast.warning("No rows selected to add");
        return;
      }
      onAdd({ items: resolved });
      handleClose(false);
    },
    [onAdd, handleClose]
  );

  return (
    <>
      <Dialog open={open} onOpenChange={handleClose}>
        <DialogContent
          className="max-w-4xl max-h-[85vh] flex flex-col p-0 gap-0"
          {...getRootProps()}
        >
          {/* Hidden file input driven by the Upload button */}
          <input
            ref={fileInputRef}
            type="file"
            accept=".xls,.xlsx,.csv"
            className="hidden"
            onChange={handleFileInputChange}
          />
          {/* react-dropzone's input (for keyboard accessibility) */}
          <input {...getInputProps()} />

          {/* Drag-over overlay */}
          {isDragActive && (
            <div className="absolute inset-0 z-10 flex items-center justify-center bg-primary/10 backdrop-blur-sm rounded-lg border-2 border-dashed border-primary pointer-events-none">
              <div className="flex flex-col items-center gap-2 text-primary">
                <FileSpreadsheet className="h-10 w-10" />
                <p className="font-medium">Drop file to parse</p>
              </div>
            </div>
          )}

          <DialogHeader className="p-6 pb-3">
            <DialogTitle>Add products</DialogTitle>
          </DialogHeader>

          {/* Primary input row — always visible */}
          <div className="px-6 pb-3">
            <div className="flex gap-2">
              {/* Wrapper has px-0.5 so the input's focus ring isn't clipped
                  by the dialog's rounded clipping region on the left edge */}
              <div className="flex-1 px-0.5">
                <Input
                  value={query}
                  onChange={(e) => setQuery(e.target.value)}
                  onKeyDown={(e) => {
                    if (e.key === "Enter") {
                      e.preventDefault();
                      runSearch();
                    }
                  }}
                  onPaste={handlePaste}
                  placeholder="Search by name, UPC, SKU, supplier # — or paste multi-line data"
                  disabled={inParseMode}
                />
              </div>
              <Button onClick={runSearch} disabled={!query.trim() || inParseMode || isSearching}>
                {isSearching ? (
                  <Loader2 className="h-4 w-4 animate-spin" />
                ) : (
                  <SearchIcon className="h-4 w-4" />
                )}
              </Button>
              <Button
                variant="outline"
                onClick={() => fileInputRef.current?.click()}
                disabled={inParseMode}
              >
                <Upload className="h-4 w-4 mr-2" />
                Upload file
              </Button>
            </div>
          </div>

          {/* Content area: parse preview OR search results OR empty hint.
              `flex-1 overflow-auto min-h-0` lets this region shrink and
              scroll independently while the sticky footer below stays put.

              isSearching is checked BEFORE the searched branch so that the
              moment the user clicks Search (which flips both `searched` and
              `isSearching` to true in the same render) we show a loading
              state instead of flashing "No results" against the previous
              empty results array while the network request is in flight. */}
          <div className="flex-1 overflow-auto px-6 pb-3 min-h-0">
            {inParseMode && table && mapping ? (
              <ParsedPreview
                table={table}
                mapping={mapping}
                filename={filename}
                onMappingChange={setMapping}
                onReset={resetAll}
              />
            ) : isSearching ? (
              <div className="text-center text-sm text-muted-foreground py-12">
                <Loader2 className="h-6 w-6 mx-auto mb-2 animate-spin opacity-60" />
                Searching…
              </div>
            ) : searched ? (
              <SearchResultsTable
                results={searchResults}
                total={searchTotal}
                loadedPids={existingPids}
                onSelect={handleSelectSearchResult}
                onLoadAll={handleLoadAllSearchResults}
              />
            ) : (
              <div className="text-center text-sm text-muted-foreground py-8">
                <SearchIcon className="h-8 w-8 mx-auto mb-3 opacity-30" />
              </div>
            )}
          </div>

          {/* Sticky footer: only rendered in parse mode. Because it's a
              flex-none sibling of the scrollable content area (not inside
              it), it stays fixed at the bottom of the dialog regardless of
              how tall the preview table grows. Search mode doesn't need a
              footer since adding is click-per-row. */}
          {inParseMode && (
            <div className="flex-none border-t bg-background px-6 py-3 flex justify-end">
              <Button
                onClick={handleResolveAndAdd}
                disabled={parseImportable.count === 0 || resolving}
              >
                {resolving ? (
                  <>
                    <Loader2 className="h-4 w-4 mr-2 animate-spin" />
                    Looking up products…
                  </>
                ) : (
                  <>
                    Add {parseImportable.count}{" "}
                    {parseImportable.count === 1 ? "product" : "products"}
                  </>
                )}
              </Button>
            </div>
          )}
        </DialogContent>
      </Dialog>

      <ReviewMatchesDialog
        open={reviewOpen}
        onOpenChange={(o) => {
          setReviewOpen(o);
          if (!o) setReviewResult(null);
        }}
        result={reviewResult}
        onConfirm={handleReviewConfirm}
      />
    </>
  );
}

// ============================================================================
// SearchResultsTable — compact results list with click-to-add + load-all
// ============================================================================

function SearchResultsTable({
  results,
  total,
  loadedPids,
  onSelect,
  onLoadAll,
}: {
  results: QuickSearchResult[];
  total: number;
  loadedPids: Set<number>;
  onSelect: (result: QuickSearchResult) => void;
  onLoadAll: () => void;
}) {
  if (results.length === 0) {
    return (
      <div className="text-center text-sm text-muted-foreground py-12">
        No results. Try a different search term.
      </div>
    );
  }

  const isTruncated = total > SEARCH_LIMIT;
  const unloadedCount = results.filter((r) => !loadedPids.has(Number(r.pid))).length;

  return (
    <div className="space-y-3">
      <div className="flex items-center justify-between text-xs text-muted-foreground">
        <span>
          {isTruncated
            ? `Showing ${SEARCH_LIMIT} of ${total} results`
            : `${total} ${total === 1 ? "result" : "results"}`}
        </span>
        {unloadedCount > 1 && (
          <Button variant="outline" size="sm" onClick={onLoadAll}>
            Add all {unloadedCount}
          </Button>
        )}
      </div>

      <div className="border rounded-md overflow-hidden">
        <Table>
          <TableHeader>
            <TableRow>
              <TableHead>Product</TableHead>
              <TableHead>Item #</TableHead>
              <TableHead>UPC</TableHead>
              <TableHead>Brand</TableHead>
              <TableHead className="text-right">Price</TableHead>
            </TableRow>
          </TableHeader>
          <TableBody>
            {results.map((r) => {
              const isLoaded = loadedPids.has(Number(r.pid));
              return (
                <TableRow
                  key={r.pid}
                  className={cn(
                    isLoaded
                      ? "opacity-50 cursor-not-allowed"
                      : "cursor-pointer hover:bg-muted/50"
                  )}
                  onClick={() => !isLoaded && onSelect(r)}
                >
                  <TableCell className="max-w-[320px]">
                    <div className="flex items-center gap-2">
                      {isLoaded && (
                        <Check className="h-3 w-3 text-emerald-600 flex-shrink-0" />
                      )}
                      <span className="truncate">{r.title}</span>
                    </div>
                  </TableCell>
                  <TableCell className="font-mono text-xs">{r.sku}</TableCell>
                  <TableCell className="font-mono text-xs">{r.barcode}</TableCell>
                  <TableCell>{r.brand}</TableCell>
                  <TableCell className="text-right">
                    ${Number(r.regular_price).toFixed(2)}
                  </TableCell>
                </TableRow>
              );
            })}
          </TableBody>
        </Table>
      </div>
    </div>
  );
}

// ============================================================================
// ParsedPreview — column-role mapping + data preview for paste/upload
// ============================================================================

function ParsedPreview({
  table,
  mapping,
  filename,
  onMappingChange,
  onReset,
}: {
  table: ParsedTable;
  mapping: DetectedMapping;
  filename: string | null;
  onMappingChange: (m: DetectedMapping) => void;
  onReset: () => void;
}) {
  // Precompute the exact set of row indices that will be imported with the
  // current mapping. Used both for the aggregate count and for row-level
  // "will be dropped" visual feedback. When no qty column is assigned,
  // each row gets an implicit qty=1 — so the check only cares about the
  // identifier cell being non-empty.
  const importableIndices = useMemo(() => {
    const set = new Set<number>();
    if (mapping.identifierIdx < 0) return set;
    const hasQtyCol = mapping.qtyIdx >= 0;
    table.rows.forEach((row, idx) => {
      const identifier = (row[mapping.identifierIdx] || "").trim();
      if (!identifier) return;
      if (hasQtyCol) {
        const qtyStr = (row[mapping.qtyIdx] || "").trim();
        const cleaned = qtyStr.replace(/[^0-9.-]/g, "");
        const qty = Math.round(Number(cleaned));
        if (!Number.isFinite(qty) || qty <= 0) return;
      }
      set.add(idx);
    });
    return set;
  }, [table, mapping]);
  const importable = importableIndices.size;
  const hasIdentifier = mapping.identifierIdx >= 0;

  const handleRoleChange = useCallback(
    (colIdx: number, role: ColumnRole) => {
      const newRoles = [...mapping.roles];
      newRoles[colIdx] = role;
      // Enforce: at most one identifier and one qty column. Demote any
      // previous holder of the role to "ignore".
      if (role === "identifier" || role === "qty") {
        for (let i = 0; i < newRoles.length; i++) {
          if (i !== colIdx && newRoles[i] === role) newRoles[i] = "ignore";
        }
      }
      onMappingChange({
        identifierIdx: newRoles.findIndex((r) => r === "identifier"),
        qtyIdx: newRoles.findIndex((r) => r === "qty"),
        roles: newRoles,
      });
    },
    [mapping, onMappingChange]
  );

  const previewRows = table.rows.slice(0, 10);

  // Single source of truth for per-role styling. The header cell, the
  // data cells in that column, and the dropdown trigger all read from
  // these helpers so visual drift is impossible.
  const columnBg = (role: ColumnRole) => {
    if (role === "identifier") return "bg-emerald-50";
    if (role === "qty") return "bg-sky-50";
    return "";
  };
  const headerBorder = (role: ColumnRole) => {
    if (role === "identifier") return "border-b-2 border-emerald-500";
    if (role === "qty") return "border-b-2 border-sky-500";
    return "border-b border-border";
  };
  const triggerStyle = (role: ColumnRole) => {
    if (role === "identifier") return "border-emerald-500 text-emerald-900 font-medium";
    if (role === "qty") return "border-sky-500 text-sky-900 font-medium";
    return "text-muted-foreground";
  };

  return (
    <div className="space-y-3">
      <div className="flex items-center justify-between text-xs text-muted-foreground">
        <span className="flex items-center gap-2">
          {filename && (
            <>
              <FileSpreadsheet className="h-3 w-3" />
              <span className="font-medium">{filename}</span>
              <span>·</span>
            </>
          )}
          <span>
            <span className="font-semibold text-foreground">{importable}</span>{" "}
            of {table.rows.length} {table.rows.length === 1 ? "row" : "rows"}{" "}
            will be imported
          </span>
        </span>
        <Button variant="ghost" size="sm" onClick={onReset}>
          <X className="h-3 w-3 mr-1" />
          Start over
        </Button>
      </div>

      <div className="border rounded-md overflow-hidden">
        <div className="overflow-x-auto">
          <table className="w-full text-xs">
            <thead>
              <tr>
                {table.headers.map((h, i) => {
                  const role = mapping.roles[i] || "ignore";
                  return (
                    <th
                      key={i}
                      className={cn(
                        "text-left p-2 align-top min-w-[140px]",
                        columnBg(role),
                        headerBorder(role)
                      )}
                    >
                      <div
                        className="font-mono text-xs truncate mb-1.5"
                        title={h || `Column ${i + 1}`}
                      >
                        {h || `Column ${i + 1}`}
                      </div>
                      <Select
                        value={role}
                        onValueChange={(v) => handleRoleChange(i, v as ColumnRole)}
                      >
                        <SelectTrigger
                          className={cn("h-7 text-xs w-full", triggerStyle(role))}
                        >
                          <SelectValue />
                        </SelectTrigger>
                        <SelectContent>
                          <SelectItem value="identifier">Identifier</SelectItem>
                          <SelectItem value="qty">Quantity</SelectItem>
                          <SelectItem value="ignore">Ignore</SelectItem>
                        </SelectContent>
                      </Select>
                    </th>
                  );
                })}
              </tr>
            </thead>
            <tbody>
              {previewRows.map((r, i) => {
                const willImport = importableIndices.has(i);
                return (
                  <tr
                    key={i}
                    className={cn(
                      "border-t",
                      !willImport && "bg-destructive/5"
                    )}
                    title={
                      !willImport
                        ? "This row will be skipped (missing identifier or invalid quantity)"
                        : undefined
                    }
                  >
                    {r.map((cell, j) => {
                      const role = mapping.roles[j] || "ignore";
                      return (
                        <td
                          key={j}
                          // max-w + truncate + whitespace-nowrap keeps every
                          // row at a single-line height regardless of cell
                          // content length. The full value is available via
                          // the title tooltip for rows the user wants to
                          // inspect (e.g. long product descriptions).
                          className={cn(
                            "p-2 max-w-[240px] truncate whitespace-nowrap",
                            columnBg(role),
                            role === "identifier" && "font-mono",
                            role === "ignore" && "text-muted-foreground/70",
                            !willImport && "line-through decoration-destructive/50"
                          )}
                          title={cell || undefined}
                        >
                          {cell || <span className="text-muted-foreground">—</span>}
                        </td>
                      );
                    })}
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
        {table.rows.length > 10 && (
          <div className="text-xs text-muted-foreground p-2 border-t bg-muted/30">
            …and {table.rows.length - 10} more rows
          </div>
        )}
      </div>

      {!hasIdentifier && (
        <div className="flex items-start gap-2 text-xs text-amber-700">
          <AlertCircle className="h-3 w-3 mt-0.5" />
          <span>Pick an identifier column above to continue.</span>
        </div>
      )}
      {hasIdentifier && importable === 0 && (
        <div className="flex items-start gap-2 text-xs text-destructive">
          <AlertCircle className="h-3 w-3 mt-0.5" />
          <span>No rows have a valid identifier.</span>
        </div>
      )}
    </div>
  );
}
