// Pure clipboard-paste parsing for the VSM editor's Tabellen-View (A14,
// Wertstrom P4, KAR-878/KAR-986, execution-prompt §10.1 "pasting from
// Excel"). Mirrors vsm-excel.ts's own parse -> Record<string, unknown>[] ->
// typed-preview-row split, but for pasted TSV text (Excel/Sheets cell-range
// copy is tab-separated) instead of an uploaded .xlsx file, and scoped to
// the Tabellen-View's own Essential-field column set (Name/Zykluszeit/
// Anzahl Mitarbeiter — A13 §7.1) rather than vsm-excel.ts's 5-time-field
// set (Advanced fields, unrelated — that file/flow is untouched by P4).
//
// numericCellOrUndefined (vsm-excel.ts) is reused verbatim per the P4-Brief:
// same nullish-vs-falsy discipline — an empty cell and a real `0` must stay
// distinguishable all the way through (leer ≠ 0, Undefined-Doktrin).

import { numericCellOrUndefined } from './vsm-excel'

const KNOWN_HEADER_CELLS = ['Prozessschritt', 'Name', 'Zykluszeit', 'Anzahl Mitarbeiter', 'Mitarbeiter']
// Review-Fix F8 (adversarial review, PR #355): case-insensitive lookup,
// lowercased-cell -> CANONICAL-cased name. Detecting the header row
// case-insensitively but then keying every DATA row by the cell's own
// (still differently-cased) raw text would silently break every row's
// column lookup below (mapClipboardRowsToPreview looks up the exact
// capitalization, e.g. row['Zykluszeit'], never row['zykluszeit']) instead
// of just fixing the one phantom-row bug this targets — so the canonical
// name, not the raw cell text, becomes the header/row key.
const KNOWN_HEADER_CELLS_BY_LOWER = new Map(KNOWN_HEADER_CELLS.map((h) => [h.toLowerCase(), h]))
const DEFAULT_HEADERS = ['Name', 'Zykluszeit', 'Anzahl Mitarbeiter']

export interface VsmTablePasteRow {
  name: string
  cycleTimeSec: number | undefined
  numWorkers: number | undefined
  /** Review-Fix F1+F6 (adversarial review, PR #355): set to the RAW pasted
   * text ONLY when it looked like a German-formatted decimal but couldn't
   * be parsed unambiguously (both ',' and '.' present — thousands-separator
   * ambiguity, e.g. "1.234,56"). `undefined` for a genuinely empty/non-
   * numeric cell, same as before — this is NOT a replacement for that case,
   * it is a THIRD state alongside "empty" and "a real number" so the
   * preview can show "nicht erkannt: '…'" instead of the same '—' an
   * actually-empty cell renders (Ehrlichkeits-Doktrin: a value the user DID
   * provide, just not parseable, is not the same as no value at all). */
  cycleTimeUnrecognized: string | undefined
  numWorkersUnrecognized: string | undefined
}

/**
 * Splits pasted clipboard text into raw string-keyed row objects, one per
 * non-blank line, cells split on tab (the format Excel/Sheets puts on the
 * clipboard for a copied cell range).
 *
 * Header-aware: if the FIRST line contains at least one cell matching a
 * known column name (Review-Fix F8: case-insensitively — "ZYKLUSZEIT"/
 * "prozessschritt" match just as well as the canonical casing), that line
 * is consumed as the header and used for column names (order-tolerant,
 * each matched cell canonicalized to the exact casing
 * mapClipboardRowsToPreview looks up; an unmatched cell keeps its own text
 * verbatim, same "pass unknown columns through" behavior as before).
 * Otherwise every line is treated as DATA, mapped onto the fixed default
 * order (Name, Zykluszeit, Anzahl Mitarbeiter) — unlike the file-based
 * Excel import (which always assumes row 1 is a header), a paste has no
 * reliable "first row" convention, so silently dropping row 1 as a header
 * when it might be real data would lose the first process step outright.
 *
 * Blank lines (trailing newline, stray whitespace) are dropped before any
 * header/data decision is made — never surfaced as a bogus empty row.
 */
export function parseClipboardTableText(text: string): Record<string, unknown>[] {
  const lines = text.split(/\r\n|\r|\n/).filter((line) => line.trim() !== '')
  if (lines.length === 0) return []

  const firstCells = lines[0].split('\t').map((c) => c.trim())
  const looksLikeHeader = firstCells.some((cell) => KNOWN_HEADER_CELLS_BY_LOWER.has(cell.toLowerCase()))
  const headers = looksLikeHeader ? firstCells.map((cell) => KNOWN_HEADER_CELLS_BY_LOWER.get(cell.toLowerCase()) ?? cell) : DEFAULT_HEADERS
  const dataLines = looksLikeHeader ? lines.slice(1) : lines

  return dataLines.map((line) => {
    const cells = line.split('\t')
    const row: Record<string, unknown> = {}
    headers.forEach((header, i) => {
      row[header] = cells[i] ?? null
    })
    return row
  })
}

