// Parseur du contenu Grav (lecture seule) — spec §7.1
// Lit user/pages/02.projets/<categorie>/<projet>/reader.md + médias.
import { readdir, readFile, stat } from 'node:fs/promises'
import { join } from 'node:path'
import { parse as parseYaml } from 'yaml'
import { marked } from 'marked'
import sharp from 'sharp'
import type { MediaAsset, MediaVariant, Project } from '~~/types'

const IMAGE_EXT = /\.(png|jpe?g|webp|gif)$/i
const VIDEO_EXT = /\.(mp4|webm)$/i
const VARIANT_RE = /^(.*)@(\d+)x(\.[^.]+)$/i

/** "01.culturelle" -> "culturelle" */
function stripOrderPrefix(dirname: string): string {
  return dirname.replace(/^\d+\./, '')
}

/** Date Grav "09-12-2024 22:19" (DD-MM-YYYY HH:mm) -> ISO */
function parseGravDate(raw: unknown): string | undefined {
  if (!raw) return undefined
  const s = String(raw).trim()
  const m = s.match(/^(\d{2})-(\d{2})-(\d{4})(?:\s+(\d{2}):(\d{2}))?$/)
  if (m) {
    const [, dd, mm, yyyy, hh = '00', min = '00'] = m
    return `${yyyy}-${mm}-${dd}T${hh}:${min}:00`
  }
  const d = new Date(s)
  return Number.isNaN(d.getTime()) ? undefined : d.toISOString()
}

function toArray(v: unknown): string[] {
  if (Array.isArray(v)) return v.map(String).filter(Boolean)
  if (typeof v === 'string' && v.trim()) return [v.trim()]
  return []
}

/** Détecte l'URL externe du projet (souvent en fin de corps). */
function detectExternalUrl(body: string): string | undefined {
  const matches = body.match(/https?:\/\/[^\s)>\]"']+/g)
  if (!matches?.length) return undefined
  return matches[matches.length - 1].replace(/[.,;]$/, '')
}

interface ParsedMarkdown {
  frontmatter: Record<string, unknown>
  body: string
}

function splitFrontmatter(raw: string): ParsedMarkdown {
  const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/)
  if (!m) return { frontmatter: {}, body: raw }
  let frontmatter: Record<string, unknown> = {}
  try {
    frontmatter = (parseYaml(m[1]) as Record<string, unknown>) ?? {}
  } catch {
    frontmatter = {}
  }
  return { frontmatter, body: m[2].trim() }
}

/**
 * Regroupe les fichiers d'un dossier projet en MediaAsset :
 * base.png + base@2x.png/@3x/@4x -> un asset avec variantes,
 * ordonnés selon media_order puis alphabétique pour le reste.
 */
function collectMedia(files: string[], mediaOrder: string[]): MediaAsset[] {
  const assets = new Map<string, MediaAsset>()

  // Le nom canonique est celui sans suffixe @Nx (c'est lui que référencent
  // media_order et aura.image). Certains projets n'ont qu'un @1x sans base.
  for (const f of files) {
    if (!IMAGE_EXT.test(f) && !VIDEO_EXT.test(f)) continue
    const m = f.match(VARIANT_RE)
    const canonical = m ? m[1] + m[3] : f
    const label = (m ? `${m[2]}x` : '1x') as MediaVariant['label']

    let asset = assets.get(canonical)
    if (!asset) {
      asset = {
        filename: canonical,
        type: VIDEO_EXT.test(canonical) ? 'video' : 'image',
        variants: [],
      }
      assets.set(canonical, asset)
    }
    const existing = asset.variants.find(v => v.label === label)
    if (!existing) {
      asset.variants.push({ label, path: f })
    } else if (!m) {
      existing.path = f // le fichier de base l'emporte sur un doublon @1x
    }
  }
  for (const a of assets.values()) {
    a.variants.sort((x, y) => parseInt(x.label) - parseInt(y.label))
  }

  // Tri : media_order d'abord, puis le reste en alphabétique
  const orderIndex = new Map(mediaOrder.map((name, i) => [name, i]))
  return [...assets.values()].sort((a, b) => {
    const ia = orderIndex.has(a.filename) ? orderIndex.get(a.filename)! : Infinity
    const ib = orderIndex.has(b.filename) ? orderIndex.get(b.filename)! : Infinity
    if (ia !== ib) return ia - ib
    return a.filename.localeCompare(b.filename)
  })
}

async function readProject(
  contentDir: string,
  categoryDir: string,
  projectDir: string,
): Promise<Project | null> {
  const dir = join(contentDir, categoryDir, projectDir)
  let raw: string
  try {
    raw = await readFile(join(dir, 'reader.md'), 'utf8')
  } catch {
    return null // pas un dossier projet (pas de reader.md)
  }

  const { frontmatter, body } = splitFrontmatter(raw)
  const files = (await readdir(dir)).filter(f => f !== 'reader.md')

  const taxonomy = (frontmatter.taxonomy ?? {}) as Record<string, unknown>
  const mediaOrder = String(frontmatter.media_order ?? '')
    .split(',')
    .map(s => s.trim())
    .filter(Boolean)

  const media = collectMedia(files, mediaOrder)
  // Dimensions des images (en-tête seulement, rapide) : le ratio est le même
  // pour toutes les variantes @Nx, la 1re suffit.
  for (const asset of media) {
    if (asset.type !== 'image') continue
    try {
      const meta = await sharp(join(dir, asset.variants[0].path)).metadata()
      asset.width = meta.width
      asset.height = meta.height
    } catch {
      // dimensions indisponibles : le choix de grille retombera sur « mixte »
    }
  }
  // Image de couverture = celle de la grille du CMS : la PREMIÈRE image de
  // media_order (cf. projet_card.html.twig : p.media.images|first), pas
  // aura.image qui sert aux partages sociaux.
  const coverImage = media.find(m => m.type === 'image')?.filename

  return {
    id: `${categoryDir}/${projectDir}`,
    category: stripOrderPrefix(categoryDir),
    title: String(frontmatter.title ?? stripOrderPrefix(projectDir)),
    bodyMarkdown: body,
    bodyHtml: await marked.parse(body),
    coverImage,
    media,
    categories: toArray(taxonomy.category),
    tags: toArray(taxonomy.tag),
    date: parseGravDate(frontmatter.date),
    externalUrl: detectExternalUrl(body),
    published: frontmatter.published !== false,
  }
}

/** Lit tous les projets du dossier 02.projets (3 catégories). */
export async function readAllProjects(contentDir: string): Promise<Project[]> {
  const projects: Project[] = []
  let categoryDirs: string[] = []
  try {
    categoryDirs = (await readdir(contentDir, { withFileTypes: true }))
      .filter(e => e.isDirectory())
      .map(e => e.name)
      .sort()
  } catch {
    return []
  }

  for (const categoryDir of categoryDirs) {
    const entries = await readdir(join(contentDir, categoryDir), { withFileTypes: true })
    for (const entry of entries.filter(e => e.isDirectory()).sort((a, b) => a.name.localeCompare(b.name))) {
      const project = await readProject(contentDir, categoryDir, entry.name)
      if (project) projects.push(project)
    }
  }

  // Tri global : plus récent d'abord (comme les listings Grav)
  projects.sort((a, b) => (b.date ?? '').localeCompare(a.date ?? ''))
  return projects
}

/** mtime le plus récent des reader.md — sert à invalider le cache mémoire. */
export async function contentFingerprint(contentDir: string): Promise<string> {
  try {
    const s = await stat(contentDir)
    return String(s.mtimeMs)
  } catch {
    return '0'
  }
}
