<?php

declare(strict_types=1);

class MediaStorage
{
    private static ?array $_storages = null;
    private static ?int $_defaultId = null;

    public static function all(): array
    {
        if (self::$_storages === null) {
            self::$_storages = [];
            try {
                $rows = Database::fetchAll("SELECT * FROM media_storages ORDER BY sort_order, id");
                foreach ($rows as $row) {
                    $row['config'] = json_decode($row['config'] ?: '{}', true) ?: [];
                    self::$_storages[(int)$row['id']] = $row;
                }
            } catch (\Throwable) {}
        }
        return self::$_storages;
    }

    public static function get(int $id): ?array
    {
        $all = self::all();
        return $all[$id] ?? null;
    }

    public static function defaultId(): int
    {
        if (self::$_defaultId === null) {
             
            foreach (self::all() as $s) {
                if ($s['is_default']) { self::$_defaultId = (int)$s['id']; break; }
            }
            if (self::$_defaultId === null) {
                $all = self::all();
                self::$_defaultId = !empty($all) ? (int)array_key_first($all) : 0;
            }
        }
        return self::$_defaultId;
    }

    public static function defaultStorage(): ?array
    {
        return self::get(self::defaultId());
    }

    public static function activeDefaultId(): int
    {
        $default = self::defaultId();
        $storage = self::get($default);

         
        if ($storage && ($storage['status'] ?? 'active') === 'active') {
            if (!self::isOverThreshold($storage)) return $default;
        }

         
        foreach (self::all() as $s) {
            if ((int)$s['id'] === $default) continue;
            if (($s['status'] ?? 'active') !== 'active') continue;
            if (!self::isOverThreshold($s)) {
                 
                Database::query('UPDATE media_storages SET is_default = (id = ?)', [(int)$s['id']]);
                self::$_defaultId = (int)$s['id'];
                self::$_storages = null;
                return (int)$s['id'];
            }
        }

         
        return 0;
    }

    public static function isAllFull(): bool
    {
        foreach (self::all() as $s) {
            if (($s['status'] ?? 'active') !== 'active') continue;
            if (!self::isOverThreshold($s)) return false;
        }
        return true;
    }

    

    public static function liveDiskUsage(array $storage): array
    {
        $total = (int)($storage['disk_total'] ?? 0);
        $used  = (int)($storage['disk_used'] ?? 0);
        if (($storage['driver'] ?? 'local') === 'local') {
            $path = APP_ROOT . '/public/uploads/videos';
            $t = @disk_total_space($path);
            $f = @disk_free_space($path);
            if ($t !== false && $f !== false && (float)$t > 0) {
                $total = (int)$t;
                $used  = (int)($t - $f);
            }
        }
        return ['disk_total' => $total, 'disk_used' => $used];
    }

    private static function isOverThreshold(array $storage): bool
    {
        $maxPct = (int)($storage['max_usage_pct'] ?? 90);
        $disk = self::liveDiskUsage($storage);
        $total = $disk['disk_total'];
        $used = $disk['disk_used'];
        if ($total <= 0) return false;  
        $usedPct = round($used / $total * 100);
        return $usedPct >= $maxPct;
    }

