'use server'
// Loop 10 (actions.ts zerlegen, Schnitt 4): der Summary-XLSX-Export.
// Verantwortung: den persistierten Vergleich als Arbeitsmappe rehydrieren
// (buildExportWorkbookFromRows, lib) und ausliefern — KEINE der drei
// deferred-Export-Varianten (G60/Multi-QAF/VvS bleiben in actions.ts,
// deferred_by_product_owner: nicht refactoren).
//
// KAR-840 PR1: regeneriert die 8-Blatt-Referenz-Mappe on demand aus den
// persistierten qaf_*-Zeilen — kein Re-Upload, kein Re-Parse; RLS-gescopte
// Reads (Original-Doku des Blocks, beim Schnitt mit umgezogen).
//
// Block wortgleich aus actions.ts verschoben (Muster #490/#491/#493):
// Importer direkt umgestellt, keine Re-Exports, uuidSchema lokal.

import { z } from 'zod'
import { createClient } from '@/lib/supabase/server'
import { logger } from '@/lib/logger'
import {
  buildExportWorkbookFromRows,
  comparisonModeRule,
  decodeBilingual,
} from '@/lib/qaf-differences'
import { sanitizeSheetFilePart } from './export-filename'
import type { QAFRow } from '@/lib/qaf-parser'
import type { PlausibilityIssue, PlausibilitySeverity, RehydratedFile } from '@/lib/qaf-differences'
import type { ActionResult, QafExportDownload } from '@/app/qaf-differences/actions'

// Identische Prüfung wie in actions.ts — bewusst lokal (siehe #490).
const uuidSchema = z
  .string()
  .regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, 'invalid uuid')

