// Export helpers for the QAF comparison sections (KAR-840 export layer).
// Client-side only: a section (charts incl. their HTML legends, tables,
// captions) is snapshotted to PNG via the SVG-foreignObject trick with the
// page styles inlined (V11 tableToPng — :root rules resolve the CSS-var
// chart colors), plus the versioned JSON view-model export. No external
// libs; data:/blob: URIs are CSP-allowed. Reuses lib/notes/pdf downloadBlob.

import { downloadBlob } from '@/lib/notes/pdf'
import {
  QAF_COMPARE_ENGINE,
  QAF_CONVENTIONS,
  deriveComparisonType,
  sectionStateOf,
  type ComparisonIdentity,
  type ComparisonTypeVerdict,
  type QafConventions,
  type SectionState,
  buildValidation,
  contentHash,
  type ValidationInput,
  type ValidationResult,
  exportProfileStamp,
  type ExportProfileStamp,
} from '@/lib/qaf-differences'

// ── Pure helpers (unit-tested) ────────────────────────────────────────────────

export type ExportKind = 'png' | 'json'

function sanitize(part: string | null): string {
  return (part ?? '').replace(/[^0-9A-Za-z_-]+/g, '_').replace(/^_+|_+$/g, '') || 'QAF'
}

/** V11-style file names: SupplierPulse_QAF_[section_]part_YYYY-MM-DD.ext */
export function exportFilename(kind: ExportKind, partNumber: string | null, now: Date, section?: string): string {
  const stamp = now.toISOString().slice(0, 10)
  const part = sanitize(partNumber)
  if (kind === 'json') return `SupplierPulse_QAF_Compare_Result_${part}_${stamp}.json`
  return `SupplierPulse_QAF_${section ? `${section}_` : ''}${part}_${stamp}.png`
}

export interface ViewModelExportInput {
  partNumber: string | null
  comparisonId: string
  currency: string | null
  generatedAtIso: string
  /** The computed section view-models, exactly as rendered. */
  sections: Record<string, unknown>
  /**
   * Identitätsmerkmale beider Seiten für die Einordnung des Vergleichs (R-23).
   * Fehlen sie, bleibt der Typ ausdrücklich unbestimmt statt geraten.
   */
  identity?: ComparisonIdentity
  /** Erwartete Zeilenzahl der Kennzahlen-Tabelle laut Registry (R-22). */
  expectedMetricsRowCount?: number
}

export interface ViewModelExport {
  format: 'supplierpulse-qaf-compare'
  version: 2
  export_profile: ExportProfileStamp
  /** Womit erzeugt — Voraussetzung dafür, zwei Läufe überhaupt vergleichen zu können. */
  engine: { name: string; version: string }
  meta: {
    partNumber: string | null
    comparisonId: string
    currency: string | null
    generatedAt: string
    /** Zeitvergleich, Standort-, Lieferantenvergleich (R-23, Befund F-20). */
    comparison_type: ComparisonTypeVerdict
  }
  /** Unter welchen Regeln die Zahlen zu lesen sind (Kap. 6.3, R-17/R-18). */
  conventions: QafConventions
  sections: Record<string, unknown>
  /**
   * Zustand je Sektion (R-24, Befund F-22): „nichts gefunden" ist eine Aussage,
   * eine wortlos leere Sektion ist keine.
   */
  section_state: Record<string, SectionState>
  /** Benannte Prüfungen mit Messwert und Grenze (R-14, Befund F-05). */
  validation: ValidationResult & { determinism: { content_hash: string | null } }
}

/**
 * Versioniertes Ergebnis-JSON im V2-Envelope.
 *
 * V1 war ein Deckel über einer Sektionsliste: Delta-Richtung, Einheiten,
 * Toleranzen, die Art des Vergleichs und der Zustand leerer Sektionen standen
 * nirgends im Dokument. Wer es las, musste all das mitbringen — und wer es
 * falsch mitbrachte, las dieselben Zahlen falsch.
 *
 * Die Sektionsinhalte bleiben unverändert; ergänzt wird, was zu ihrer richtigen
 * Lesart nötig ist.
 */
export function buildViewModelExport(input: ViewModelExportInput): ViewModelExport {
  const sectionState: Record<string, SectionState> = {}
  for (const [name, content] of Object.entries(input.sections)) {
    sectionState[name] = sectionStateOf(content)
  }

  const validation = buildValidation({
    bridge: (input.sections.bridge as ValidationInput['bridge']) ?? null,
    movers: (input.sections.movers as ValidationInput['movers']) ?? null,
    metricsRows: (input.sections.metricsTable as ValidationInput['metricsRows']) ?? null,
    expectedMetricsRowCount: input.expectedMetricsRowCount,
    // Der Hash wird nach dem Aufbau ergänzt (er hasht dieses Dokument), deshalb
    // trägt die Prüfung hier die Absicht und `withContentHash` das Ergebnis.
    contentHashPresent: false,
    tolerances: {
      bridgeEur: QAF_CONVENTIONS.tolerances.bridgeEur,
      reconciliationEur: QAF_CONVENTIONS.tolerances.reconciliationEur,
    },
  })

  return {
    format: 'supplierpulse-qaf-compare',
    version: 2,
    // Loop 12: der profilübergreifende Stempel — `version: 2` bleibt für
    // Bestands-Leser stehen, `export_profile` ist die eine gemeinsame Form.
    export_profile: exportProfileStamp('viewmodel_json'),
    engine: { name: QAF_COMPARE_ENGINE.name, version: QAF_COMPARE_ENGINE.version },
    meta: {
      partNumber: input.partNumber,
      comparisonId: input.comparisonId,
      currency: input.currency,
      generatedAt: input.generatedAtIso,
      comparison_type: deriveComparisonType(
        input.identity ?? {
          partNumberAward: input.partNumber,
          partNumberCurrent: input.partNumber,
          supplierAward: null,
          supplierCurrent: null,
          siteAward: null,
          siteCurrent: null,
        },
      ),
    },
    conventions: QAF_CONVENTIONS,
    sections: input.sections,
    section_state: sectionState,
    validation: { ...validation, determinism: { content_hash: null } },
  }
}

