<?php
// ============================================================
// tools/renderer/Renderer.php
// ============================================================
// MISSION      : Parseur unique Scolaide — rend pages, quiz,
//                exercices depuis fichier ou DB (mode hybride)
//
// FORMAT FICHIER : {username}__{ref_code}__{slug}.{ext}
//   ex: chaieb__cr-111111__techniques-de-calcul.md
//
// UTILISATION :
//   require_once '/path/to/tools/renderer/Renderer.php';
//   $html = Renderer::page('cr-111111', $pdo);
//   $data = Renderer::quiz('qz-abc123', $pdo);
//   $html = Renderer::markdown('**texte** $x^2$');
//
// VERSION : 2.0 — format username__ref__slug
// ============================================================

if (!defined('SCOLAIDE_RENDERER_LOADED')) {
define('SCOLAIDE_RENDERER_LOADED', true);

if (!defined('CONTENT_DIR')) {
    define('CONTENT_DIR', dirname(dirname(__DIR__)) . '/content');
}

require_once __DIR__ . '/vendor/autoload.php';

use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
use League\CommonMark\Extension\Footnote\FootnoteExtension;
use League\CommonMark\MarkdownConverter;

class Renderer
{
    private static ?MarkdownConverter $_conv     = null;
    private static ?MarkdownConverter $_convSafe = null;
    private static array $_fileCache = []; // cache glob par ref

    // ── Convertisseur CommonMark ──────────────────────────────
    private static function conv(bool $safe = false): MarkdownConverter
    {
        $k = $safe ? '_convSafe' : '_conv';
        if (self::$$k === null) {
            $env = new Environment([
                'html_input'         => $safe ? 'strip' : 'allow',
                'allow_unsafe_links' => false,
                'max_nesting_level'  => 100,
            ]);
            $env->addExtension(new CommonMarkCoreExtension());
            $env->addExtension(new GithubFlavoredMarkdownExtension());
            try { $env->addExtension(new FootnoteExtension()); } catch (\Throwable $e) {}
            self::$$k = new MarkdownConverter($env);
        }
        return self::$$k;
    }

    // ── Slug ─────────────────────────────────────────────────
    public static function slug(string $title): string
    {
        $map = [
            'é'=>'e','è'=>'e','ê'=>'e','ë'=>'e','à'=>'a','â'=>'a','ä'=>'a',
            'ù'=>'u','û'=>'u','ü'=>'u','ô'=>'o','ö'=>'o','î'=>'i','ï'=>'i',
            'ç'=>'c','ñ'=>'n','É'=>'e','È'=>'e','Ê'=>'e','À'=>'a','Â'=>'a',
            'Ù'=>'u','Û'=>'u','Ô'=>'o','Î'=>'i','Ç'=>'c',
            '—'=>'-','–'=>'-','\''=>'','"'=>'',
        ];
        $s = strtr($title, $map);
        $s = strtolower(preg_replace('/[^a-z0-9]+/i', '-', $s));
        $s = trim(preg_replace('/-+/', '-', $s), '-');
        return substr($s, 0, 40);
    }

    // ── Nom de fichier canonique ──────────────────────────────
    public static function filename(string $username, string $ref, string $title, string $ext = 'md'): string
    {
        $user = preg_replace('/[^a-z0-9\.\-]/i', '', $username);
        $ref  = preg_replace('/[^a-z0-9\-]/i', '', $ref);
        $slug = self::slug($title);
        return "{$user}__{$ref}__{$slug}.{$ext}";
    }

    // ── Trouver un fichier par ref (glob + cache) ─────────────
    // Cherche dans content/{type}/*__{ref}__*.{ext}
    public static function findFile(string $type, string $ref): ?string
    {
        $ref  = preg_replace('/[^a-z0-9\-]/i', '', $ref);
        $key  = $type . ':' . $ref;
        if (isset(self::$_fileCache[$key])) return self::$_fileCache[$key];

        $dir  = CONTENT_DIR . '/' . $type;
        foreach (['md','qz','py'] as $ext) {
            $pattern = $dir . '/*__' . $ref . '__*.' . $ext;
            $files   = glob($pattern);
            if (!empty($files)) {
                self::$_fileCache[$key] = $files[0];
                return $files[0];
            }
        }
        self::$_fileCache[$key] = null;
        return null;
    }

    // ── Vider le cache (après écriture) ──────────────────────
    public static function clearCache(string $ref = ''): void
    {
        if ($ref === '') { self::$_fileCache = []; return; }
        foreach (array_keys(self::$_fileCache) as $k) {
            if (str_contains($k, ':' . $ref)) unset(self::$_fileCache[$k]);
        }
    }

    // ── Parser front-matter YAML minimaliste ─────────────────
    public static function parseFrontMatter(string $raw): array
    {
        $meta = []; $content = $raw;
        $raw2 = ltrim($raw);
        if (str_starts_with($raw2, '---')) {
            if (preg_match('/^---\r?\n(.*?)\r?\n---\r?\n/s', $raw2, $m)) {
                $content = substr($raw2, strlen($m[0]));
                foreach (explode("\n", $m[1]) as $line) {
                    $line = trim($line);
                    if ($line === '' || $line[0] === '#') continue;
                    if (!str_contains($line, ':')) continue;
                    [$k, $v] = explode(':', $line, 2);
                    $k = trim($k); $v = trim($v, " \t\"'");
                    if ($v === 'true')       $v = true;
                    elseif ($v === 'false')  $v = false;
                    elseif (is_numeric($v))  $v = (int)$v;
                    $meta[$k] = $v;
                }
            }
        }
        return ['meta' => $meta, 'content' => $content];
    }

    // ── Rendu Markdown avec protection LaTeX ─────────────────
    public static function markdown(string $raw, bool $safe = false): string
    {
        if (trim($raw) === '') return '';
        $ph = []; $i = 0;

        // $$...$$ display
        $raw = preg_replace_callback('/\$\$(.+?)\$\$/s', function($m) use (&$ph,&$i) {
            $k = "MBLOCK{$i}"; $i++;
            $ph[$k] = '<div class="math math-display">\\['
                    . htmlspecialchars($m[1], ENT_QUOTES, 'UTF-8') . '\\]</div>';
            return $k;
        }, $raw);

        // $...$ inline
        $raw = preg_replace_callback('/(?<!\$)\$([^\$\n]{1,300}?)\$(?!\$)/', function($m) use (&$ph,&$i) {
            $k = "MINLINE{$i}"; $i++;
            $ph[$k] = '<span class="math math-inline">\\('
                    . htmlspecialchars($m[1], ENT_QUOTES, 'UTF-8') . '\\)</span>';
            return $k;
        }, $raw);

        try {
            $html = (string) self::conv($safe)->convert($raw);
        } catch (\Throwable $e) {
            error_log('[Renderer::markdown] ' . $e->getMessage());
            return '<pre>' . htmlspecialchars($raw, ENT_QUOTES, 'UTF-8') . '</pre>';
        }

        return str_replace(array_keys($ph), array_values($ph), $html);
    }

    // ── Rendre une page ───────────────────────────────────────
    public static function page(string $ref, ?\PDO $pdo = null): string
    {
        $file = self::findFile('pages', $ref);
        if ($file !== null) {
            ['content' => $md] = self::parseFrontMatter(file_get_contents($file));
            return self::markdown($md);
        }
        // Fallback DB
        if ($pdo) {
            try {
                $st = $pdo->prepare(
                    "SELECT content_html, content_md FROM pages
                     WHERE ref_code=:ref AND deleted_at IS NULL LIMIT 1"
                );
                $st->execute([':ref' => $ref]);
                $row = $st->fetch();
                if ($row) {
                    if (!empty($row['content_html'])) return $row['content_html'];
                    if (!empty($row['content_md']))   return self::markdown($row['content_md']);
                }
            } catch (\Throwable $e) {
                error_log('[Renderer::page] ' . $e->getMessage());
            }
        }
        return '';
    }

    // ── Métadonnées d'une page ────────────────────────────────
    public static function pageMeta(string $ref): array
    {
        $file = self::findFile('pages', $ref);
        if ($file === null) return [];
        return self::parseFrontMatter(file_get_contents($file))['meta'];
    }

    // ── Parser un quiz ────────────────────────────────────────
    public static function quiz(string $ref, ?\PDO $pdo = null): array
    {
        $empty = ['meta'=>[],'questions'=>[],'intro'=>'','raw'=>''];

        $file = self::findFile('quizzes', $ref);
        $raw  = $file ? file_get_contents($file) : null;

        // Fallback DB
        if ($raw === null && $pdo) {
            try {
                $st = $pdo->prepare(
                    "SELECT content_raw, intro_raw FROM quizzes
                     WHERE ref_code=:ref AND deleted_at IS NULL LIMIT 1"
                );
                $st->execute([':ref' => $ref]);
                $row = $st->fetch();
                if ($row && !empty($row['content_raw'])) {
                    $raw = $row['content_raw'];
                    if (!empty($row['intro_raw'])) {
                        $introYaml = "---\nintro: |\n  "
                            . str_replace("\n", "\n  ", trim($row['intro_raw']))
                            . "\n---\n\n";
                        $raw = $introYaml . $raw;
                    }
                }
            } catch (\Throwable $e) {
                error_log('[Renderer::quiz] ' . $e->getMessage());
            }
        }

        if ($raw === null) return $empty;
        ['meta'=>$meta,'content'=>$content] = self::parseFrontMatter($raw);
        $intro     = $meta['intro'] ?? '';
        $questions = self::parseQuestions($content);
        return ['meta'=>$meta,'questions'=>$questions,'intro'=>$intro,'raw'=>$content];
    }

    // ── Parser text2quiz → tableau questions ─────────────────
    public static function parseQuestions(string $raw): array
    {
        $types = ['VF','QCM','QR','QRC','Trous','TrousRC',
                  'Ordre','Grille','Etiquettes','Association','Correspondance'];
        $questions = []; $idx = 0;

        foreach (preg_split('/\r?\n/', $raw) as $line) {
            $line = trim($line);
            if ($line === '') continue;

            // R: R+: R-:
            if (preg_match('/^R([+\-]?):\s*(.+)$/u', $line, $rm)) {
                if (!empty($questions)) {
                    $last = count($questions)-1;
                    $t=trim($rm[1]); $tx=trim($rm[2]);
                    if ($t==='+')     $questions[$last]['r_correct']   = $tx;
                    elseif ($t==='-') $questions[$last]['r_incorrect'] = $tx;
                    else { $questions[$last]['r_correct']=$tx; $questions[$last]['r_incorrect']=$tx; }
                }
                continue;
            }

            $sep = strpos($line, ' || ');
            if ($sep === false) continue;
            $type = trim(substr($line, 0, $sep));
            if (!in_array($type, $types, true)) continue;

            $parts = explode(' || ', $line);
            $q     = self::_parseLine($type, $parts, $idx);
            if ($q !== null) { $questions[] = $q; $idx++; }
        }
        return $questions;
    }

    private static function _parseLine(string $type, array $p, int $idx): ?array
    {
        switch ($type) {
            case 'VF':
                return count($p)<3 ? null : ['type'=>'VF','id'=>$idx,'question'=>$p[1],
                    'data'=>['correct'=>trim($p[2])==='V'?'V':'F']];

            case 'QCM': case 'QR': {
                if (count($p)<3) return null;
                $ch=[]; $co=[];
                foreach (explode('|',$p[2]) as $item) {
                    $item=trim($item);
                    if (str_starts_with($item,'V:')){ $v=trim(substr($item,2)); $ch[]=$v; $co[]=$v; }
                    else $ch[]=$item;
                }
                return ['type'=>$type,'id'=>$idx,'question'=>$p[1],
                    'data'=>$type==='QCM'?['choices'=>$ch,'corrects'=>$co]:['choices'=>$ch,'correct'=>$co[0]??'']];
            }

            case 'QRC':
                return count($p)<3 ? null : ['type'=>'QRC','id'=>$idx,'question'=>$p[1],
                    'data'=>['corrects'=>array_map('trim',explode('|',$p[2]))]];

            case 'Trous': case 'TrousRC': {
                $rc=($type==='TrousRC'); $text=trim($p[1]??''); $blanks=[];
                preg_match_all('/\{\{(.*?)\}\}/',$text,$ms,PREG_SET_ORDER);
                foreach ($ms as $m) {
                    $opts=array_map('trim',explode('|',$m[1]));
                    $correct=null; $choices=[];
                    foreach ($opts as $opt) {
                        if (str_starts_with($opt,'V:')){ $v=trim(substr($opt,2)); $choices[]=$v; if(!$correct)$correct=$v; }
                        else { $choices[]=$opt; if($rc&&!$correct)$correct=$opt; }
                    }
                    $blanks[]=['correct'=>$correct,'choices'=>$choices,'rc'=>$rc];
                }
                return ['type'=>$type,'id'=>$idx,'question'=>'','data'=>['text'=>$text,'blanks'=>$blanks]];
            }

            case 'Ordre': {
                $s1=trim($p[1]??''); $s2=trim($p[2]??'');
                $items=substr_count($s2,'|')>substr_count($s1,'|')?$s2:$s1;
                $cons =substr_count($s2,'|')>substr_count($s1,'|')?$s1:$s2;
                return ['type'=>'Ordre','id'=>$idx,'question'=>$cons,
                    'data'=>['items'=>array_values(array_filter(array_map('trim',explode('|',$items))))]];
            }

            case 'Grille': case 'Etiquettes': {
                $cats=array_map('trim',explode('|',$p[1]??'')); $grps=[];
                preg_match_all('/\{\{(.*?)\}\}/',$p[2]??'',$ms);
                foreach ($ms[1] as $g) $grps[]=array_map('trim',explode('|',$g));
                return ['type'=>$type,'id'=>$idx,'question'=>trim($p[3]??''),
                    'data'=>['cats'=>$cats,'itemsByCategory'=>$grps]];
            }

            case 'Association': {
                $pairs=[]; preg_match_all('/\{\{(.*?)\}\}/',$p[1]??'',$ms);
                foreach ($ms[1] as $m) {
                    $pair=array_map('trim',explode('|',$m));
                    $pairs[]=['label'=>$pair[0]??'','match'=>$pair[1]??''];
                }
                return ['type'=>'Association','id'=>$idx,'question'=>trim($p[2]??''),
                    'data'=>['pairs'=>$pairs]];
            }

            case 'Correspondance': {
                $r=$p[2]??''; $strict=false;
                if (preg_match('/\{\{\s*ORDRE\s*\|/',$r)){ $strict=true; $r=preg_replace('/ORDRE\s*\|/','',$r); }
                $grps=[]; preg_match_all('/\{\{(.*?)\}\}/',$r,$ms);
                foreach ($ms[1] as $g) $grps[]=array_map('trim',explode('|',$g));
                return ['type'=>'Correspondance','id'=>$idx,'question'=>trim($p[3]??''),
                    'data'=>['category'=>$p[1]??'','correct'=>$grps[0]??[],'incorrect'=>$grps[1]??[],'orderStrict'=>$strict]];
            }
        }
        return null;
    }

    // ── Écrire un fichier ─────────────────────────────────────
    public static function writePage(string $username, string $ref, string $title, array $meta, string $content): bool
    {
        $dir  = CONTENT_DIR . '/pages';
        if (!is_dir($dir)) mkdir($dir, 0755, true);
        // Supprimer l'ancien fichier si la ref existe déjà
        $old = self::findFile('pages', $ref);
        if ($old && $old !== '') @unlink($old);
        self::clearCache($ref);
        $fname = self::filename($username, $ref, $title, 'md');
        $yaml  = self::_buildYaml(array_merge(['ref_code'=>$ref,'title'=>$title], $meta));
        return file_put_contents($dir.'/'.$fname, $yaml.$content) !== false;
    }

    public static function writeQuiz(string $username, string $ref, string $title, array $meta, string $content): bool
    {
        $dir  = CONTENT_DIR . '/quizzes';
        if (!is_dir($dir)) mkdir($dir, 0755, true);
        $old  = self::findFile('quizzes', $ref);
        if ($old && $old !== '') @unlink($old);
        self::clearCache($ref);
        $fname = self::filename($username, $ref, $title, 'qz');
        $yaml  = self::_buildYaml(array_merge(['ref_code'=>$ref,'title'=>$title], $meta));
        return file_put_contents($dir.'/'.$fname, $yaml.$content) !== false;
    }

    private static function _buildYaml(array $meta): string
    {
        $yaml = "---\n";
        foreach ($meta as $k => $v) {
            if (is_bool($v))       $yaml .= "$k: " . ($v?'true':'false') . "\n";
            elseif (is_int($v))    $yaml .= "$k: $v\n";
            elseif (is_string($v)) $yaml .= "$k: " . str_replace(["\n","\r"],' ',$v) . "\n";
        }
        return $yaml . "---\n\n";
    }

    // ── Infos d'un fichier (parse le nom) ────────────────────
    public static function parseFilename(string $filepath): array
    {
        $base  = basename($filepath);
        $noext = preg_replace('/\.(md|qz|py)$/', '', $base);
        $parts = explode('__', $noext, 3);
        return [
            'username' => $parts[0] ?? '',
            'ref_code' => $parts[1] ?? '',
            'slug'     => $parts[2] ?? '',
            'ext'      => pathinfo($filepath, PATHINFO_EXTENSION),
        ];
    }

    // ── Lister les fichiers d'un type ─────────────────────────
    public static function listFiles(string $type, string $username = ''): array
    {
        $pattern = CONTENT_DIR . '/' . $type . '/';
        $pattern .= $username ? $username . '__*' : '*';
        $pattern .= '.*';
        $files = glob($pattern) ?: [];
        return array_map([self::class, 'parseFilename'], $files);
    }
}

} // end SCOLAIDE_RENDERER_LOADED