export async function exportQafComparisonXlsx(
  comparisonId: string,
  generatedAtLabel: string,
): Promise<ActionResult<QafExportDownload>> {
  if (!uuidSchema.safeParse(comparisonId).success) return { ok: false, error: 'invalid comparison id' }

  const supabase = await createClient()

  // RLS returns no row (no error) when the caller does not own the comparison.
  // engine_version is loaded so the export can reproduce the ruleEnforcement
  // mode the comparison was actually persisted under (KAR-889 reviewer
  // finding) — without it the export would use whatever RULE_ENGINE_CONFIG is
  // live at export time, which can silently diverge from the on-screen result
  // for old comparisons after a later flip of the global default.
  const { data: cmp, error: cmpErr } = await supabase
    .from('qaf_comparison')
    .select('id, project_id, part_number, baseline_file_id, comparison_file_id, comparison_mode, engine_version')
    .eq('id', comparisonId)
    .maybeSingle()
  if (cmpErr) return { ok: false, error: `comparison load failed: ${cmpErr.message}` }
  if (!cmp) return { ok: false, error: 'Vergleich nicht gefunden oder kein Zugriff.' }
  // KAR-943 adversarial-review F2/F4 fix (13.07.2026): reads from the central
  // COMPARISON_MODE_RULES registry instead of an ad-hoc per-mode if-chain —
  // this is the SAME "a fell-through-to-Summary-export EMPTY-workbook bug"
  // class KAR-942 adversarial-review F3 fixed for 'multi_qaf' below; using the
  // registry means 'multi_qaf_variant_vs_standard' (which has NO
  // qaf_manufacturing_step rows either, same "no G60/summary parse" source as
  // 'multi_qaf' — see bridge.ts/variant-vs-standard.ts module headers) is
  // covered automatically, and any FUTURE mode with exportSupported: false is
  // too, without a fourth copy of this guard ever needing to be written.
  const exportRule = comparisonModeRule(cmp.comparison_mode as string | null)
  if (!exportRule.exportSupported) {
    // G60 data lives in qaf_g60_tab, not qaf_manufacturing_step (KAR-913/P4.3:
    // use exportQafG60ComparisonXlsx instead); 'multi_qaf'/
    // 'multi_qaf_variant_vs_standard' have no qaf_manufacturing_step rows at
    // all (compare-flow.ts/variant-vs-standard.ts module headers, "no
    // G60/summary parse") — every case would otherwise silently
    // produce/download a plausible-looking but misleadingly EMPTY XLSX
    // instead of erroring.
    return { ok: false, error: exportRule.exportNotSupportedError ?? 'Export für diesen Vergleichs-Modus nicht verfügbar.' }
  }
  if (!cmp.baseline_file_id || !cmp.comparison_file_id) {
    return { ok: false, error: 'Vergleich hat keine zwei Dateien — Export nicht möglich.' }
  }

  const fileIds = [cmp.baseline_file_id, cmp.comparison_file_id] as string[]

  const [filesRes, partRes, stepsRes, plausRes] = await Promise.all([
    supabase.from('qaf_file').select('id, original_file_name, template_type').in('id', fileIds),
    cmp.part_number
      ? supabase
          .from('qaf_part')
          .select('part_number, part_name, variant, project_code, supplier, quotation_date, version')
          .eq('project_id', cmp.project_id)
          .eq('part_number', cmp.part_number as string)
          .maybeSingle()
      : Promise.resolve({ data: null, error: null }),
    supabase.from('qaf_manufacturing_step').select('file_id, raw_values').in('file_id', fileIds),
    // Persisted plausibility (computed per-file at analyze time) — used verbatim
    // so the export's Plausibilitäts-Sheet matches the on-screen result exactly.
    supabase
      .from('qaf_plausibility_issue')
      .select('issue_type, severity, step_label, field, explanation')
      .eq('comparison_id', comparisonId),
  ])

  if (filesRes.error) return { ok: false, error: `file load failed: ${filesRes.error.message}` }
  if (partRes.error) return { ok: false, error: `part load failed: ${partRes.error.message}` }
  if (stepsRes.error) return { ok: false, error: `step load failed: ${stepsRes.error.message}` }
  if (plausRes.error) return { ok: false, error: `plausibility load failed: ${plausRes.error.message}` }

  const fileById = new Map((filesRes.data ?? []).map((f) => [f.id, f]))
  const baselineFile = fileById.get(cmp.baseline_file_id)
  const comparisonFile = fileById.get(cmp.comparison_file_id)
  if (!baselineFile || !comparisonFile) return { ok: false, error: 'Dateizeilen fehlen — Export nicht möglich.' }

  const stepsByFile = new Map<string, QAFRow[]>()
  for (const row of stepsRes.data ?? []) {
    const list = stepsByFile.get(row.file_id) ?? []
    // raw_values is persisted as the full QAFRow shape (analyze-time parse).
    list.push(row.raw_values as QAFRow)
    stepsByFile.set(row.file_id, list)
  }

  const part = (partRes.data ?? null) as RehydratedFile['part']
  const baseline: RehydratedFile = { file: baselineFile, part, steps: stepsByFile.get(cmp.baseline_file_id) ?? [] }
  const comparison: RehydratedFile = { file: comparisonFile, part, steps: stepsByFile.get(cmp.comparison_file_id) ?? [] }

  // Map the persisted rows to the engine's PlausibilityIssue shape so the export
  // uses the analyze-time findings verbatim (fidelity — see rehydrate override).
  // KAR-906/P3.2: `r.explanation` may be a bilingual-encoded envelope
  // (persistence-mapper.ts's encodeBilingual) — decode it back into
  // explanation (DE) + explanationEn here, once, so both the "Erklärung" and
  // "Explanation (EN)" export columns show clean text instead of the raw
  // envelope. Legacy (pre-KAR-906) rows decode to { de: text, en: text }
  // (decodeBilingual's documented plain-text fallback), so explanationEn
  // ends up identical to explanation for those — acceptable, matches the
  // task's "Bestands-Issues ohne EN tolerant" contract.
  const plausibilityOverride: PlausibilityIssue[] = (plausRes.data ?? []).map((r) => {
    const { de, en } = decodeBilingual(r.explanation)
    return {
      type: r.issue_type,
      severity: r.severity as PlausibilitySeverity,
      field: r.field ?? undefined,
      step: r.step_label ?? undefined,
      explanation: de,
      explanationEn: en || undefined,
    }
  })

  let buffer: ArrayBuffer
  try {
    buffer = await buildExportWorkbookFromRows({
      generatedAtLabel,
      baseline,
      comparison,
      plausibilityOverride,
      engineVersion: cmp.engine_version,
    })
  } catch (e) {
    return { ok: false, error: `Export-Erzeugung fehlgeschlagen: ${e instanceof Error ? e.message : String(e)}` }
  }

  const base64 = Buffer.from(buffer).toString('base64')
  const filename = `QAF_Vergleich_${sanitizeSheetFilePart(cmp.part_number)}.xlsx`

  // Best-effort audit trail (never fail the download on a log-write hiccup), but
  // attribute the export to the caller and surface a genuine insert failure.
  const { data: claims } = await supabase.auth.getClaims()
  const exportedBy = (claims?.claims?.sub as string | undefined) ?? null
  const { error: auditErr } = await supabase
    .from('qaf_export')
    .insert({ project_id: cmp.project_id, comparison_id: cmp.id, format: 'xlsx', status: 'ready', exported_by: exportedBy })
  if (auditErr) logger.warn('qaf.export.audit_insert_failed', { comparisonId, error: auditErr.message })

  return { ok: true, data: { filename, base64 } }
}