/** Review-Fix F1+F6 (adversarial review, PR #355): result of parsing ONE
 * pasted cell as a German-tolerant number — a 3rd state alongside "a real
 * number" and "genuinely empty", see VsmTablePasteRow.cycleTimeUnrecognized
 * above.
 *
 * Wertstrom Excel-Roundtrip (KAR-878/KAR-986): exported — the Excel-Vorlagen-
 * Import's parser (vsm-import-xlsx.ts) reuses this exact German-tolerant
 * cell parsing for TEXT-typed Excel cells (a user who typed "45,5" into a
 * text-formatted cell hits the same comma/ambiguity rules a pasted cell
 * does), per the Brief's explicit instruction to reuse the P4 canonicalization
 * rather than re-implement it. Behavior is unchanged — only the export
 * keywords were added. */
export interface ParsedPasteCell {
  value: number | undefined
  unrecognized: string | undefined
}

/**
 * Review-Fix F1+F6 (adversarial review, PR #355): PASTE-PATH ONLY —
 * numericCellOrUndefined (vsm-excel.ts) was written for native ExcelJS
 * number cells (already locale-independent JS numbers by the time they
 * reach it); reusing it VERBATIM for raw clipboard TEXT silently broke on
 * German-formatted decimals, since bare `Number(raw)` only understands '.'
 * as the decimal separator. `Number("45,5")` is `NaN` → the paste's own
 * numericCellOrUndefined call returned `undefined`, indistinguishable from
 * an actually-empty cell — a real, user-provided measurement silently
 * became "keine Angabe". The file-based Excel import (vsm-excel.ts) is
 * deliberately UNCHANGED — ExcelJS cells never carry this ambiguity.
 *
 * Rules: a string with EXACTLY one comma and NO dot is unambiguously a
 * German decimal ("45,5" -> 45.5). A string with BOTH a comma and a dot is
 * a thousands-separator ambiguity ("1.234,56" could be DE thousands+decimal,
 * but treating it as such by symmetry would also mis-parse a genuine EN
 * "1,234.56" the same naive way) — treated as UNPARSEABLE rather than
 * guessed, with the raw text preserved so the preview can say so instead of
 * silently showing the same '—' an empty cell renders. Anything else
 * (a plain dot-decimal, an integer, multiple stray commas, non-numeric
 * text, a genuinely empty cell) falls through to the existing
 * numericCellOrUndefined behavior, unchanged.
 */
export function parseGermanTolerantCell(raw: unknown): ParsedPasteCell {
  if (typeof raw !== 'string') return { value: numericCellOrUndefined(raw), unrecognized: undefined }
  const trimmed = raw.trim()
  if (trimmed === '') return { value: undefined, unrecognized: undefined }
  const commaCount = (trimmed.match(/,/g) ?? []).length
  const hasDot = trimmed.includes('.')
  if (commaCount === 1 && hasDot) {
    return { value: undefined, unrecognized: trimmed }
  }
  if (commaCount === 1 && !hasDot) {
    const n = Number(trimmed.replace(',', '.'))
    return { value: Number.isNaN(n) ? undefined : n, unrecognized: undefined }
  }
  return { value: numericCellOrUndefined(trimmed), unrecognized: undefined }
}

/**
 * Raw pasted rows -> the Tabellen-View's preview shape. Rows without a name
 * (after trim) are dropped — same "no half-named rows" convention
 * mapExcelRowsToPreview/the Quick-Start-Wizard use.
 */
export function mapClipboardRowsToPreview(rows: Record<string, unknown>[]): VsmTablePasteRow[] {
  return rows
    .map((row) => {
      const cycleTime = parseGermanTolerantCell(row['Zykluszeit'])
      const numWorkers = parseGermanTolerantCell(row['Anzahl Mitarbeiter'] ?? row['Mitarbeiter'])
      return {
        name: String(row['Prozessschritt'] ?? row['Name'] ?? '').trim(),
        cycleTimeSec: cycleTime.value,
        cycleTimeUnrecognized: cycleTime.unrecognized,
        numWorkers: numWorkers.value,
        numWorkersUnrecognized: numWorkers.unrecognized,
      }
    })
    .filter((r) => r.name.length > 0)
}
