<?php
/**
 * modules/soporte/DocIndex.php
 * Descubre los documentos en docs/, arma el árbol de navegación y resuelve el
 * control de acceso por sección.
 *
 * Convenciones:
 *   docs/<seccion>/<NN>-<slug>.md   → el prefijo NN solo ordena, no se muestra
 *   El título sale del primer encabezado `# ` del archivo.
 *
 * Un documento puede restringir su visibilidad con una cabecera al inicio:
 *
 *     ---
 *     roles: enfermero, recepcionista
 *     ---
 *
 * Sin cabecera, hereda el permiso de su sección. Los administradores ven todo.
 */

final class DocIndex
{
    /** Secciones, en orden de aparición. `admin` = solo administradores. */
    public const SECCIONES = [
        'manual'      => ['titulo' => 'Manual de usuario',   'icono' => 'fas fa-book-reader', 'admin' => false,
                          'resumen' => 'Cómo usar el sistema, paso a paso, según su rol.'],
        'tecnica'     => ['titulo' => 'Documentación técnica','icono' => 'fas fa-code',        'admin' => true,
                          'resumen' => 'Cada módulo por dentro: tablas, endpoints y dependencias.'],
        'arquitectura'=> ['titulo' => 'Arquitectura',         'icono' => 'fas fa-sitemap',     'admin' => true,
                          'resumen' => 'Cómo está armado el sistema y por qué.'],
        'operacion'   => ['titulo' => 'Operación y soporte',  'icono' => 'fas fa-life-ring',   'admin' => true,
                          'resumen' => 'Qué hacer cuando algo falla. Configuraciones críticas.'],
    ];

    /** Roles que ven todo, sin importar lo que declare cada documento. */
    public const ROLES_TOTALES = ['admin', 'superadmin'];

    public static function dir(): string
    {
        return __DIR__ . '/docs';
    }

    private static function rolActual(): string
    {
        return $_SESSION['admin_user']['role'] ?? '';
    }

    private static function esAdmin(): bool
    {
        return in_array(self::rolActual(), self::ROLES_TOTALES, true);
    }

    /**
     * Separa la cabecera del cuerpo de un documento.
     * @return array{0: array<string,string>, 1: string}  [metadatos, cuerpo]
     */
    public static function leer(string $archivo): array
    {
        $texto = (string)@file_get_contents($archivo);
        if (!preg_match('/^---\R(.*?)\R---\R?(.*)$/s', $texto, $m)) {
            return [[], $texto];
        }
        $meta = [];
        foreach (preg_split('/\R/', $m[1]) as $linea) {
            if (preg_match('/^\s*([\w-]+)\s*:\s*(.*)$/', $linea, $kv)) {
                $meta[strtolower($kv[1])] = trim($kv[2]);
            }
        }
        return [$meta, ltrim($m[2])];
    }

    /** ¿El usuario actual puede ver este documento concreto? */
    public static function puedeVerDoc(string $seccion, string $archivo): bool
    {
        if (self::esAdmin()) return true;
        if (!self::puedeVer($seccion)) return false;

        [$meta] = self::leer($archivo);
        if (empty($meta['roles'])) return true; // sin cabecera: hereda la sección

        $permitidos = array_filter(array_map('trim', explode(',', strtolower($meta['roles']))));
        return in_array(strtolower(self::rolActual()), $permitidos, true);
    }

    /** ¿El usuario actual puede ver esta sección? */
    public static function puedeVer(string $seccion): bool
    {
        $cfg = self::SECCIONES[$seccion] ?? null;
        if (!$cfg) return false;
        if (!$cfg['admin']) return true;
        return self::esAdmin();
    }

    /** Secciones visibles para el usuario actual. */
    public static function seccionesVisibles(): array
    {
        return array_filter(
            self::SECCIONES,
            fn($s) => self::puedeVer($s),
            ARRAY_FILTER_USE_KEY
        );
    }

