// Pipeline de génération PDF (spec §10) :
// 1. gel du contenu (§7.3) : textes + médias utilisés copiés dans data/frozen/<id>/
// 2. rendu de la route /print/<id> par Playwright/Chromium (mêmes composants Vue)
// 3. page.pdf() au format 16:9 (960×540 pt ≡ 1280×720 px)
// 4. post-traitement Ghostscript : recompression + métadonnées
import { copyFile, mkdir, rename, rm, writeFile } from 'node:fs/promises'
import { execFile } from 'node:child_process'
import { promisify } from 'node:util'
import { dirname, join } from 'node:path'
import { chromium } from 'playwright'
import type { FrozenSnapshot, Portfolio, Project } from '~~/types'
import { useContentSource } from './content-source'
import { savePortfolio } from './portfolio-store'

const execFileAsync = promisify(execFile)

export function sanitizeFileName(name: string): string {
  const base = name
    .normalize('NFD')
    .replace(/[̀-ͯ]/g, '')
    .replace(/[^\w\s-]/g, '')
    .trim()
    .replace(/\s+/g, '-')
    .toLowerCase()
  return (base || 'portfolio') + '.pdf'
}

/** Fige le contenu utilisé : projets référencés + copie des fichiers médias. */
export async function freezeContent(portfolio: Portfolio, fileName: string): Promise<FrozenSnapshot> {
  const config = useRuntimeConfig()
  const source = useContentSource()
  const frozenDir = join(config.dataDir, 'frozen', portfolio.id)

  // Re-création à neuf du snapshot
  await rm(frozenDir, { recursive: true, force: true })
  await mkdir(frozenDir, { recursive: true })

  const projectIds = new Set<string>()
  for (const page of portfolio.pages) {
    if (page.kind === 'project' || page.kind === 'project-grid') projectIds.add(page.projectId)
    if (page.kind === 'free') {
      for (const ref of page.media ?? []) projectIds.add(ref.slice(0, ref.lastIndexOf('/')))
    }
  }
  if (portfolio.cover.backgroundImage) {
    const ref = portfolio.cover.backgroundImage
    projectIds.add(ref.slice(0, ref.lastIndexOf('/')))
  }

  const projects: Record<string, Project> = {}
  for (const id of projectIds) {
    const project = await source.getProject(id)
    if (!project) continue
    projects[id] = project
    // Copie de toutes les variantes des médias du projet utilisés
    for (const asset of project.media) {
      for (const variant of asset.variants) {
        const abs = source.resolveMediaPath(id, variant.path)
        if (!abs) continue
        const dest = join(frozenDir, id, variant.path)
        await mkdir(dirname(dest), { recursive: true })
        try {
          await copyFile(abs, dest)
        } catch {
          // média manquant : le rendu retombera sur le contenu live
        }
      }
    }
  }

  // Instantané de la composition (mise en forme incluse) : la vue publique
  // rendra CETTE version figée, indépendante des éditions ultérieures.
  return {
    generatedAt: new Date().toISOString(),
    fileName,
    projects,
    name: portfolio.name,
    cover: structuredClone(portfolio.cover),
    pages: structuredClone(portfolio.pages),
  }
}

export interface GenerateResult {
  fileName: string
  downloadUrl: string
  sizeBytes: number
  pageCount: number
}

