// Service de fichiers médias, partagé par la route authentifiée
// (/api/media/…) et la route publique (/api/public/<id>/media/…).
// - `width` > 0 : image redimensionnée via sharp (cache disque). La source est
//   choisie parmi les variantes @1x…@4x : la plus petite qui couvre la largeur
//   demandée (spec §10.1 : on descend, on n'agrandit pas).
// - `wantPoster` : première frame d'une vidéo (ffmpeg), servie comme image.
// - `roots` : racines de résolution testées dans l'ordre (snapshot figé d'abord,
//   contenu live ensuite ; la route publique ne passe que le snapshot figé).
import { createHash } from 'node:crypto'
import { createReadStream } from 'node:fs'
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises'
import { execFile } from 'node:child_process'
import { promisify } from 'node:util'
import { extname, join, normalize } from 'node:path'
import type { H3Event } from 'h3'
import sharp from 'sharp'

const execFileAsync = promisify(execFile)

const MIME: Record<string, string> = {
  '.png': 'image/png',
  '.jpg': 'image/jpeg',
  '.jpeg': 'image/jpeg',
  '.webp': 'image/webp',
  '.gif': 'image/gif',
  '.mp4': 'video/mp4',
  '.webm': 'video/webm',
}

// Cache mémoire des largeurs d'images (clé : chemin + mtime)
const widthCache = new Map<string, number>()

async function imageWidth(abs: string, mtimeMs: number): Promise<number> {
  const key = `${abs}:${mtimeMs}`
  const cached = widthCache.get(key)
  if (cached) return cached
  const meta = await sharp(abs).metadata()
  const w = meta.width ?? 0
  widthCache.set(key, w)
  return w
}

interface FoundFile {
  abs: string
  size: number
  mtimeMs: number
}

async function findExisting(paths: string[]): Promise<FoundFile[]> {
  const found: FoundFile[] = []
  for (const abs of paths) {
    try {
      const s = await stat(abs)
      found.push({ abs, size: s.size, mtimeMs: s.mtimeMs })
    } catch {
      // absent
    }
  }
  return found
}

/** Découpe le chemin d'URL `projectId/.../filename` en (projectId, filename). */
export function splitMediaPath(rawPath: string): { projectId: string; filename: string } {
  const segments = rawPath.split('/').map(decodeURIComponent).filter(Boolean)
  if (segments.length < 2 || segments.some(s => s === '..')) {
    throw createError({ statusCode: 400, statusMessage: 'Chemin invalide' })
  }
  return {
    filename: segments[segments.length - 1],
    projectId: segments.slice(0, -1).join('/'),
  }
}

export interface ServeMediaOptions {
  roots: string[] // racines de résolution, dans l'ordre de priorité
  projectId: string
  filename: string
  width: number
  wantPoster: boolean
  dataDir: string
}

/** Résout, redimensionne/extrait puis renvoie un média. Logique commune. */
export async function serveMedia(event: H3Event, opts: ServeMediaOptions) {
  const { roots, projectId, filename, width, wantPoster, dataDir } = opts

  // Le nom canonique peut ne pas exister sur disque : variantes @Nx en repli
  const ext = extname(filename).toLowerCase()
  const variantNames = [
    filename,
    ...['1', '2', '3', '4', '5', '6'].map(n => `${filename.slice(0, -ext.length)}@${n}x${ext}`),
  ]

  let candidates: FoundFile[] = []
  for (const root of roots) {
    const paths = variantNames.map((name) => {
      const abs = normalize(join(root, projectId, name))
      if (!abs.startsWith(normalize(root) + '/')) {
        throw createError({ statusCode: 400, statusMessage: 'Chemin invalide' })
      }
      return abs
    })
    candidates = await findExisting(paths)
    if (candidates.length) break
  }
  if (!candidates.length) {
    throw createError({ statusCode: 404, statusMessage: 'Média introuvable' })
  }

  const primary = candidates[0]
  setHeader(event, 'Cache-Control', 'public, max-age=3600')

  // ---- Poster de vidéo ----
  if (wantPoster && MIME[ext]?.startsWith('video/')) {
    const cacheDir = join(dataDir, 'cache', 'posters')
    const key = createHash('sha1').update(`${primary.abs}:${primary.mtimeMs}`).digest('hex')
    const posterPath = join(cacheDir, `${key}.jpg`)

    try {
      await stat(posterPath)
    } catch {
      await mkdir(cacheDir, { recursive: true })
      try {
        await execFileAsync('ffmpeg', [
          '-y', '-ss', '1', '-i', primary.abs, '-frames:v', '1', '-q:v', '3', posterPath,
        ])
      } catch {
        // vidéos plus courtes qu'une seconde : première frame
        await execFileAsync('ffmpeg', ['-y', '-i', primary.abs, '-frames:v', '1', '-q:v', '3', posterPath])
      }
    }

    setHeader(event, 'Content-Type', 'image/jpeg')
    if (width > 0) {
      return await sharp(posterPath)
        .resize({ width: Math.min(width, 3200), withoutEnlargement: true })
        .jpeg({ quality: 82 })
        .toBuffer()
    }
    return sendStream(event, createReadStream(posterPath))
  }

  // ---- Image redimensionnée ----
  if (width > 0 && MIME[ext]?.startsWith('image/') && ext !== '.gif') {
    const target = Math.min(width, 3200)

    // Choix de la source : la plus petite variante couvrant la cible
    let best = candidates[0]
    let bestWidth = await imageWidth(best.abs, best.mtimeMs)
    for (const c of candidates.slice(1)) {
      const w = await imageWidth(c.abs, c.mtimeMs)
      const covers = w >= target
      const bestCovers = bestWidth >= target
      if ((covers && (!bestCovers || w < bestWidth)) || (!covers && !bestCovers && w > bestWidth)) {
        best = c
        bestWidth = w
      }
    }

    const cacheDir = join(dataDir, 'cache', 'thumbs')
    const key = createHash('sha1').update(`${best.abs}:${target}:${best.mtimeMs}`).digest('hex')
    const cachePath = join(cacheDir, `${key}.webp`)

    try {
      const cached = await readFile(cachePath)
      setHeader(event, 'Content-Type', 'image/webp')
      return cached
    } catch {
      // pas en cache : on génère
    }

    await mkdir(cacheDir, { recursive: true })
    const resized = await sharp(best.abs)
      .resize({ width: target, withoutEnlargement: true })
      .webp({ quality: 80 })
      .toBuffer()
    await writeFile(cachePath, resized)
    setHeader(event, 'Content-Type', 'image/webp')
    return resized
  }

  // ---- Fichier original ----
  setHeader(event, 'Content-Type', MIME[ext] ?? 'application/octet-stream')
  setHeader(event, 'Content-Length', primary.size)
  return sendStream(event, createReadStream(primary.abs))
}
