// Loop 6 Etappe 3: die EINE Lade+Katalog-Komposition für V2-Seite und
// kanonischen Export.
//
// Vorher existierte dieser Block zweimal spaltenidentisch
// (app/qaf-differences/[id]/v2/page.tsx und exportQafCanonicalJson in
// actions.ts) — genau der „Lade-Glue ein zweites Mal", den der
// exportQafCanonicalJson-JSDoc als bekannte Duplizierung benannte
// (Etappe-1b-Stand). Parität zwischen Seite und
// Export wird hier STRUKTURELL erzwungen (eine Stelle lädt und
// komponiert), nicht durch nachträglichen Zahlenvergleich.
//
// Fehlersemantik: Die Selects laufen vollständig; jeder Quellen-Fehler
// wird BENANNT zurückgegeben und die Producer laufen mit leeren Daten
// weiter. Die Aufrufer entscheiden verschieden — die Seite zeigt den
// Ladefehler-Banner über leeren Sektionen (Silent-Failure-Härtung
// 10.08., #466), der Export bricht bei jedem Quellen-Fehler ab. Beide
// Semantiken bleiben hier unangetastet, weil diese Funktion keine davon
// erzwingt.
//
// BEWUSST NICHT hier: sha256/byte_size der Originale (Export-Pflicht,
// Admin-Storage-Download on-Klick — nie pro Seitenaufruf), die
// Seiten-Presentation-Producer (Buckets/Brücke/Partition/Coverage/
// Comparability — die kanonischen analysis_areas sind erst 6/24 befüllt,
// „Seite liest Areas" ist heute nicht möglich) und die
// seiten-spezifischen Selects (declared_comparison_type,
// qaf_plausibility_issue). KEIN 'use server' — dieses Modul ist
// Server-Code, aber keine Action.

import type { createClient } from '@/lib/supabase/server'
import { buildOverviewRun, type OverviewRun } from '@/lib/qaf-differences/internal/overview-run'
import { resolvePersistedEngineConfig } from '@/lib/qaf-differences/internal/rehydrate'
import { summaryMetricsFromRows } from '@/lib/qaf-differences/internal/rehydrate'
import {
  differencesFromManufacturing,
  tableNamesByDifferenceId,
} from '@/lib/qaf-differences/internal/differences-from-manufacturing'
import {
  differencesFromMaterial,
  materialTableNamesByDifferenceId,
} from '@/lib/qaf-differences/internal/differences-from-material'
import { buildCatalog } from '@/lib/qaf-differences/internal/all-differences'

type ServerSupabase = Awaited<ReturnType<typeof createClient>>

export interface CompositionComparisonRef {
  id: string
  project_id: string
  part_number: string | null
  baseline_file_id: string | null
  comparison_file_id: string | null
  /** Engine-Config-Treue (Review-Befund #502): das persistierte
   * qaf_comparison.engine_version-JSONB — der Lauf rechnet mit der
   * Formel-Engine-Einstellung des Vergleichs, nicht mit dem heutigen
   * Default (resolvePersistedEngineConfig, wie compare.ts). Pflichtfeld,
   * damit kein Aufrufer die Config still verliert; alle drei Konsumenten
   * selecten die Spalte bereits. */
  engine_version: unknown
}

export interface CompositionFileRow {
  id: string
  original_file_name: string
  template_type: string | null
  storage_key: string | null
  capability_matrix: unknown
}

export interface CompositionPartRow {
  part_name: string | null
  supplier: string | null
  variant: string | null
  quotation_date: string | null
}

/** Benannte Lade-Fehler je Quelle — `null` heißt fehlerfrei geladen. */
export interface CompositionErrors {
  files: string | null
  metrics: string | null
  stepMatches: string | null
  mfgDiffs: string | null
  mfgSteps: string | null
  matDiffs: string | null
  part: string | null
}

export interface ComparisonComposition {
  files: CompositionFileRow[]
  part: CompositionPartRow | null
  errors: CompositionErrors
  /** true, wenn mindestens eine Quelle einen Fehler meldete. */
  hasErrors: boolean
  run: OverviewRun
  mfg: ReturnType<typeof differencesFromManufacturing>
  mat: ReturnType<typeof differencesFromMaterial>
  mfgCatalog: ReturnType<typeof buildCatalog>
  matCatalog: ReturnType<typeof buildCatalog>
  mfgTableNames: ReturnType<typeof tableNamesByDifferenceId>
  matTableNames: ReturnType<typeof materialTableNamesByDifferenceId>
  /** differenceId → `summary.<metricKey>` (kanonische metric_id-Konvention). */
  metricIdByDifferenceId: ReadonlyMap<string, string>
}

/**
 * Lädt die persistierten Zeilen eines Vergleichs und komponiert daraus die
 * geteilten Producer-Ergebnisse (OverviewRun, MFG-/MAT-Kataloge). Das
 * `comparison`-Objekt laden die Aufrufer selbst — sie brauchen
 * unterschiedliche Zusatzspalten und unterschiedliche
 * Nicht-gefunden-Semantik (Seite: notFound, Export: benannter Abbruch).
 */