    public static function checkHealth(int $storageId): array
    {
        $s = self::get($storageId);
        if (!$s) return ['status' => 'error', 'message' => 'Storage not found'];

        if ($s['driver'] === 'local') {
            return self::checkLocalHealth();
        }

        $cfg = is_array($s['config']) ? $s['config'] : (json_decode($s['config'] ?: '{}', true) ?: []);
        $result = ['status' => 'ok', 'message' => null, 'disk_total' => 0, 'disk_used' => 0];

         
        try {
            if ($s['driver'] === 'ftp') {
                $c = !empty($cfg['ssl']) ? @ftp_ssl_connect($cfg['host'] ?? '', (int)($cfg['port'] ?? 21), 10) : @ftp_connect($cfg['host'] ?? '', (int)($cfg['port'] ?? 21), 10);
                if (!$c) return ['status' => 'error', 'message' => 'Cannot connect to FTP server'];
                if (!@ftp_login($c, $cfg['user'] ?? '', $cfg['pass'] ?? '')) { ftp_close($c); return ['status' => 'error', 'message' => 'FTP login failed']; }
                ftp_close($c);
            } elseif ($s['driver'] === 's3') {
                $ok = self::put(self::createTestFile(), '.tubepress_health_check', $storageId);
                if (!$ok) return ['status' => 'error', 'message' => 'S3 write test failed'];
            } elseif ($s['driver'] === 'webdav') {
                $curl = curl_init(rtrim($cfg['url'] ?? '', '/'));
                curl_setopt_array($curl, [CURLOPT_CUSTOMREQUEST => 'PROPFIND', CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_HTTPHEADER => ['Depth: 0']]);
                if (($cfg['user'] ?? '') !== '') { curl_setopt($curl, CURLOPT_USERPWD, $cfg['user'] . ':' . ($cfg['pass'] ?? '')); }
                curl_exec($curl); $code = (int)curl_getinfo($curl, CURLINFO_HTTP_CODE); curl_close($curl);
                if ($code === 401) return ['status' => 'error', 'message' => 'WebDAV authentication failed'];
                if ($code === 0 || $code >= 500) return ['status' => 'error', 'message' => 'WebDAV server unreachable'];
            }
        } catch (\Throwable $e) {
            return ['status' => 'error', 'message' => $e->getMessage()];
        }

         
        $monitorUrl = $cfg['monitor_url'] ?? '';
        if ($monitorUrl !== '') {
            $monitorResult = self::checkRemoteHealth($monitorUrl, $cfg['monitor_key'] ?? '');
            if (isset($monitorResult['disk_total'])) $result['disk_total'] = $monitorResult['disk_total'];
            if (isset($monitorResult['disk_used'])) $result['disk_used'] = $monitorResult['disk_used'];
             
            if ($monitorResult['status'] === 'error') {
                $result['message'] = 'Monitor: ' . ($monitorResult['message'] ?? 'unreachable');
            } elseif ($monitorResult['status'] === 'warning') {
                $result['status'] = 'warning';
                $result['message'] = $monitorResult['message'];
            }
        }

        return $result;
    }

    private static function createTestFile(): string
    {
        $tmp = tempnam(sys_get_temp_dir(), 'tp');
        file_put_contents($tmp, 'health-check-' . time());
        return $tmp;
    }

    private static function checkLocalHealth(): array
    {
        $path = APP_ROOT . '/public/uploads/videos';
        $webUser = function_exists('posix_getpwuid') && function_exists('posix_geteuid')
            ? (posix_getpwuid(posix_geteuid())['name'] ?? 'www-data') : 'www-data';

         
         
         
        if (!is_dir($path)) {
            @mkdir($path, 0755, true);
        }
        if (!is_dir($path)) {
            return [
                'status'  => 'error',
                'message' => "Local media folder is missing and could not be auto-created: {$path}. "
                           . "Create it and give the web-server user ('{$webUser}') ownership: "
                           . "mkdir -p {$path} && chown -R {$webUser}:{$webUser} " . APP_ROOT . '/public/uploads',
                'disk_total' => 0, 'disk_used' => 0,
            ];
        }

        $total = @disk_total_space($path);
        $free  = @disk_free_space($path);
        if ($total === false || $free === false) {
            $why = !is_readable($path)
                ? "the web-server user ('{$webUser}') cannot read it — fix ownership: chown -R {$webUser}:{$webUser} {$path}"
                : "disk_total_space() returned false — check PHP open_basedir restrictions and that the filesystem under {$path} is mounted";
            return [
                'status'  => 'error',
                'message' => "Cannot read disk info for {$path}: {$why}.",
                'disk_total' => 0, 'disk_used' => 0,
            ];
        }

        $used   = $total - $free;
        $result = ['status' => 'ok', 'message' => null, 'disk_total' => (int)$total, 'disk_used' => (int)$used];

         
        if (!is_writable($path)) {
            $result['status']  = 'warning';
            $result['message'] = "Folder exists but is not writable by the web-server user ('{$webUser}'): {$path}. "
                               . "Run: chown -R {$webUser}:{$webUser} {$path} && chmod -R u+rwX {$path}";
        }
        return $result;
    }

