/**
 * Spreadsheet parsing helpers for the Create PO page.
 *
 * The parsing helpers (parsePasted, detectDelimiter, autoMapHeaderNames,
 * toIntOrUndefined) were originally adapted from a now-removed
 * forecasting/QuickOrderBuilder.tsx prototype, scoped down to the
 * 2-column (identifier, qty) PO use case.
 *
 * Two main entry points:
 *   - parsePastedTable(text)        → headers + rows from a TSV/CSV string
 *   - parseWorkbookFirstSheet(buf)  → headers + rows from an .xlsx/.xls/.csv file
 *
 * Plus a `autoDetectColumns(headers)` that picks the most likely identifier
 * and qty column for the auto-detect path. Both PasteTab and UploadTab
 * funnel through these helpers and end at the same RawIdentifierRow[]
 * shape consumed by `resolveIdentifiers()`.
 */

import * as XLSX from "xlsx";
import type { RawIdentifierRow } from "./types";

// --- Header synonym lists -----------------------------------------------------

const IDENTIFIER_HEADER_SYNONYMS = [
  "upc",
  "barcode",
  "bar code",
  "ean",
  "jan",
  "sku",
  "item",
  "item#",
  "item number",
  "item no",
  "item_no",
  "supplier #",
  "supplier no",
  "supplier_no",
  "supplier number",
  "notions #",
  "notions no",
  "notions_no",
  "notions number",
  "product code",
  "code",
  "id",
  "pid",
];

const QTY_HEADER_SYNONYMS = [
  "qty",
  "quantity",
  "order qty",
  "order quantity",
  "amount",
  "count",
  "units",
];

function normalizeHeader(h: string): string {
  return (h || "").trim().toLowerCase();
}

// --- Delimiter detection ------------------------------------------------------

function detectDelimiter(text: string): string {
  // Heuristic: prefer tab, then comma, then semicolon. Sample first 5 lines.
  const lines = text.split(/\r?\n/).slice(0, 5);
  const counts: Record<string, number> = { "\t": 0, ",": 0, ";": 0 };
  for (const line of lines) {
    counts["\t"] += (line.match(/\t/g) || []).length;
    counts[","] += (line.match(/,/g) || []).length;
    counts[";"] += (line.match(/;/g) || []).length;
  }
  return Object.entries(counts).sort((a, b) => b[1] - a[1])[0][0];
}

// --- Pasted text parser -------------------------------------------------------

export interface ParsedTable {
  headers: string[];
  rows: string[][];
  /** True when the first row appears to be a header (contains non-numeric strings). */
  hasHeader: boolean;
}

/**
 * Parse a pasted TSV/CSV string. Detects delimiter, splits cleanly, pads
 * short rows to header width, and trims whitespace per cell. All cells are
 * preserved as STRINGS — we never coerce to numbers here, because UPCs with
 * leading zeros must survive parsing intact.
 *
 * Header detection: if the first row's cells are mostly non-numeric strings
 * we treat it as a header and pull it out; otherwise we synthesize generic
 * "Column 1", "Column 2" headers and treat all rows as data. This makes the
 * paste flow forgiving: users can paste data with or without a header row.
 */
export function parsePastedTable(text: string): ParsedTable {
  if (!text || !text.trim()) return { headers: [], rows: [], hasHeader: false };

  const delimiter = detectDelimiter(text);
  const lines = text
    .split(/\r?\n/)
    .map((l) => l.trimEnd()) // trim trailing whitespace but keep tabs as separators
    .filter((l) => l.length > 0);

  if (lines.length === 0) return { headers: [], rows: [], hasHeader: false };

  const split = (line: string) => line.split(delimiter).map((s) => s.trim());

  const firstRow = split(lines[0]);
  const restRows = lines.slice(1).map(split);

  // Header heuristic: at least one cell in the first row is non-numeric and
  // not empty, AND there's more than one row of data (otherwise treat the
  // single row as data).
  const looksLikeHeader =
    firstRow.length >= 2 &&
    firstRow.some((c) => c && !/^[0-9.\-+ ]+$/.test(c));

  if (looksLikeHeader && restRows.length > 0) {
    const headers = firstRow.map((h) => h || "");
    const rows = restRows.map((r) => {
      while (r.length < headers.length) r.push("");
      return r;
    });
    return { headers, rows, hasHeader: true };
  }

  // No header — synthesize column names and treat every line as data
  const allRows = [firstRow, ...restRows];
  const maxCols = Math.max(...allRows.map((r) => r.length));
  const headers = Array.from({ length: maxCols }, (_, i) => `Column ${i + 1}`);
  const rows = allRows.map((r) => {
    while (r.length < maxCols) r.push("");
    return r;
  });
  return { headers, rows, hasHeader: false };
}