    /**
     * Árbol completo: [seccion => ['titulo'=>..,'docs'=>[['slug','titulo','archivo'],...]]]
     * Solo incluye lo que el usuario puede ver.
     */
    public static function arbol(): array
    {
        $arbol = [];
        foreach (self::seccionesVisibles() as $sec => $cfg) {
            $ruta = self::dir() . '/' . $sec;
            if (!is_dir($ruta)) continue;

            $docs = [];
            foreach (glob($ruta . '/*.md') ?: [] as $archivo) {
                if (!self::puedeVerDoc($sec, $archivo)) continue;
                $base = basename($archivo, '.md');
                $slug = preg_replace('/^\d+-/', '', $base);
                $docs[] = [
                    'slug'    => $slug,
                    'titulo'  => self::titulo($archivo, $slug),
                    'archivo' => $archivo,
                    'orden'   => $base,
                ];
            }
            usort($docs, fn($a, $b) => strcmp($a['orden'], $b['orden']));

            // Una sección sin documentos visibles no se muestra
            if (!$docs) continue;

            $arbol[$sec] = $cfg + ['docs' => $docs];
        }
        return $arbol;
    }

    /** Ruta del archivo pedido, o null si no existe o no hay acceso. */
    public static function resolver(string $seccion, string $slug): ?string
    {
        if (!self::puedeVer($seccion)) return null;
        // Evitar traversal: los slugs son [a-z0-9-]
        if (!preg_match('/^[a-z0-9-]+$/', $seccion) || !preg_match('/^[a-z0-9-]+$/', $slug)) return null;

        foreach (glob(self::dir() . '/' . $seccion . '/*.md') ?: [] as $archivo) {
            if (preg_replace('/^\d+-/', '', basename($archivo, '.md')) !== $slug) continue;
            return self::puedeVerDoc($seccion, $archivo) ? $archivo : null;
        }
        return null;
    }

    /** Primer encabezado `# ` del archivo; si no hay, el slug capitalizado. */
    private static function titulo(string $archivo, string $slugFallback): string
    {
        $fh = @fopen($archivo, 'r');
        if ($fh) {
            $lineas = 0;
            while (($l = fgets($fh)) !== false && $lineas++ < 30) {
                if (preg_match('/^#\s+(.+)$/', trim($l), $m)) { fclose($fh); return trim($m[1]); }
            }
            fclose($fh);
        }
        return ucfirst(str_replace('-', ' ', $slugFallback));
    }

    /** Palabras que no aportan al puntaje de relevancia. */
    private const VACIAS = [
        'como','cual','cuales','donde','cuando','porque','para','pero','esta','este','esto',
        'con','sin','por','que','del','las','los','una','uno','del','sus','sobre','desde',
        'hacer','tengo','puedo','quiero','necesito','ayuda','favor','the','and','not','del',
    ];

    /** Secciones que puede consultar el asistente. Ver contextoIA(). */
    public const SECCIONES_IA = ['manual'];