export async function loadComparisonComposition(
  supabase: ServerSupabase,
  comparison: CompositionComparisonRef,
): Promise<ComparisonComposition> {
  const fileIds = [comparison.baseline_file_id, comparison.comparison_file_id].filter(
    (v): v is string => v !== null,
  )

  const [filesRes, metricsRes, stepMatchesRes, mfgDiffsRes, mfgStepsRes, matDiffsRes, partRes] = await Promise.all([
    supabase
      .from('qaf_file')
      .select('id, original_file_name, template_type, storage_key, capability_matrix:g60_meta->capability_matrix')
      .in('id', fileIds),
    supabase
      .from('qaf_summary_metric')
      .select('file_id, metric_key, value, currency, source_cell, sheet, formula, value_state')
      .in('file_id', fileIds),
    supabase
      .from('qaf_step_match')
      .select('id, alt_step_id, neu_step_id, match_status')
      .eq('comparison_id', comparison.id),
    supabase
      .from('qaf_manufacturing_diff')
      .select('step_match_id, field, alt_value, neu_value, delta_absolute, status, source_alt, source_neu')
      .eq('comparison_id', comparison.id),
    fileIds.length
      ? supabase.from('qaf_manufacturing_step').select('id, process_name, position_number, row_index').in('file_id', fileIds)
      : Promise.resolve({
          data: [] as { id: string; process_name: string | null; position_number: string | null; row_index: number | null }[],
          error: null,
        }),
    supabase
      .from('qaf_material_diff')
      .select(
        'mapping_id, award_rows, current_rows, award_names, current_names, match_type, cost_award, cost_current, delta, effect, zero_cost',
      )
      .eq('comparison_id', comparison.id),
    comparison.part_number
      ? supabase
          .from('qaf_part')
          .select('part_name, supplier, variant, quotation_date')
          .eq('project_id', comparison.project_id)
          .eq('part_number', comparison.part_number)
          .maybeSingle()
      : Promise.resolve({ data: null, error: null }),
  ])

  const errors: CompositionErrors = {
    files: filesRes.error?.message ?? null,
    metrics: metricsRes.error?.message ?? null,
    stepMatches: stepMatchesRes.error?.message ?? null,
    mfgDiffs: mfgDiffsRes.error?.message ?? null,
    mfgSteps: mfgStepsRes.error?.message ?? null,
    matDiffs: matDiffsRes.error?.message ?? null,
    part: partRes.error?.message ?? null,
  }
  const hasErrors = Object.values(errors).some((e) => e !== null)

  const files = (filesRes.data ?? []) as CompositionFileRow[]
  const part = (partRes.data ?? null) as CompositionPartRow | null

  const preisblockVon = (fileId: string | null) => {
    if (fileId === null) return null
    const zeilen = (metricsRes.data ?? []).filter((m) => m.file_id === fileId)
    if (zeilen.length === 0) return null
    const datei = files.find((f) => f.id === fileId)
    return summaryMetricsFromRows(zeilen, datei?.template_type ?? null)
  }
  const nameVon = (fileId: string | null) =>
    files.find((f) => f.id === fileId)?.original_file_name ?? 'unbekannt'

  const run = buildOverviewRun({
    award: preisblockVon(comparison.baseline_file_id),
    current: preisblockVon(comparison.comparison_file_id),
    awardFileNameDe: nameVon(comparison.baseline_file_id),
    currentFileNameDe: nameVon(comparison.comparison_file_id),
    // Engine-Config-Treue (#502-Review): mit der PERSISTIERTEN Einstellung
    // des Vergleichs rechnen, nicht mit dem heutigen Default.
    formulaEngineEnabled: resolvePersistedEngineConfig(comparison.engine_version).formulaEngine.enabled,
  })

  const stepById = new Map((mfgStepsRes.data ?? []).map((s) => [s.id, s] as const))
  const matchById = new Map((stepMatchesRes.data ?? []).map((m) => [m.id, m] as const))
  const mfg = differencesFromManufacturing(
    (mfgDiffsRes.data ?? []).map((d) => {
      const match = d.step_match_id === null ? undefined : matchById.get(d.step_match_id)
      const step =
        match === undefined ? undefined : (stepById.get(match.neu_step_id ?? '') ?? stepById.get(match.alt_step_id ?? ''))
      return {
        diff: d,
        stationDe:
          step?.process_name ?? (step?.position_number != null ? `Position ${step.position_number}` : 'Unbekannte Station'),
        stationAnchorRow: step?.row_index ?? null,
        matchStatus: match?.match_status ?? null,
      }
    }),
  )
  const mat = differencesFromMaterial(matDiffsRes.data ?? [])
  const mfgCatalog = buildCatalog(mfg.differences)
  const matCatalog = buildCatalog(mat.differences)

  const metricIdByDifferenceId = new Map<string, string>()
  for (const [metricKey, ids] of run.summaryDifferenceIdsByMetricKey) {
    for (const id of ids) metricIdByDifferenceId.set(id, `summary.${metricKey}`)
  }

  return {
    files,
    part,
    errors,
    hasErrors,
    run,
    mfg,
    mat,
    mfgCatalog,
    matCatalog,
    mfgTableNames: tableNamesByDifferenceId(mfgCatalog.records, mfg.nameByTuple),
    matTableNames: materialTableNamesByDifferenceId(matCatalog.records, mat.nameByTuple),
    metricIdByDifferenceId,
  }
}