/**
 * Ergänzt den Inhalts-Hash und zieht die Determinismus-Prüfung nach (R-19).
 *
 * Getrennt vom Aufbau, weil der Hash über das fertige Dokument läuft und die
 * Web-Crypto-API asynchron ist. Erst danach ist `determinism_hash_present`
 * belegbar — vorher wäre es eine Behauptung.
 */
export async function withContentHash(vm: ViewModelExport): Promise<ViewModelExport> {
  const hash = await contentHash(vm)
  return {
    ...vm,
    validation: {
      ...vm.validation,
      checks: vm.validation.checks.map((c) =>
        c.checkId === 'determinism_hash_present' ? { ...c, status: 'pass' as const } : c,
      ),
      counts: {
        ...vm.validation.counts,
        pass: vm.validation.counts.pass + 1,
        fail: vm.validation.counts.fail - 1,
      },
      passed: vm.validation.counts.fail - 1 === 0,
      determinism: { content_hash: hash },
    },
  }
}

/** Escape text for embedding as an XML text node (style content in the SVG). */
export function escapeXmlText(s: string): string {
  return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/]]>/g, '')
}

// ── Browser-only helpers ──────────────────────────────────────────────────────

export function downloadJson(data: unknown, filename: string): void {
  downloadBlob(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }), filename)
}

/** Same-origin stylesheet text — inlined into the foreignObject snapshot. */
function collectCssText(): string {
  let css = ''
  for (const sheet of Array.from(document.styleSheets)) {
    try {
      for (const rule of Array.from(sheet.cssRules)) css += rule.cssText + '\n'
    } catch {
      // cross-origin sheet — skip (none expected under our CSP)
    }
  }
  return css
}

function loadImage(src: string): Promise<HTMLImageElement> {
  return new Promise((resolve, reject) => {
    const img = new Image()
    img.onload = () => resolve(img)
    img.onerror = () => reject(new Error('SVG-Snapshot konnte nicht geladen werden'))
    img.src = src
  })
}

/**
 * Snapshot a DOM section to a PNG blob via SVG foreignObject with the page
 * styles inlined (V11 tableToPng, extended to whole sections): charts keep
 * their HTML legends, CSS-var colors resolve via the inlined :root rules.
 * Interactive controls are flattened to text; [data-export-exclude] nodes
 * (e.g. the export button itself) are removed from the snapshot.
 */
export async function elementToPngBlob(el: HTMLElement, scale = 2): Promise<Blob> {
  const width = Math.min(1600, el.scrollWidth + 24)
  const height = el.scrollHeight + 24
  const clone = el.cloneNode(true) as HTMLElement
  clone.querySelectorAll('[data-export-exclude]').forEach((node) => node.remove())
  clone.querySelectorAll('input, select, button').forEach((node) => {
    const span = document.createElement('span')
    if (node.tagName === 'SELECT') {
      span.textContent = (node as HTMLSelectElement).selectedOptions[0]?.textContent ?? ''
    } else if (node.tagName === 'INPUT') {
      const input = node as HTMLInputElement
      span.textContent = input.type === 'checkbox' ? (input.checked ? '☑' : '☐') : input.value
    } else {
      span.textContent = node.textContent ?? ''
    }
    node.replaceWith(span)
  })
  const xml = new XMLSerializer().serializeToString(clone)
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"><foreignObject width="100%" height="100%"><div xmlns="http://www.w3.org/1999/xhtml" style="background:#fff;padding:12px"><style>${escapeXmlText(collectCssText())}</style>${xml}</div></foreignObject></svg>`
  const img = await loadImage('data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg))
  const canvas = document.createElement('canvas')
  canvas.width = width * scale
  canvas.height = height * scale
  const ctx = canvas.getContext('2d')!
  ctx.scale(scale, scale)
  ctx.fillStyle = '#ffffff'
  ctx.fillRect(0, 0, width, height)
  ctx.drawImage(img, 0, 0, width, height)
  return new Promise((resolve, reject) =>
    canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('PNG-Erzeugung fehlgeschlagen'))), 'image/png'),
  )
}

/** Export one section as a single PNG (charts + legends + tables together). */
export async function exportSectionPng(el: HTMLElement, filename: string): Promise<void> {
  downloadBlob(await elementToPngBlob(el), filename)
}
