// QAF-Korpus Batch-Runner (SupplierPulse Real-Korpus-Matrix).
//
// Liest jede Datei aus dem vertraulichen QAF-Korpus, fährt sie durch dieselbe
// Erkennungs-/Klassifikations-Pipeline wie kadi-v2's echter Ingest-Pfad
// (app/qaf-differences/actions.ts: loadExcelWorkbook -> detectG60 ->
// detectMultiQaf -> buildTemplateFingerprint / fingerprintMultiQafTemplate),
// OHNE jede Supabase-Persistenz — reines Read-Only-Research-Tooling.
//
// Pro Datei streng try/catch: EIN Crash darf den Batch nie stoppen (Vorgabe).
// Keine Zellwerte/Preise werden geloggt oder geschrieben — nur strukturelle
// Klassifikationsergebnisse. Läuft NUR lokal, Output geht NICHT ins Repo.
//
// Aufruf: npx tsx batch-runner.ts <input-dir> <output-json>

import { readdirSync, statSync, readFileSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import { createHash } from 'node:crypto'

import {
  loadExcelWorkbook,
  summarizeWorkbookSheets,
  parseSummarySheetFromWorkbook,
  detectG60,
  g60WorkbookFromExcelJs,
  parseG60WorkbookGuarded,
  DEFAULT_ENGINE_CONFIG,
  detectMultiQaf,
  multiQafDetectionInputFromExcelJs,
  buildTemplateFingerprint,
  summaryLocatedKeys,
  fingerprintMultiQafTemplateFromExcelJs,
  parseMaterialSheet,
  parseSbmSheet,
  parseRmrSheet,
  parseLogisticsSheet,
  parseLccnSheet,
  parseCo2eSheet,
  type QAFFieldKey,
  type MaterialFieldKey,
  type SbmFieldKey,
  type RmrFieldKey,
  type LccnFieldKey,
  type Co2eFieldKey,
} from '../../kadi-v2/lib/qaf-differences'
import { parseQAFTemplate } from '../../kadi-v2/lib/qaf-parser'

const PER_FILE_TIMEOUT_MS = 60_000
const SLOW_FILE_THRESHOLD_MS = 10_000

interface FileResult {
  file: string
  ext: string
  sizeBytes: number
  sha256_16: string
  durationMs: number
  timedOut: boolean
  loadOk: boolean
  loadErrorClass: string | null
  loadErrorMessage: string | null
  sheetCount: number | null
  hasInputSheet: boolean | null
  g60Detected: boolean | null
  g60FingerprintFamily: string | null
  g60FingerprintClassification: string | null
  multiQaf: {
    classification: string
    strongSignalIds: string[]
    weakSignalIds: string[]
  } | null
  multiQafError: string | null
  multiQafTemplateFamily: string | null
  multiQafTemplateClassification: string | null
  standardTemplateFingerprint: {
    classification: string
    matchedProfile: string | null
    facetCoverage: Record<string, number>
  } | null
  standardTemplateFingerprintError: string | null
  primaryClassification: string
  errors: string[]
}

function truncate(msg: string, n = 300): string {
  return msg.length > n ? msg.slice(0, n) + '…' : msg
}

function errMsg(e: unknown): string {
  if (e instanceof Error) return truncate(e.message)
  return truncate(String(e))
}

function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
  return new Promise((resolve, reject) => {
    const t = setTimeout(() => reject(new Error(`__TIMEOUT__ after ${ms}ms`)), ms)
    promise.then(
      (v) => {
        clearTimeout(t)
        resolve(v)
      },
      (e) => {
        clearTimeout(t)
        reject(e)
      },
    )
  })
}