    /**
     * Selecciona los documentos más relevantes para una pregunta y devuelve su
     * texto, listo para dárselo a un modelo de lenguaje.
     *
     * Dos restricciones se aplican a la vez:
     *
     *  1. Por sección: el asistente solo consulta el manual de usuario. Está
     *     para ayudar a usar el sistema, no a mantenerlo — la documentación
     *     técnica, de arquitectura y de operación queda fuera incluso para
     *     administradores, que pueden leerla directamente en el módulo.
     *  2. Por rol: dentro del manual solo entra lo que ese usuario podría leer
     *     por su cuenta, así el asistente no puede revelar contenido ajeno.
     *
     * @return array{0: string, 1: array<int,string>}  [contexto, títulos usados]
     */
    public static function contextoIA(string $pregunta, int $maxDocs = 4, int $maxChars = 14000): array
    {
        $palabras = array_values(array_filter(
            preg_split('/[^a-záéíóúñü0-9]+/u', mb_strtolower($pregunta)),
            fn($p) => mb_strlen($p) > 2 && !in_array($p, self::VACIAS, true)
        ));
        if (!$palabras) return ['', []];

        // Textos de los documentos consultables
        $docs = [];
        foreach (self::arbol() as $sec => $cfg) {
            if (!in_array($sec, self::SECCIONES_IA, true)) continue;
            foreach ($cfg['docs'] as $doc) {
                [, $cuerpo] = self::leer($doc['archivo']);
                $docs[] = [
                    'titulo'  => $doc['titulo'],
                    'seccion' => $cfg['titulo'],
                    'cuerpo'  => $cuerpo,
                    'heno'    => mb_strtolower($doc['titulo'] . ' ' . $cuerpo),
                ];
            }
        }
        if (!$docs) return ['', []];

        // Peso de cada palabra según en cuántos documentos aparece: una que está
        // en todos ("paciente") no distingue nada; una que está en pocos sí.
        $total = count($docs);
        $peso  = [];
        foreach ($palabras as $p) {
            $enCuantos = 0;
            foreach ($docs as $d) if (str_contains($d['heno'], $p)) $enCuantos++;
            $peso[$p] = $enCuantos ? log(1 + $total / $enCuantos) : 0;
        }

        $candidatos = [];
        foreach ($docs as $d) {
            $puntaje = 0.0;
            foreach ($palabras as $p) {
                if (!$peso[$p]) continue;
                // El título pesa mucho más que una mención en el cuerpo
                $puntaje += substr_count(mb_strtolower($d['titulo']), $p) * 12 * $peso[$p];
                $puntaje += min(substr_count($d['heno'], $p), 6) * $peso[$p];
            }
            if ($puntaje > 0) $candidatos[] = ['puntaje' => $puntaje] + $d;
        }
        if (!$candidatos) return ['', []];

        usort($candidatos, fn($a, $b) => $b['puntaje'] <=> $a['puntaje']);

        // Umbral absoluto: por debajo son coincidencias sueltas de palabras
        // sin relación real con la pregunta, y darle eso al modelo lo lleva a
        // responder con lo que tenga a mano en vez de admitir que no sabe.
        if ($candidatos[0]['puntaje'] < 6) return ['', []];

        // Y relativo: descartar lo que quede muy por debajo del mejor resultado
        $corte = $candidatos[0]['puntaje'] * 0.45;
        $candidatos = array_values(array_filter($candidatos, fn($c) => $c['puntaje'] >= $corte));
        $candidatos = array_slice($candidatos, 0, $maxDocs);

        $porDoc  = (int)floor($maxChars / count($candidatos));
        $ctx     = '';
        $titulos = [];
        foreach ($candidatos as $c) {
            $texto = preg_replace('/\{\{\w+\}\}/', '', $c['cuerpo']); // los marcadores no aportan
            $texto = mb_substr(trim($texto), 0, $porDoc);
            $ctx .= "\n\n===== [{$c['seccion']}] {$c['titulo']} =====\n" . $texto;
            $titulos[] = $c['titulo'];
        }
        return [trim($ctx), $titulos];
    }

    /**
     * Índice para el buscador: un registro por documento con su texto plano.
     * Solo incluye secciones visibles para el usuario actual.
     */
    public static function indiceBusqueda(): array
    {
        $out = [];
        foreach (self::arbol() as $sec => $cfg) {
            foreach ($cfg['docs'] as $doc) {
                [, $texto] = self::leer($doc['archivo']);
                // Aplanar: sin marcas, sin saltos, compacto
                $texto = preg_replace('/```.*?```/s', ' ', $texto);
                $texto = preg_replace('/[#>*_`|-]+/', ' ', $texto);
                $texto = preg_replace('/\s+/', ' ', $texto);
                $out[] = [
                    's' => $sec,
                    'u' => $doc['slug'],
                    't' => $doc['titulo'],
                    'c' => $cfg['titulo'],
                    'x' => mb_substr(trim($texto), 0, 4000),
                ];
            }
        }
        return $out;
    }
}