export async function generatePdf(portfolio: Portfolio, rawFileName?: string): Promise<GenerateResult> {
  const config = useRuntimeConfig()
  const fileName = sanitizeFileName(rawFileName || portfolio.frozenContent?.fileName?.replace(/\.pdf$/, '') || portfolio.name)

  // Gel du contenu : réutilisé tel quel s'il existe (régénération à l'identique),
  // sinon créé depuis le contenu live. « Réactualiser depuis Grav » le supprime.
  if (!portfolio.frozenContent) {
    portfolio.frozenContent = await freezeContent(portfolio, fileName)
  } else {
    portfolio.frozenContent.fileName = fileName
  }
  await savePortfolio(portfolio)

  const exportsDir = join(config.dataDir, 'exports')
  const workDir = join(config.dataDir, 'cache', 'pdf-work')
  await mkdir(exportsDir, { recursive: true })
  await mkdir(workDir, { recursive: true })
  const rawPdf = join(workDir, `${portfolio.id}-raw.pdf`)
  const finalPdf = join(workDir, `${portfolio.id}-final.pdf`)

  // ---- Rendu Chromium ----
  const browser = await chromium.launch()
  try {
    const page = await browser.newPage({
      viewportSize: { width: 1588, height: 1123 }, // A3 paysage en px CSS
      // Le jeton doit accompagner chaque sous-requête (médias, API), pas
      // seulement la navigation initiale.
      extraHTTPHeaders: { 'x-internal-token': config.internalToken },
    })
    const url = `http://localhost:3000/print/${portfolio.id}?internal-token=${encodeURIComponent(config.internalToken)}`
    const response = await page.goto(url, { waitUntil: 'networkidle', timeout: 120_000 })
    if (!response?.ok()) {
      throw new Error(`Rendu print en échec (HTTP ${response?.status()})`)
    }
    // Attendre fonts + images (les médias pleine résolution peuvent être longs)
    await page.evaluate(async () => {
      // Chargement forcé de TOUTES les faces déclarées (fonts.ready ne couvre
      // que celles déjà en cours de chargement)
      await Promise.all(Array.from((document as any).fonts).map((f: any) => f.load().catch(() => {})))
      await (document as any).fonts.ready
      const images = Array.from(document.querySelectorAll('img'))
      await Promise.all(images.map(img => img.complete
        ? Promise.resolve()
        : new Promise((resolve) => { img.onload = img.onerror = resolve })))
    })
    await page.emulateMedia({ media: 'print' })
    // Bug Chromium : le texte peint pendant la période « invisible » de
    // font-display: block reste absent du PDF même une fois les fontes
    // chargées. Invalider tous les styles + reflow force un repaint complet
    // avec les glyphes réels.
    await page.evaluate(async () => {
      for (const sheet of document.styleSheets) sheet.disabled = true
      void document.body.offsetHeight
      for (const sheet of document.styleSheets) sheet.disabled = false
      void document.body.offsetHeight
      await (document as any).fonts.ready
      await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)))
    })
    await page.pdf({
      path: rawPdf,
      printBackground: true,
      preferCSSPageSize: true,
      timeout: 120_000,
    })
  } finally {
    await browser.close()
  }

  // ---- Post-traitement Ghostscript : recompression + métadonnées (§10.4) ----
  const pdfmarks = join(workDir, `${portfolio.id}-marks.ps`)
  const psEscape = (s: string) => s.replace(/[\\()]/g, c => `\\${c}`)
  await writeFile(
    pdfmarks,
    `[ /Title (${psEscape(portfolio.cover.title || portfolio.name)})\n`
    + `  /Author (Figures Libres)\n`
    + `  /Creator (Générateur de portfolios Figures Libres)\n`
    + `  /DOCINFO pdfmark\n`,
    'latin1',
  )
  await execFileAsync('gs', [
    '-dBATCH', '-dNOPAUSE', '-dQUIET', '-dSAFER',
    '-sDEVICE=pdfwrite',
    '-dCompatibilityLevel=1.6',
    '-dAutoRotatePages=/None',
    '-sColorConversionStrategy=RGB',
    '-dDownsampleColorImages=true',
    '-dColorImageDownsampleType=/Bicubic',
    '-dColorImageResolution=200',
    '-dDownsampleGrayImages=true',
    '-dGrayImageResolution=200',
    '-dMonoImageResolution=300',
    `-sOutputFile=${finalPdf}`,
    rawPdf,
    pdfmarks,
  ])

  const exportPath = join(exportsDir, fileName)
  await rename(finalPdf, exportPath)
  await rm(rawPdf, { force: true })
  await rm(pdfmarks, { force: true })

  const { size } = await import('node:fs/promises').then(fs => fs.stat(exportPath))

  // On aligne updatedAt sur lastExport.at (touch=false) : le PDF est ainsi
  // marqué « à jour » tant que le portfolio n'est pas réédité (toute sauvegarde
  // ultérieure retouche updatedAt et le rend périmé).
  const exportedAt = new Date().toISOString()
  portfolio.lastExport = { fileName, at: exportedAt }
  portfolio.updatedAt = exportedAt
  await savePortfolio(portfolio, false)

  return {
    fileName,
    downloadUrl: `/api/exports/${encodeURIComponent(fileName)}`,
    sizeBytes: size,
    pageCount: portfolio.pages.length,
  }
}