async function analyzeOneFile(filePath: string, fileName: string): Promise<FileResult> {
  const start = Date.now()
  const ext = path.extname(fileName).toLowerCase()
  const buffer = readFileSync(filePath)
  const sha256_16 = createHash('sha256').update(buffer).digest('hex').slice(0, 16)

  const result: FileResult = {
    file: fileName,
    ext,
    sizeBytes: buffer.byteLength,
    sha256_16,
    durationMs: 0,
    timedOut: false,
    loadOk: false,
    loadErrorClass: null,
    loadErrorMessage: null,
    sheetCount: null,
    hasInputSheet: null,
    g60Detected: null,
    g60FingerprintFamily: null,
    g60FingerprintClassification: null,
    multiQaf: null,
    multiQafError: null,
    multiQafTemplateFamily: null,
    multiQafTemplateClassification: null,
    standardTemplateFingerprint: null,
    standardTemplateFingerprintError: null,
    primaryClassification: 'unclassified',
    errors: [],
  }

  // ── Load ───────────────────────────────────────────────────────────────
  let excelWb: Awaited<ReturnType<typeof loadExcelWorkbook>>
  try {
    excelWb = await loadExcelWorkbook(buffer)
    result.loadOk = true
    result.sheetCount = excelWb.worksheets.length
  } catch (e) {
    result.loadOk = false
    result.loadErrorMessage = errMsg(e)
    result.loadErrorClass = /\.xls$/i.test(fileName) ? 'legacy_xls_unsupported' : 'load_error_other'
    result.primaryClassification = result.loadErrorClass
    result.errors.push(`load: ${result.loadErrorMessage}`)
    result.durationMs = Date.now() - start
    return result
  }

  // ── G60 detection ────────────────────────────────────────────────────────
  let g60Detected = false
  try {
    const g60wb = g60WorkbookFromExcelJs(excelWb)
    result.hasInputSheet = g60wb.sheet('INPUT') !== null
    g60Detected = detectG60(g60wb)
    result.g60Detected = g60Detected

    if (g60Detected) {
      try {
        const parsedG60 = parseG60WorkbookGuarded(g60wb, DEFAULT_ENGINE_CONFIG.g60StructureGuard)
        const g60Fingerprint = buildTemplateFingerprint({
          sheets: summarizeWorkbookSheets(excelWb),
          summary: null,
          manufacturing: null,
          g60: {
            tabCount: Object.keys(parsedG60.tabs).length,
            inputStructureOk: parsedG60.inputStructure.ok,
            softMismatchTabCount: Object.keys(parsedG60.tabStructure).length,
            excludedTabCount: Object.keys(parsedG60.excludedTabs).length,
          },
        })
        result.g60FingerprintFamily = g60Fingerprint.matchedProfile
        result.g60FingerprintClassification = g60Fingerprint.classification
      } catch (e) {
        result.errors.push(`g60-fingerprint: ${errMsg(e)}`)
      }
    }
  } catch (e) {
    result.errors.push(`g60-detect: ${errMsg(e)}`)
  }

  // ── Multi-QAF detection ──────────────────────────────────────────────────
  try {
    const detection = detectMultiQaf(multiQafDetectionInputFromExcelJs(excelWb))
    result.multiQaf = {
      classification: detection.classification,
      strongSignalIds: detection.signals.filter((s) => s.strength === 'strong').map((s) => s.id),
      weakSignalIds: detection.signals.filter((s) => s.strength === 'weak').map((s) => s.id),
    }
    if (detection.classification !== 'standard_qaf') {
      try {
        const fp = fingerprintMultiQafTemplateFromExcelJs(excelWb, { detection })
        result.multiQafTemplateFamily = fp.family
        result.multiQafTemplateClassification = fp.classification
      } catch (e) {
        result.errors.push(`multi-qaf-fingerprint: ${errMsg(e)}`)
      }
    }
  } catch (e) {
    result.multiQafError = errMsg(e)
    result.errors.push(`multi-qaf-detect: ${result.multiQafError}`)
  }

  // ── Standard template fingerprint (skip for G60 — already fingerprinted above) ──
  if (!g60Detected) {
    try {
      const { summaryMetrics } = parseSummarySheetFromWorkbook(excelWb)

      let steps: Awaited<ReturnType<typeof parseQAFTemplate>> = []
      try {
        const file = new File([new Uint8Array(buffer)], fileName, {
          type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
        })
        steps = await parseQAFTemplate(file)
      } catch (e) {
        result.errors.push(`manufacturing-parse: ${errMsg(e)}`)
      }

      const safeParse = async <T>(label: string, fn: () => Promise<T>): Promise<T | null> => {
        try {
          return await fn()
        } catch (e) {
          result.errors.push(`${label}: ${errMsg(e)}`)
          return null
        }
      }

      const materialParsed = await safeParse('material-parse', () => parseMaterialSheet(excelWb))
      const sbmParsed = await safeParse('sbm-parse', () => parseSbmSheet(excelWb))
      const rmrParsed = await safeParse('rmr-parse', () => parseRmrSheet(excelWb))
      await safeParse('logistics-parse', () => parseLogisticsSheet(excelWb)) // parsed for parity, not used in fingerprint
      const lccnParsed = await safeParse('lccn-parse', () => parseLccnSheet(excelWb))
      const co2eParsed = await safeParse('co2e-parse', () => parseCo2eSheet(excelWb))

      const approxManufacturingHeaderRow = (rows: { sourceCells?: Record<string, unknown> }[]): number | null => {
        for (const r of rows) {
          for (const cell of Object.values(r.sourceCells ?? {})) {
            const addr = String(cell ?? '')
            const m = addr.match(/(\d+)/)
            if (m) return parseInt(m[1], 10)
          }
        }
        return null
      }

      const templateFingerprint = buildTemplateFingerprint({
        sheets: summarizeWorkbookSheets(excelWb),
        summary: summaryMetrics
          ? { template: summaryMetrics.template, locatedMetricKeys: summaryLocatedKeys(summaryMetrics.metrics) }
          : null,
        manufacturing: steps.length
          ? {
              matchedFieldKeys: [...new Set(steps.flatMap((s) => Object.keys(s.sourceCells ?? {})))] as QAFFieldKey[],
              approxHeaderRow: approxManufacturingHeaderRow(steps),
            }
          : null,
        material: materialParsed
          ? { matchedFieldKeys: [...new Set(materialParsed.flatMap((r) => Object.keys(r.sourceCells ?? {})))] as MaterialFieldKey[] }
          : null,
        sbm: sbmParsed
          ? { matchedFieldKeys: [...new Set(sbmParsed.flatMap((r) => Object.keys(r.sourceCells ?? {})))] as SbmFieldKey[] }
          : null,
        rmr: rmrParsed
          ? { matchedFieldKeys: [...new Set(rmrParsed.flatMap((r) => Object.keys(r.sourceCells ?? {})))] as RmrFieldKey[] }
          : null,
        lccn: lccnParsed ? { matchedFieldKeys: [...new Set(Object.keys(lccnParsed.sourceCells ?? {}))] as LccnFieldKey[] } : null,
        co2e: co2eParsed
          ? {
              matchedFieldKeys: [
                ...new Set([
                  ...Object.keys(co2eParsed.summary.sourceCells ?? {}),
                  ...co2eParsed.materialRows.flatMap((r) => Object.keys(r.sourceCells ?? {})),
                ]),
              ] as Co2eFieldKey[],
            }
          : null,
        g60: null,
      })

      const facetCoverage: Record<string, number> = {}
      const s = templateFingerprint.structure
      if (s.summary) facetCoverage.summary = s.summary.coverageRatio
      if (s.manufacturing) facetCoverage.manufacturing = s.manufacturing.coverageRatio
      if (s.material) facetCoverage.material = s.material.coverageRatio
      if (s.sbm) facetCoverage.sbm = s.sbm.coverageRatio
      if (s.rmr) facetCoverage.rmr = s.rmr.coverageRatio
      if (s.lccn) facetCoverage.lccn = s.lccn.coverageRatio
      if (s.co2e) facetCoverage.co2e = s.co2e.coverageRatio

      result.standardTemplateFingerprint = {
        classification: templateFingerprint.classification,
        matchedProfile: templateFingerprint.matchedProfile,
        facetCoverage,
      }
    } catch (e) {
      result.standardTemplateFingerprintError = errMsg(e)
      result.errors.push(`standard-fingerprint: ${result.standardTemplateFingerprintError}`)
    }
  }

  // ── Primary classification (for the matrix) ─────────────────────────────
  if (g60Detected) {
    result.primaryClassification = 'g60_detail'
  } else if (result.multiQaf?.classification === 'confirmed_multi_qaf') {
    result.primaryClassification = 'confirmed_multi_qaf'
  } else if (result.multiQaf?.classification === 'probable_multi_qaf') {
    result.primaryClassification = 'probable_multi_qaf'
  } else if (result.multiQaf?.classification === 'ambiguous') {
    result.primaryClassification = 'ambiguous'
  } else if (result.standardTemplateFingerprint) {
    result.primaryClassification = 'standard_summary'
  } else if (result.standardTemplateFingerprintError) {
    result.primaryClassification = 'standard_parse_error'
  } else {
    result.primaryClassification = 'unclassified'
  }

  result.durationMs = Date.now() - start
  return result
}