    private static function checkRemoteHealth(string $monitorUrl, string $key): array
    {
        $url = rtrim($monitorUrl, '/') . '?action=status&key=' . urlencode($key);
        $ch = curl_init($url);
        curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_SSL_VERIFYPEER => true]);
        $body = curl_exec($ch);
        $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $err = curl_error($ch);
        curl_close($ch);

        if ($code === 0) return ['status' => 'error', 'message' => 'Cannot connect: ' . ($err ?: 'timeout'), 'disk_total' => 0, 'disk_used' => 0];
        if ($code === 403) return ['status' => 'error', 'message' => 'Authentication failed (wrong key)', 'disk_total' => 0, 'disk_used' => 0];
        if ($code !== 200) return ['status' => 'error', 'message' => "HTTP {$code}", 'disk_total' => 0, 'disk_used' => 0];

        $data = json_decode($body, true);
        if (!$data || !isset($data['ok'])) return ['status' => 'error', 'message' => 'Invalid response', 'disk_total' => 0, 'disk_used' => 0];

        $total = (int)($data['disk_total'] ?? 0);
        $used = (int)($data['disk_used'] ?? 0);
        $writable = $data['write_test'] ?? false;

        $status = 'ok';
        $message = null;

        if (!$writable) { $status = 'error'; $message = 'Disk is not writable'; }
        elseif ($total > 0 && ($used / $total * 100) > 95) { $status = 'warning'; $message = 'Disk almost full (>95%)'; }

        return ['status' => $status, 'message' => $message, 'disk_total' => $total, 'disk_used' => $used];
    }

    public static function mediaUrl(?int $storageId): string
    {
        if ($storageId === null || $storageId === 0) {
            return rtrim(Setting::get('media_url', ''), '/');
        }
        $s = self::get($storageId);
        return $s ? rtrim($s['media_url'], '/') : '';
    }

    public static function put(string $localFile, string $remotePath, ?int $storageId = null): bool
    {
        $s = self::get($storageId ?? self::defaultId());
        if (!$s || $s['driver'] === 'local') return true;

        try {
            $cfg = $s['config'];
            if ($s['driver'] === 'ftp') return self::ftpPut($cfg, $localFile, $remotePath);
            if ($s['driver'] === 's3') return self::s3Put($cfg, $localFile, $remotePath);
            if ($s['driver'] === 'webdav') return self::davPut($cfg, $localFile, $remotePath);
        } catch (\Throwable $e) {
            error_log("MediaStorage put error (storage {$s['name']}): " . $e->getMessage());
        }
        return false;
    }

    public static function putData(string $data, string $remotePath, ?int $storageId = null): bool
    {
        $tmp = tempnam(sys_get_temp_dir(), 'ms');
        file_put_contents($tmp, $data);
        $ok = self::put($tmp, $remotePath, $storageId);
        @unlink($tmp);
        return $ok;
    }

    public static function isRemote(?int $storageId = null): bool
    {
        $s = self::get($storageId ?? self::defaultId());
        return $s && $s['driver'] !== 'local';
    }

    

    public static function exists(string $remotePath, ?int $storageId = null): bool
    {
        $s = self::get($storageId ?? self::defaultId());
        if (!$s) return false;
        if ($s['driver'] === 'local') return true;
        try {
            $cfg = $s['config'];
            if ($s['driver'] === 'ftp') {
                $c = self::ftpConn($cfg);
                $size = @ftp_size($c, ltrim($remotePath, '/'));
                ftp_close($c);
                return $size >= 0;
            }
            if ($s['driver'] === 'webdav') {
                $baseUrl = rtrim($cfg['url'] ?? '', '/');
                $ch = curl_init($baseUrl . '/' . ltrim($remotePath, '/'));
                curl_setopt_array($ch, [CURLOPT_NOBODY => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT => 15, CURLOPT_TIMEOUT => 30, CURLOPT_FOLLOWLOCATION => true]);
                if (($cfg['user'] ?? '') !== '') { curl_setopt($ch, CURLOPT_USERPWD, $cfg['user'] . ':' . ($cfg['pass'] ?? '')); curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); }
                curl_exec($ch);
                $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
                curl_close($ch);
                return $code >= 200 && $code < 300;
            }
            if ($s['driver'] === 's3') {
                return self::s3Exists($cfg, $remotePath);
            }
        } catch (\Throwable $e) {
            error_log("MediaStorage exists error (storage {$s['name']}): " . $e->getMessage());
        }
        return false;
    }

    public static function deleteFile(string $remotePath, ?int $storageId = null): bool
    {
        $s = self::get($storageId ?? self::defaultId());
        if (!$s || $s['driver'] === 'local') return true;

        try {
            $cfg = $s['config'];
            if ($s['driver'] === 'ftp') return self::ftpDelete($cfg, $remotePath);
            if ($s['driver'] === 's3') return self::s3Delete($cfg, $remotePath);
            if ($s['driver'] === 'webdav') return self::davDelete($cfg, $remotePath);
        } catch (\Throwable $e) {
            error_log("MediaStorage delete error (storage {$s['name']}): " . $e->getMessage());
        }
        return false;
    }

    public static function deleteRemoteFolder(string $folder, ?int $storageId = null): bool
    {
        $s = self::get($storageId ?? self::defaultId());
        if (!$s || $s['driver'] === 'local') return true;

        $basePath = 'uploads/videos/' . $folder . '/';

        try {
            $cfg = $s['config'];
            if ($s['driver'] === 'ftp') {
                $c = self::ftpConn($cfg);
                $files = @ftp_nlist($c, $basePath);
                if ($files) {
                    foreach ($files as $f) @ftp_delete($c, $f);
                }
                @ftp_rmdir($c, $basePath);
                ftp_close($c);
                return true;
            }
            if ($s['driver'] === 's3') {
                foreach (['video.mp4','video.webm','video.ogg','thumb.jpg','thumb_small.jpg','preview.mp4','timeline.jpg','timeline.vtt'] as $f) {
                    self::s3Delete($cfg, $basePath . $f);
                }
                foreach (['2160', '1440', '1080', '720', '480', '360'] as $h) {
                    self::s3Delete($cfg, $basePath . "video_{$h}p.mp4");
                }
                return true;
            }
            if ($s['driver'] === 'webdav') {
                $baseUrl = rtrim($cfg['url'] ?? '', '/');
                $dirUrl = $baseUrl . '/' . rtrim($basePath, '/');
                 
                 
                 
                 
                $members = self::davList($dirUrl, $cfg);
                if (!empty($members)) {
                    foreach ($members as $m) self::davReq('DELETE', $m, $cfg);
                } else {
                    foreach (['video.mp4','video.webm','video.ogg','thumb.jpg','thumb_small.jpg','preview.mp4','timeline.jpg','timeline.vtt'] as $f) {
                        self::davReq('DELETE', $baseUrl . '/' . $basePath . $f, $cfg);
                    }
                    foreach (['2160','1440','1080','720','480','360'] as $h) {
                        self::davReq('DELETE', $baseUrl . '/' . $basePath . "video_{$h}p.mp4", $cfg);
                    }
                }
                self::davReq('DELETE', $dirUrl, $cfg);  
                return true;
            }
        } catch (\Throwable $e) {
            error_log("MediaStorage deleteFolder error (storage {$s['name']}): " . $e->getMessage());
        }
        return false;
    }

    public static function resetCache(): void
    {
        self::$_storages = null;
        self::$_defaultId = null;
    }

     

    private static function ftpConn(array $cfg): mixed
    {
        if (!function_exists('ftp_connect')) {
            throw new \RuntimeException('The PHP "ftp" extension is required to use an FTP storage backend. Install ext-ftp and reload PHP.');
        }
        $host = $cfg['host'] ?? '';
        $port = (int)($cfg['port'] ?? 21);
        $user = $cfg['user'] ?? '';
        $pass = $cfg['pass'] ?? '';
        $ssl = !empty($cfg['ssl']);

        $c = $ssl ? @ftp_ssl_connect($host, $port, 15) : @ftp_connect($host, $port, 15);
        if (!$c) throw new \RuntimeException("FTP connect failed: {$host}:{$port}");
        if (!@ftp_login($c, $user, $pass)) { ftp_close($c); throw new \RuntimeException('FTP login failed'); }
        if ($cfg['passive'] ?? true) ftp_pasv($c, true);

        $path = $cfg['path'] ?? '/';
        if ($path !== '' && $path !== '/') @ftp_chdir($c, $path);

        return $c;
    }

    private static function ftpPut(array $cfg, string $local, string $remote): bool
    {
        $c = self::ftpConn($cfg);
        $parts = explode('/', trim(dirname($remote), '/'));
        $cur = '';
        foreach ($parts as $p) { if ($p === '') continue; $cur .= '/' . $p; @ftp_mkdir($c, $cur); }
        $ok = @ftp_put($c, $remote, $local, FTP_BINARY);
        ftp_close($c);
        return $ok;
    }

    private static function ftpDelete(array $cfg, string $remote): bool
    {
        $c = self::ftpConn($cfg);
        $ok = @ftp_delete($c, $remote);
        ftp_close($c);
        return $ok;
    }

     

    private static function s3Put(array $cfg, string $local, string $remote): bool
    {
        $key = ($cfg['prefix'] ?? '') !== '' ? rtrim($cfg['prefix'], '/') . '/' . ltrim($remote, '/') : ltrim($remote, '/');
        $ep = ($cfg['endpoint'] ?? '') !== '' ? rtrim($cfg['endpoint'], '/') : "https://{$cfg['bucket']}.s3.{$cfg['region']}.amazonaws.com";
        $path = '/' . ltrim($key, '/');
        if (($cfg['endpoint'] ?? '') !== '') $path = '/' . $cfg['bucket'] . $path;
        $host = parse_url($ep, PHP_URL_HOST);
        $date = gmdate('Ymd\THis\Z');
        $ds = gmdate('Ymd');

        $ext = strtolower(pathinfo($remote, PATHINFO_EXTENSION));
        $mimeMap = ['mp4' => 'video/mp4', 'webm' => 'video/webm', 'ogg' => 'video/ogg', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif', 'webp' => 'image/webp', 'vtt' => 'text/vtt'];
        $contentType = $mimeMap[$ext] ?? 'application/octet-stream';

        $hdrs = ['content-type' => $contentType, 'host' => $host, 'x-amz-content-sha256' => 'UNSIGNED-PAYLOAD', 'x-amz-date' => $date];
        ksort($hdrs);
        $sh = implode(';', array_keys($hdrs));
        $ch = ''; foreach ($hdrs as $k => $v) $ch .= "{$k}:{$v}\n";
        $cr = "PUT\n{$path}\n\n{$ch}\n{$sh}\nUNSIGNED-PAYLOAD";
        $scope = "{$ds}/{$cfg['region']}/s3/aws4_request";
        $sts = "AWS4-HMAC-SHA256\n{$date}\n{$scope}\n" . hash('sha256', $cr);
        $sk = hash_hmac('sha256', $ds, 'AWS4' . $cfg['secret'], true);
        $sk = hash_hmac('sha256', $cfg['region'], $sk, true);
        $sk = hash_hmac('sha256', 's3', $sk, true);
        $sk = hash_hmac('sha256', 'aws4_request', $sk, true);
        $sig = hash_hmac('sha256', $sts, $sk);
        $auth = "AWS4-HMAC-SHA256 Credential={$cfg['key']}/{$scope}, SignedHeaders={$sh}, Signature={$sig}";

        $fh = fopen($local, 'rb');
        $curl = curl_init($ep . $path);
        curl_setopt_array($curl, [CURLOPT_CUSTOMREQUEST => 'PUT', CURLOPT_HTTPHEADER => ["Authorization: {$auth}", "Content-Type: {$contentType}", "x-amz-content-sha256: UNSIGNED-PAYLOAD", "x-amz-date: {$date}"], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 300, CURLOPT_UPLOAD => true, CURLOPT_INFILE => $fh, CURLOPT_INFILESIZE => filesize($local)]);
        curl_exec($curl);
        $code = (int)curl_getinfo($curl, CURLINFO_HTTP_CODE);
        curl_close($curl);
        fclose($fh);
        return $code >= 200 && $code < 300;
    }

    private static function s3Delete(array $cfg, string $remote): bool
    {
        $key = ($cfg['prefix'] ?? '') !== '' ? rtrim($cfg['prefix'], '/') . '/' . ltrim($remote, '/') : ltrim($remote, '/');
        $ep = ($cfg['endpoint'] ?? '') !== '' ? rtrim($cfg['endpoint'], '/') : "https://{$cfg['bucket']}.s3.{$cfg['region']}.amazonaws.com";
        $path = '/' . ltrim($key, '/');
        if (($cfg['endpoint'] ?? '') !== '') $path = '/' . $cfg['bucket'] . $path;
        $host = parse_url($ep, PHP_URL_HOST);
        $date = gmdate('Ymd\THis\Z');
        $ds = gmdate('Ymd');
        $hdrs = ['host' => $host, 'x-amz-content-sha256' => hash('sha256', ''), 'x-amz-date' => $date];
        ksort($hdrs);
        $sh = implode(';', array_keys($hdrs));
        $ch = ''; foreach ($hdrs as $k => $v) $ch .= "{$k}:{$v}\n";
        $cr = "DELETE\n{$path}\n\n{$ch}\n{$sh}\n" . hash('sha256', '');
        $scope = "{$ds}/{$cfg['region']}/s3/aws4_request";
        $sts = "AWS4-HMAC-SHA256\n{$date}\n{$scope}\n" . hash('sha256', $cr);
        $sk = hash_hmac('sha256', $ds, 'AWS4' . $cfg['secret'], true);
        $sk = hash_hmac('sha256', $cfg['region'], $sk, true);
        $sk = hash_hmac('sha256', 's3', $sk, true);
        $sk = hash_hmac('sha256', 'aws4_request', $sk, true);
        $sig = hash_hmac('sha256', $sts, $sk);
        $auth = "AWS4-HMAC-SHA256 Credential={$cfg['key']}/{$scope}, SignedHeaders={$sh}, Signature={$sig}";
        $curl = curl_init($ep . $path);
        curl_setopt_array($curl, [CURLOPT_CUSTOMREQUEST => 'DELETE', CURLOPT_HTTPHEADER => ["Authorization: {$auth}", "x-amz-content-sha256: " . hash('sha256', ''), "x-amz-date: {$date}"], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30]);
        curl_exec($curl);
        $code = (int)curl_getinfo($curl, CURLINFO_HTTP_CODE);
        curl_close($curl);
        return $code >= 200 && $code < 300 || $code === 404;
    }

    private static function s3Exists(array $cfg, string $remote): bool
    {
        $key = ($cfg['prefix'] ?? '') !== '' ? rtrim($cfg['prefix'], '/') . '/' . ltrim($remote, '/') : ltrim($remote, '/');
        $ep = ($cfg['endpoint'] ?? '') !== '' ? rtrim($cfg['endpoint'], '/') : "https://{$cfg['bucket']}.s3.{$cfg['region']}.amazonaws.com";
        $path = '/' . ltrim($key, '/');
        if (($cfg['endpoint'] ?? '') !== '') $path = '/' . $cfg['bucket'] . $path;
        $host = parse_url($ep, PHP_URL_HOST);
        $date = gmdate('Ymd\THis\Z');
        $ds = gmdate('Ymd');
        $hdrs = ['host' => $host, 'x-amz-content-sha256' => hash('sha256', ''), 'x-amz-date' => $date];
        ksort($hdrs);
        $sh = implode(';', array_keys($hdrs));
        $ch = ''; foreach ($hdrs as $k => $v) $ch .= "{$k}:{$v}\n";
        $cr = "HEAD\n{$path}\n\n{$ch}\n{$sh}\n" . hash('sha256', '');
        $scope = "{$ds}/{$cfg['region']}/s3/aws4_request";
        $sts = "AWS4-HMAC-SHA256\n{$date}\n{$scope}\n" . hash('sha256', $cr);
        $sk = hash_hmac('sha256', $ds, 'AWS4' . $cfg['secret'], true);
        $sk = hash_hmac('sha256', $cfg['region'], $sk, true);
        $sk = hash_hmac('sha256', 's3', $sk, true);
        $sk = hash_hmac('sha256', 'aws4_request', $sk, true);
        $sig = hash_hmac('sha256', $sts, $sk);
        $auth = "AWS4-HMAC-SHA256 Credential={$cfg['key']}/{$scope}, SignedHeaders={$sh}, Signature={$sig}";
        $curl = curl_init($ep . $path);
        curl_setopt_array($curl, [CURLOPT_NOBODY => true, CURLOPT_HTTPHEADER => ["Authorization: {$auth}", "x-amz-content-sha256: " . hash('sha256', ''), "x-amz-date: {$date}"], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30]);
        curl_exec($curl);
        $code = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE);
        curl_close($curl);
        return $code >= 200 && $code < 300;
    }

    private static function davDelete(array $cfg, string $remote): bool
    {
        $baseUrl = rtrim($cfg['url'] ?? '', '/');
        $code = self::davReq('DELETE', $baseUrl . '/' . ltrim($remote, '/'), $cfg);
        return $code >= 200 && $code < 300 || $code === 404;
    }

     

    private static function davPut(array $cfg, string $local, string $remote): bool
    {
        $baseUrl = rtrim($cfg['url'] ?? '', '/');

         
         
         
         
        if ((@filesize($local) ?: 0) > 95 * 1024 * 1024) {
            [$chunkEndpoint, $chunkRoot] = self::davChunkTarget($baseUrl);
            if ($chunkEndpoint !== null && self::davPutChunked($chunkEndpoint, $chunkRoot, $remote, $local, $cfg)) {
                return true;
            }
            error_log('MediaStorage: chunked upload unavailable/failed for ' . $remote . ' — trying direct PUT');
        }

        $parts = explode('/', trim(dirname($remote), '/'));
        $cur = '';
        foreach ($parts as $p) {
            if ($p === '') continue;
            $cur .= '/' . $p;
            self::davReq('MKCOL', $baseUrl . $cur, $cfg);
        }
        $r = self::davReq('PUT', $baseUrl . '/' . ltrim($remote, '/'), $cfg, $local);
        return $r >= 200 && $r < 300;
    }

    

    private static function davChunkTarget(string $baseUrl): array
    {
        $p = parse_url($baseUrl);
        if (!$p || empty($p['host'])) return [null, null];
        $origin = ($p['scheme'] ?? 'https') . '://' . $p['host'] . (isset($p['port']) ? ':' . $p['port'] : '');
        $path = rtrim($p['path'] ?? '', '/');
        if ($path === '/dav') return [$origin . '/tp-upload.php', 'main'];
        if (preg_match('#^/([^/]+)/dav$#', $path, $m)) return [$origin . '/tp-upload.php', $m[1]];
        return [null, null];
    }

    

    private static function davPutChunked(string $endpoint, string $root, string $remote, string $local, array $cfg): bool
    {
        $size = (int) (@filesize($local) ?: 0);
        if ($size <= 0) return false;
        $fh = @fopen($local, 'rb');
        if (!$fh) return false;
        $chunkSize = 90 * 1024 * 1024;  
        $ok = true;
        for ($offset = 0; $offset < $size; $offset += $chunkSize) {
            $len = (int) min($chunkSize, $size - $offset);
            fseek($fh, $offset);
            $remaining = $len;
            $ch = curl_init($endpoint);
            curl_setopt_array($ch, [
                CURLOPT_UPLOAD         => true,             
                CURLOPT_CUSTOMREQUEST  => 'POST',          
                CURLOPT_INFILESIZE     => $len,
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_READFUNCTION   => function ($c, $stream, $maxlen) use ($fh, &$remaining) {
                    if ($remaining <= 0) return '';
                    $buf = fread($fh, (int) min($maxlen, $remaining));
                    if ($buf === false) return '';
                    $remaining -= strlen($buf);
                    return $buf;
                },
                CURLOPT_HTTPHEADER     => [
                    'X-TP-Root: ' . $root,
                    'X-TP-Path: ' . ltrim($remote, '/'),
                    'X-TP-Offset: ' . $offset,
                    'X-TP-Total: ' . $size,
                    'Content-Type: application/octet-stream',
                    'Expect:',
                ],
                CURLOPT_CONNECTTIMEOUT  => 15,
                CURLOPT_TIMEOUT         => 1800,
                CURLOPT_LOW_SPEED_LIMIT => 1024,
                CURLOPT_LOW_SPEED_TIME  => 120,
            ]);
            if (($cfg['user'] ?? '') !== '') {
                curl_setopt($ch, CURLOPT_USERPWD, $cfg['user'] . ':' . ($cfg['pass'] ?? ''));
                curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
            }
            $resp = curl_exec($ch);
            $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);
            if ($code < 200 || $code >= 300) {
                error_log("MediaStorage davPutChunked: HTTP $code at offset $offset for $remote :: " . substr((string) $resp, 0, 200));
                $ok = false;
                break;
            }
        }
        fclose($fh);
        return $ok;
    }

    

    private static function davList(string $dirUrl, array $cfg): array
    {
        $ch = curl_init($dirUrl);
        curl_setopt_array($ch, [
            CURLOPT_CUSTOMREQUEST => 'PROPFIND', CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT => 15, CURLOPT_TIMEOUT => 30,
            CURLOPT_HTTPHEADER => ['Depth: 1'], CURLOPT_FOLLOWLOCATION => true,
        ]);
        if (($cfg['user'] ?? '') !== '') {
            curl_setopt($ch, CURLOPT_USERPWD, $cfg['user'] . ':' . ($cfg['pass'] ?? ''));
            curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
        }
        $body = curl_exec($ch);
        $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        if ($code < 200 || $code >= 300 || !is_string($body) || $body === '') return [];
        if (!preg_match_all('#<[a-z0-9]*:?href>\s*([^<]+?)\s*</[a-z0-9]*:?href>#i', $body, $m)) return [];

        $scheme = parse_url($dirUrl, PHP_URL_SCHEME); $host = parse_url($dirUrl, PHP_URL_HOST);
        $port = parse_url($dirUrl, PHP_URL_PORT);
        $origin = $scheme . '://' . $host . ($port ? ':' . $port : '');
        $dirPath = rtrim((string) parse_url($dirUrl, PHP_URL_PATH), '/');

        $out = [];
        foreach ($m[1] as $href) {
            $href = html_entity_decode($href, ENT_QUOTES);
            $path = rtrim((string) (parse_url($href, PHP_URL_PATH) ?: $href), '/');
            if ($path === '' || $path === $dirPath) continue;           
            if (rtrim(str_replace('\\', '/', dirname($path)), '/') !== $dirPath) continue;  
            $out[] = $origin . $path;
        }
        return $out;
    }

    private static function davReq(string $method, string $url, array $cfg, ?string $file = null): int
    {
        $curl = curl_init($url);
         
         
         
         
         
         
        curl_setopt_array($curl, [
            CURLOPT_CUSTOMREQUEST  => $method,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT => 15,
            CURLOPT_TIMEOUT        => 3600,
            CURLOPT_LOW_SPEED_LIMIT => 1024,
            CURLOPT_LOW_SPEED_TIME  => 120,
            CURLOPT_FOLLOWLOCATION => true,
        ]);
        if (($cfg['user'] ?? '') !== '') {
            curl_setopt($curl, CURLOPT_USERPWD, $cfg['user'] . ':' . ($cfg['pass'] ?? ''));
             
             
             
             
             
            curl_setopt($curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
        }
        if ($method === 'PUT' && $file) {
            $fh = fopen($file, 'rb');
            curl_setopt($curl, CURLOPT_UPLOAD, true);
            curl_setopt($curl, CURLOPT_INFILE, $fh);
            curl_setopt($curl, CURLOPT_INFILESIZE, filesize($file));
        }
        curl_exec($curl);
        $code = (int)curl_getinfo($curl, CURLINFO_HTTP_CODE);
        curl_close($curl);
        if (isset($fh) && is_resource($fh)) fclose($fh);
        return $code;
    }
}
