<?php
/**
 * includes/OneDriveStorage.php — Helper Microsoft Graph / OneDrive para SIGVP
 *
 * Cubre TODAS las carpetas de assets/uploads:
 * actas, bitacoras, certificados, comprobantes_practicas,
 * comprobantes_vinculacion, convalidaciones, documentos_personales,
 * fichas_ppp, foto, informes, solicitudes, titulacion,
 * vinculacion, vinculacion_grupos, documentos_ppp
 *
 * Coloca este archivo en: C:\xampp\htdocs\SIGVP\includes\OneDriveStorage.php
 */

require_once __DIR__ . '/../config/onedrive.php';

class OneDriveStorage
{
    // ── Prefijo que identifica rutas OneDrive en la BD ────────────
    const PREFIX = 'onedrive:';

    private static ?string $accessToken = null;
    private static int $tokenExpiry = 0;

    // ────────────────────────────────────────────────────────────────
    // OBTIENE el access token (lo cachea en memoria por 55 minutos)
    // ────────────────────────────────────────────────────────────────
    private static function getToken(): string
    {
        if (self::$accessToken && time() < self::$tokenExpiry) {
            return self::$accessToken;
        }

        $url  = 'https://login.microsoftonline.com/' . OD_TENANT_ID . '/oauth2/v2.0/token';
        $body = http_build_query([
            'grant_type'    => 'client_credentials',
            'client_id'     => OD_CLIENT_ID,
            'client_secret' => OD_CLIENT_SECRET,
            'scope'         => 'https://graph.microsoft.com/.default',
        ]);

        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => $body,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER     => ['Content-Type: application/x-www-form-urlencoded'],
        ]);
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpCode !== 200) {
            throw new RuntimeException('Error al obtener token de Microsoft Graph. HTTP ' . $httpCode . ': ' . $response);
        }

        $data = json_decode($response, true);
        if (empty($data['access_token'])) {
            throw new RuntimeException('Token vacío en respuesta de Microsoft: ' . $response);
        }

        self::$accessToken = $data['access_token'];
        self::$tokenExpiry = time() + ($data['expires_in'] ?? 3600) - 300;

        return self::$accessToken;
    }

    // ────────────────────────────────────────────────────────────────
    // Construye la URL base para operar dentro de SIGVP-Uploads
    // ────────────────────────────────────────────────────────────────
    private static function driveUrl(string $path = ''): string
    {
        $base = OD_GRAPH_URL . '/drives/' . OD_DRIVE_ID . '/items/' . OD_FOLDER_ID;
        return $path ? $base . ':/' . ltrim($path, '/') . ':' : $base;
    }

    // ────────────────────────────────────────────────────────────────
    // SUBE un archivo al OneDrive
    //
    // @param string $tmpPath     Ruta temporal ($_FILES['x']['tmp_name'])
    // @param string $carpeta     Carpeta lógica (ej: 'bitacoras', 'actas')
    // @param string $nombreFinal Nombre definitivo del archivo
    // @param string|null $subdir Subdirectorio opcional (ej: id_practica)
    // @return string             Referencia para BD: "onedrive:carpeta/subdir/nombre"
    // ────────────────────────────────────────────────────────────────
    public static function upload(
        string $tmpPath,
        string $carpeta,
        string $nombreFinal,
        ?string $subdir = null
    ): string {
        $rutaRelativa = self::buildPath($carpeta, $nombreFinal, $subdir);
        $token        = self::getToken();
        $fileSize     = filesize($tmpPath);

        // Archivos < 4MB: subida simple
        if ($fileSize < 4 * 1024 * 1024) {
            self::uploadSimple($tmpPath, $rutaRelativa, $token);
        } else {
            // Archivos >= 4MB: subida por sesión (chunked)
            self::uploadChunked($tmpPath, $rutaRelativa, $token, $fileSize);
        }

        return self::PREFIX . $rutaRelativa;
    }

    // ── Subida simple (< 4MB) ─────────────────────────────────────
    private static function uploadSimple(string $tmpPath, string $rutaRelativa, string $token): void
    {
        $url     = self::driveUrl($rutaRelativa) . '/content';
        $content = file_get_contents($tmpPath);
        $mime    = mime_content_type($tmpPath) ?: 'application/octet-stream';

        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_CUSTOMREQUEST  => 'PUT',
            CURLOPT_POSTFIELDS     => $content,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER     => [
                'Authorization: Bearer ' . $token,
                'Content-Type: ' . $mime,
                'Content-Length: ' . strlen($content),
            ],
        ]);
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if (!in_array($httpCode, [200, 201])) {
            throw new RuntimeException('Error al subir archivo a OneDrive. HTTP ' . $httpCode . ': ' . $response);
        }
    }

    // ── Subida chunked (>= 4MB) ───────────────────────────────────
    private static function uploadChunked(string $tmpPath, string $rutaRelativa, string $token, int $fileSize): void
    {
        // 1. Crear sesión de subida
        $url         = self::driveUrl($rutaRelativa) . '/createUploadSession';
        $sessionBody = json_encode(['item' => ['@microsoft.graph.conflictBehavior' => 'replace']]);

        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => $sessionBody,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER     => [
                'Authorization: Bearer ' . $token,
                'Content-Type: application/json',
            ],
        ]);
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpCode !== 200) {
            throw new RuntimeException('Error al crear sesión de subida. HTTP ' . $httpCode);
        }

        $session     = json_decode($response, true);
        $uploadUrl   = $session['uploadUrl'];
        $chunkSize   = 5 * 1024 * 1024; // 5MB por chunk
        $handle      = fopen($tmpPath, 'rb');
        $offset      = 0;

        while (!feof($handle)) {
            $chunk     = fread($handle, $chunkSize);
            $chunkLen  = strlen($chunk);
            $rangeEnd  = $offset + $chunkLen - 1;

            $ch = curl_init($uploadUrl);
            curl_setopt_array($ch, [
                CURLOPT_CUSTOMREQUEST  => 'PUT',
                CURLOPT_POSTFIELDS     => $chunk,
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_HTTPHEADER     => [
                    'Content-Length: ' . $chunkLen,
                    'Content-Range: bytes ' . $offset . '-' . $rangeEnd . '/' . $fileSize,
                ],
            ]);
            $response = curl_exec($ch);
            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);

            if (!in_array($httpCode, [200, 201, 202])) {
                fclose($handle);
                throw new RuntimeException('Error en chunk upload. HTTP ' . $httpCode . ': ' . $response);
            }

            $offset += $chunkLen;
        }
        fclose($handle);
    }

    // ────────────────────────────────────────────────────────────────
    // OBTIENE la URL temporal de descarga directa de OneDrive (~1h)
    // Úsala en <img src> cuando quieras mostrar la imagen en el HTML
    // sin necesitar un proxy PHP.
    //
    // @param string $rutaBD  Valor de la BD: "onedrive:carpeta/archivo.jpg"
    // @return string|null    URL temporal de descarga, o null si falla
    // ────────────────────────────────────────────────────────────────
    public static function getDownloadUrl(string $rutaBD): ?string
    {
        if (!self::esOneDrive($rutaBD)) {
            return null; // no es OneDrive
        }

        try {
            $rutaRelativa = self::pathFromRuta($rutaBD);
            $token        = self::getToken();
            $url          = self::driveUrl($rutaRelativa) . '?select=@microsoft.graph.downloadUrl,name';

            $ch = curl_init($url);
            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $token],
            ]);
            $response = curl_exec($ch);
            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);

            if ($httpCode !== 200) return null;

            $data = json_decode($response, true);
            return $data['@microsoft.graph.downloadUrl'] ?? null;

        } catch (\Throwable $e) {
            return null;
        }
    }

    // ────────────────────────────────────────────────────────────────
    // SIRVE el archivo al navegador (stream directo sin URL pública)
    // ────────────────────────────────────────────────────────────────
    public static function stream(
        string $rutaBD,
        string $nombreArchivo,
        ?string $mimeForce = null
    ): void {
        $rutaRelativa = self::pathFromRuta($rutaBD);
        $token        = self::getToken();

        // Obtener URL de descarga
        $url = self::driveUrl($rutaRelativa) . '?select=@microsoft.graph.downloadUrl,name';
        $ch  = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $token],
        ]);
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpCode !== 200) {
            http_response_code(404);
            exit('Archivo no encontrado en OneDrive.');
        }

        $data        = json_decode($response, true);
        $downloadUrl = $data['@microsoft.graph.downloadUrl'] ?? null;

        if (!$downloadUrl) {
            http_response_code(404);
            exit('URL de descarga no disponible.');
        }

        $mime = $mimeForce ?? self::guessMime($nombreArchivo);
        header('Content-Type: ' . $mime);
        header('Content-Disposition: inline; filename="' . $nombreArchivo . '"');
        header('Cache-Control: private, max-age=3600');

        // Stream del archivo
        $ch = curl_init($downloadUrl);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => false,
            CURLOPT_WRITEFUNCTION  => function($ch, $data) {
                echo $data;
                return strlen($data);
            },
        ]);
        curl_exec($ch);
        curl_close($ch);
        exit;
    }

    // ────────────────────────────────────────────────────────────────
    // ELIMINA un archivo de OneDrive
    // ────────────────────────────────────────────────────────────────
    public static function delete(string $rutaBD): bool
    {
        if (!self::esOneDrive($rutaBD)) return false;

        $rutaRelativa = self::pathFromRuta($rutaBD);
        $token        = self::getToken();
        $url          = self::driveUrl($rutaRelativa);

        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_CUSTOMREQUEST  => 'DELETE',
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $token],
        ]);
        curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        return in_array($httpCode, [204, 404]);
    }

    // ────────────────────────────────────────────────────────────────
    // Detecta si la ruta en BD es de OneDrive o local
    // ────────────────────────────────────────────────────────────────
    public static function esOneDrive(string $rutaBD): bool
    {
        return str_starts_with($rutaBD, self::PREFIX);
    }

    // ────────────────────────────────────────────────────────────────
    // Servir archivo — detecta automáticamente OneDrive o local
    // Usar en todos los viewers para compatibilidad gradual
    // ────────────────────────────────────────────────────────────────
    public static function servirArchivo(
        string $rutaBD,
        string $nombreArchivo,
        ?string $mimeForce = null
    ): void {
        if (self::esOneDrive($rutaBD)) {
            self::stream($rutaBD, $nombreArchivo, $mimeForce);
        } else {
            self::servirLocal($rutaBD, $nombreArchivo, $mimeForce);
        }
    }

    // ────────────────────────────────────────────────────────────────
    // Construye la ruta relativa dentro de SIGVP-Uploads
    // Ej: buildPath('bitacoras','bit_12.jpg','12') → "bitacoras/12/bit_12.jpg"
    // ────────────────────────────────────────────────────────────────
    public static function buildPath(
        string $carpeta,
        string $nombreFinal,
        ?string $subdir = null
    ): string {
        $parts = [trim($carpeta, '/')];
        if ($subdir !== null && $subdir !== '') {
            $parts[] = trim((string)$subdir, '/');
        }
        $parts[] = $nombreFinal;
        return implode('/', $parts);
    }

    // ────────────────────────────────────────────────────────────────
    // Extrae la ruta relativa desde el valor guardado en BD
    // Ej: "onedrive:bitacoras/12/bit_12.jpg" → "bitacoras/12/bit_12.jpg"
    // ────────────────────────────────────────────────────────────────
    public static function pathFromRuta(string $rutaBD): string
    {
        return substr($rutaBD, strlen(self::PREFIX));
    }

    // ────────────────────────────────────────────────────────────────
    // Sirve archivo local (compatibilidad con archivos existentes)
    // ────────────────────────────────────────────────────────────────
    private static function servirLocal(
        string $rutaRelativa,
        string $nombreArchivo,
        ?string $mimeForce = null
    ): void {
        $sep          = DIRECTORY_SEPARATOR;
        $rutaAbsoluta = BASE_PATH . $sep . str_replace(['/', '\\'], $sep, $rutaRelativa);

        if (!file_exists($rutaAbsoluta)) {
            http_response_code(404);
            exit('Archivo no encontrado: ' . htmlspecialchars($rutaRelativa));
        }

        $mime = $mimeForce ?? self::guessMime($nombreArchivo);
        header('Content-Type: ' . $mime);
        header('Content-Disposition: inline; filename="' . $nombreArchivo . '"');
        header('Content-Length: ' . filesize($rutaAbsoluta));
        header('Cache-Control: private, max-age=3600');
        readfile($rutaAbsoluta);
        exit;
    }

    // ────────────────────────────────────────────────────────────────
    // Adivina el MIME type por extensión
    // ────────────────────────────────────────────────────────────────
    private static function guessMime(string $filename): string
    {
        $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
        return match($ext) {
            'pdf'        => 'application/pdf',
            'jpg','jpeg' => 'image/jpeg',
            'png'        => 'image/png',
            'webp'       => 'image/webp',
            'docx'       => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
            'xlsx'       => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
            default      => 'application/octet-stream',
        };
    }

    // ────────────────────────────────────────────────────────────────
    // LISTA archivos de una carpeta en OneDrive (para el Explorador)
    // @param string      $carpeta  Ej: 'comprobantes_practicas'
    // @param int|null    $instId   Si se pasa, filtra dentro de inst_N/
    // @param string|null $subdir   Subdirectorio adicional opcional
    // @return array  Cada item: [name, size, modified, downloadUrl, webUrl, mime]
    // ────────────────────────────────────────────────────────────────
    public static function listFolder(
        string $carpeta,
        ?int $instId = null,
        ?string $subdir = null
    ): array {
        $token = self::getToken();

        $ruta = $carpeta;
        if ($instId !== null && $instId > 0) {
            $ruta = 'inst_' . $instId . '/' . ltrim($carpeta, '/');
        }
        if ($subdir) {
            $ruta .= '/' . trim($subdir, '/');
        }

        $url = self::driveUrl($ruta)
             . '/children?$select=name,size,webUrl,@microsoft.graph.downloadUrl,lastModifiedDateTime,file'
             . '&$top=200&$orderby=lastModifiedDateTime desc';

        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER     => [
                'Authorization: Bearer ' . $token,
                'Content-Type: application/json',
            ],
        ]);
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpCode === 404) return [];
        if ($httpCode !== 200) {
            throw new RuntimeException('Error al listar OneDrive. HTTP ' . $httpCode);
        }

        $data  = json_decode($response, true);
        $items = [];
        foreach ($data['value'] ?? [] as $item) {
            if (!isset($item['file'])) continue; // solo archivos, no subcarpetas
            $items[] = [
                'name'        => $item['name'],
                'size'        => $item['size'] ?? 0,
                'webUrl'      => $item['webUrl'] ?? '',
                'downloadUrl' => $item['@microsoft.graph.downloadUrl'] ?? '',
                'modified'    => substr($item['lastModifiedDateTime'] ?? '', 0, 10),
                'mime'        => $item['file']['mimeType'] ?? 'application/octet-stream',
            ];
        }
        return $items;
    }

    // ────────────────────────────────────────────────────────────────
    // LISTA subcarpetas de primer nivel (para navegar por ID de práctica, etc.)
    // ────────────────────────────────────────────────────────────────
    public static function listSubfolders(string $carpeta, ?int $instId = null): array
    {
        $token = self::getToken();

        $ruta = $carpeta;
        if ($instId !== null && $instId > 0) {
            $ruta = 'inst_' . $instId . '/' . ltrim($carpeta, '/');
        }

        $url = self::driveUrl($ruta) . '/children?$select=name,folder&$top=100';

        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER     => [
                'Authorization: Bearer ' . $token,
                'Content-Type: application/json',
            ],
        ]);
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpCode === 404) return [];

        $data    = json_decode($response, true);
        $folders = [];
        foreach ($data['value'] ?? [] as $item) {
            if (isset($item['folder'])) {
                $folders[] = $item['name'];
            }
        }
        return $folders;
    }

}