async function main() {
  const inputDir = process.argv[2]
  const outputJsonPath = process.argv[3]
  if (!inputDir || !outputJsonPath) {
    console.error('Usage: npx tsx batch-runner.ts <input-dir> <output-json>')
    process.exit(1)
  }

  const files = readdirSync(inputDir)
    .filter((name) => /\.(xlsx|xlsm|xls)$/i.test(name))
    .filter((name) => statSync(path.join(inputDir, name)).isFile())
    .sort()

  console.log(`Batch-Runner: ${files.length} Dateien in ${inputDir}`)
  const batchStart = Date.now()
  const results: FileResult[] = []

  for (let i = 0; i < files.length; i++) {
    const fileName = files[i]
    const filePath = path.join(inputDir, fileName)
    process.stdout.write(`[${i + 1}/${files.length}] ${fileName} ... `)
    try {
      const result = await withTimeout(analyzeOneFile(filePath, fileName), PER_FILE_TIMEOUT_MS)
      results.push(result)
      console.log(`${result.primaryClassification} (${result.durationMs}ms)${result.errors.length ? ' [' + result.errors.length + ' err]' : ''}`)
    } catch (e) {
      const msg = errMsg(e)
      const timedOut = msg.includes('__TIMEOUT__')
      results.push({
        file: fileName,
        ext: path.extname(fileName).toLowerCase(),
        sizeBytes: statSync(filePath).size,
        sha256_16: '',
        durationMs: PER_FILE_TIMEOUT_MS,
        timedOut,
        loadOk: false,
        loadErrorClass: timedOut ? 'timeout' : 'unexpected_crash',
        loadErrorMessage: msg,
        sheetCount: null,
        hasInputSheet: null,
        g60Detected: null,
        g60FingerprintFamily: null,
        g60FingerprintClassification: null,
        multiQaf: null,
        multiQafError: null,
        multiQafTemplateFamily: null,
        multiQafTemplateClassification: null,
        standardTemplateFingerprint: null,
        standardTemplateFingerprintError: null,
        primaryClassification: timedOut ? 'timeout' : 'unexpected_crash',
        errors: [msg],
      })
      console.log(`CRASH/TIMEOUT: ${msg}`)
    }
  }

  const batchDurationMs = Date.now() - batchStart
  console.log(`\nBatch fertig in ${(batchDurationMs / 1000).toFixed(1)}s für ${files.length} Dateien.`)

  writeFileSync(
    outputJsonPath,
    JSON.stringify(
      {
        generatedAt: new Date().toISOString(),
        inputDir,
        fileCount: files.length,
        batchDurationMs,
        slowFileThresholdMs: SLOW_FILE_THRESHOLD_MS,
        perFileTimeoutMs: PER_FILE_TIMEOUT_MS,
        results,
      },
      null,
      2,
    ),
  )
  console.log(`Rohdaten geschrieben nach ${outputJsonPath}`)
}

main().catch((e) => {
  console.error('FATAL', e)
  process.exit(1)
})
