<?php

declare(strict_types=1);

namespace PpPhp\Core\Storage;

/**
 * OneDriveDriver — store File/Image uploads in a connected Microsoft OneDrive
 * (or SharePoint) via the Microsoft Graph API.
 *
 * OAuth2 (Files.ReadWrite + offline_access) with a stored refresh token. Graph
 * is path-based, so the per-field sub-folder maps directly:
 * root:/<folder>/YYYY/MM/<hash>.<ext>. The ref's `key` is the Graph item id;
 * url() returns the item's short-lived pre-authenticated @microsoft.graph.
 * downloadUrl (no need to make the file public). Small files (<4 MB) use a
 * single PUT; larger files should use an upload session (future).
 *
 * Connection config: clientId, clientSecret, refreshToken, folder (base path).
 */
final class OneDriveDriver implements StorageDriver
{
    private array $conn;
    private string $folder;
    private const GRAPH = 'https://graph.microsoft.com/v1.0/me/drive';
    private const DENY = ['php', 'phtml', 'phar', 'pht', 'cgi', 'pl', 'sh', 'exe'];

    public function __construct(array $config = [])
    {
        $this->conn = $config;
        $this->folder = LocalDriver::sanitizeFolder((string) ($config['folder'] ?? ''));
    }

    public static function id(): string { return 'onedrive'; }

    private function token(): string { return StorageOAuth::accessToken('onedrive', $this->conn); }

    public function put(string $tmpPath, array $meta): array
    {
        $token = $this->token();
        if ($token === '') throw new \RuntimeException('OneDrive is not connected.');
        $name = (string) ($meta['name'] ?? basename($tmpPath));
        $ext  = strtolower((string) ($meta['ext'] ?? pathinfo($name, PATHINFO_EXTENSION)));
        if ($ext === '' || in_array($ext, self::DENY, true)) throw new \RuntimeException('File type is not allowed.');
        $mime = (string) ($meta['mime'] ?? 'application/octet-stream') ?: 'application/octet-stream';
        $body = @file_get_contents($tmpPath);
        if ($body === false) throw new \RuntimeException('Could not read the upload.');
        if (strlen($body) > 4 * 1024 * 1024) {
            throw new \RuntimeException('File is larger than 4 MB — OneDrive large-file upload sessions are not enabled yet.');
        }

        $stem = bin2hex(random_bytes(8));
        $safeExt = preg_replace('/[^a-z0-9]/', '', $ext) ?: 'bin';
        // Flat, predictable path: [<folder>/]<hash>.<ext> (no date nesting).
        $rel = ($this->folder !== '' ? $this->folder . '/' : '') . $stem . '.' . $safeExt;
        $path = implode('/', array_map('rawurlencode', explode('/', $rel)));

        [$status, $resp] = $this->http('PUT', self::GRAPH . "/root:/$path:/content", $token, $body, $mime);
        $json = json_decode($resp, true);
        if ($status < 200 || $status >= 300 || empty($json['id'])) {
            throw new \RuntimeException('OneDrive upload failed (HTTP ' . $status . ').');
        }
        return StorageRef::normalise(['v' => StorageRef::VERSION, 'driver' => 'onedrive', 'key' => (string) $json['id'],
            'name' => $name, 'size' => (int) ($meta['size'] ?? strlen($body)), 'mime' => $mime]
            + (isset($meta['w']) ? ['w' => (int) $meta['w'], 'h' => (int) ($meta['h'] ?? 0)] : []));
    }

    public function url(array $ref, array $opts = []): string
    {
        if (($ref['url'] ?? '') !== '') return (string) $ref['url'];
        $token = $this->token();
        $id = (string) ($ref['key'] ?? '');
        if ($token === '' || $id === '') return '';
        // The item's pre-authenticated download URL (short-lived, no login needed).
        [$status, $resp] = $this->http('GET', self::GRAPH . "/items/$id?select=id,@microsoft.graph.downloadUrl", $token, null);
        if ($status < 200 || $status >= 300) return '';
        $json = json_decode($resp, true);
        return (string) ($json['@microsoft.graph.downloadUrl'] ?? '');
    }

    public function openStream(array $ref)
    {
        $token = $this->token();
        $id = (string) ($ref['key'] ?? '');
        if ($token === '' || $id === '') return null;
        [$status, $body] = $this->http('GET', self::GRAPH . "/items/$id/content", $token, null);
        if ($status < 200 || $status >= 300) return null;
        $fh = fopen('php://temp', 'r+b'); fwrite($fh, (string) $body); rewind($fh); return $fh;
    }

    public function delete(array $ref): bool
    {
        $token = $this->token();
        $id = (string) ($ref['key'] ?? '');
        if ($token === '' || $id === '') return false;
        [$status] = $this->http('DELETE', self::GRAPH . "/items/$id", $token, null);
        return ($status >= 200 && $status < 300) || $status === 404;
    }

    public function exists(array $ref): bool
    {
        $token = $this->token();
        $id = (string) ($ref['key'] ?? '');
        if ($token === '' || $id === '') return false;
        [$status] = $this->http('GET', self::GRAPH . "/items/$id?select=id", $token, null);
        return $status >= 200 && $status < 300;
    }

    /** Verify the account is connected (a refresh token that mints an access token). @return array{ok:bool,message:string} */
    public function test(): array
    {
        if ((string) ($this->conn['clientId'] ?? '') === '') return ['ok' => false, 'message' => 'Client ID is required.'];
        if ((string) ($this->conn['refreshToken'] ?? '') === '') return ['ok' => false, 'message' => 'Not connected yet — click Connect to authorize the account.'];
        return $this->token() !== '' ? ['ok' => true, 'message' => 'Connected to OneDrive.'] : ['ok' => false, 'message' => 'Could not refresh the access token — reconnect the account.'];
    }

    public function capabilities(): array
    {
        return ['directUpload' => false, 'publicUrl' => false, 'signedUrl' => true, 'chunked' => false, 'maxBytes' => 4 * 1024 * 1024];
    }

    /** @return array{0:int,1:string} */
    private function http(string $method, string $url, string $token, ?string $body, string $contentType = ''): array
    {
        if (!function_exists('curl_init')) return [0, ''];
        $headers = ['Authorization: Bearer ' . $token];
        if ($contentType !== '') $headers[] = 'Content-Type: ' . $contentType;
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_CUSTOMREQUEST => $method, CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => $headers, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => 120,
        ]);
        if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
        $resp = curl_exec($ch);
        $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        return [$status, is_string($resp) ? $resp : ''];
    }
}