// --- Workbook (xlsx/xls/csv) parser -------------------------------------------

/**
 * Parse the first sheet of a workbook (read from an ArrayBuffer) into the
 * same {headers, rows} shape as parsePastedTable. All cell values are
 * coerced to STRINGS to keep UPCs intact (raw: false). Empty cells become
 * empty strings.
 */
export function parseWorkbookFirstSheet(buffer: ArrayBuffer): ParsedTable {
  // raw:false → use formatted text values (preserves leading zeros if cell
  // is text-formatted in the source spreadsheet); cellDates:true → date
  // cells become Date objects which we then stringify.
  const wb = XLSX.read(buffer, {
    type: "array",
    cellDates: true,
    raw: false,
    codepage: 65001,
    WTF: false,
  });
  const sheetName = wb.SheetNames[0];
  if (!sheetName) return { headers: [], rows: [], hasHeader: false };
  const sheet = wb.Sheets[sheetName];

  // header:1 returns rows as arrays; defval ensures missing cells become ""
  const aoa = XLSX.utils.sheet_to_json<unknown[]>(sheet, {
    header: 1,
    raw: false,
    defval: "",
  });

  if (!aoa.length) return { headers: [], rows: [], hasHeader: false };

  const stringify = (v: unknown): string => {
    if (v === null || v === undefined) return "";
    if (v instanceof Date) return v.toISOString().slice(0, 10);
    return String(v).trim();
  };

  const allRows = aoa.map((row) => row.map(stringify));
  const firstRow = allRows[0];
  const restRows = allRows.slice(1).filter((r) => r.some((c) => c.length > 0));

  const looksLikeHeader =
    firstRow.length >= 2 &&
    firstRow.some((c) => c && !/^[0-9.\-+ ]+$/.test(c));

  if (looksLikeHeader && restRows.length > 0) {
    const headers = firstRow.map((h) => h || "");
    const rows = restRows.map((r) => {
      while (r.length < headers.length) r.push("");
      return r;
    });
    return { headers, rows, hasHeader: true };
  }

  const all = restRows.length > 0 ? [firstRow, ...restRows] : [firstRow];
  const maxCols = Math.max(...all.map((r) => r.length));
  const headers = Array.from({ length: maxCols }, (_, i) => `Column ${i + 1}`);
  const rows = all.map((r) => {
    while (r.length < maxCols) r.push("");
    return r;
  });
  return { headers, rows, hasHeader: false };
}

// --- Auto-detect column roles -------------------------------------------------

export type ColumnRole = "identifier" | "qty" | "ignore";

export interface DetectedMapping {
  /** Index of the column to treat as the product identifier (UPC/SKU/etc.) */
  identifierIdx: number;
  /** Index of the column to treat as the quantity. */
  qtyIdx: number;
  /** Per-column role assignment, indexed by header position. */
  roles: ColumnRole[];
}

/**
 * Best-effort guess of which column is the identifier and which is the qty.
 *
 * Strategy:
 *   1. If headers contain known synonyms, use them.
 *   2. Otherwise inspect the first few data rows: a column where most cells
 *      look numeric AND values are small (<10000) is the qty; the other is
 *      the identifier.
 *   3. Fall back to: column 0 = identifier, column 1 = qty.
 */
export function autoDetectColumns(
  headers: string[],
  rows: string[][]
): DetectedMapping {
  const norm = headers.map(normalizeHeader);

  const findFirst = (syns: string[]): number => {
    for (const s of syns) {
      const idx = norm.findIndex((h) => h === s || (h && h.includes(s)));
      if (idx >= 0) return idx;
    }
    return -1;
  };

  let identifierIdx = findFirst(IDENTIFIER_HEADER_SYNONYMS);
  let qtyIdx = findFirst(QTY_HEADER_SYNONYMS);

  // If headers didn't tell us, look at the data
  if (identifierIdx < 0 || qtyIdx < 0) {
    const sample = rows.slice(0, 10);
    const numCols = headers.length;
    const numericLikeRatios: number[] = [];
    const avgValues: number[] = [];
    for (let c = 0; c < numCols; c++) {
      let numericCount = 0;
      let sum = 0;
      let parsedCount = 0;
      for (const row of sample) {
        const cell = (row[c] || "").trim();
        if (!cell) continue;
        // Treat as "numeric and small" if it parses to an int <= 10000
        if (/^\d+$/.test(cell)) {
          const n = Number(cell);
          if (Number.isFinite(n)) {
            numericCount += 1;
            if (n <= 10000) {
              sum += n;
              parsedCount += 1;
            }
          }
        }
      }
      numericLikeRatios.push(sample.length > 0 ? numericCount / sample.length : 0);
      avgValues.push(parsedCount > 0 ? sum / parsedCount : Infinity);
    }

    if (qtyIdx < 0) {
      // Best qty candidate: smallest avg value, prefer columns where >50% rows are numeric
      let bestIdx = -1;
      let bestAvg = Infinity;
      for (let c = 0; c < numCols; c++) {
        if (numericLikeRatios[c] >= 0.5 && avgValues[c] < bestAvg) {
          bestAvg = avgValues[c];
          bestIdx = c;
        }
      }
      qtyIdx = bestIdx;
    }

    if (identifierIdx < 0) {
      // Identifier = first column that isn't qty
      for (let c = 0; c < numCols; c++) {
        if (c !== qtyIdx) {
          identifierIdx = c;
          break;
        }
      }
    }
  }

  // Final fallback: identifier must point somewhere; qty is optional and
  // stays unassigned (-1) when there's no other column to use. applyMapping
  // defaults each row's qty to 1 when qtyIdx is -1 — so a single-column
  // paste of UPCs now imports as "one of each" instead of trying to parse
  // the UPC itself as a qty (which previously produced billion-unit rows).
  if (identifierIdx < 0) identifierIdx = 0;
  if (qtyIdx < 0) {
    qtyIdx = headers.length > 1 && identifierIdx !== 1 ? 1 : -1;
  }

  const roles: ColumnRole[] = headers.map((_, i) => {
    if (i === identifierIdx) return "identifier";
    if (i === qtyIdx) return "qty";
    return "ignore";
  });

  return { identifierIdx, qtyIdx, roles };
}

// --- Convert mapped rows → RawIdentifierRow[] --------------------------------

/**
 * Apply a column mapping to the parsed table to produce raw identifier
 * rows. Rows with an empty identifier are dropped.
 *
 * When `qtyIdx` is -1 (no qty column assigned), each row gets a default
 * qty of 1 — this makes single-column pastes like "paste a list of UPCs,
 * one per line" work without forcing users to add a qty column. When
 * `qtyIdx` is set but a row has an invalid/non-positive qty value, that
 * row is dropped.
 */
export function applyMapping(
  table: ParsedTable,
  mapping: DetectedMapping
): RawIdentifierRow[] {
  const out: RawIdentifierRow[] = [];
  const hasQtyColumn = mapping.qtyIdx >= 0;
  for (const row of table.rows) {
    const identifier = (row[mapping.identifierIdx] || "").trim();
    if (!identifier) continue;

    let qty = 1;
    if (hasQtyColumn) {
      const qtyStr = (row[mapping.qtyIdx] || "").trim();
      // Strip non-numeric chars from qty (commas, spaces, decimals)
      const cleaned = qtyStr.replace(/[^0-9.-]/g, "");
      const parsed = Math.round(Number(cleaned));
      if (!Number.isFinite(parsed) || parsed <= 0) continue;
      qty = parsed;
    }

    out.push({ identifier, qty });
  }
  return out;
}
