<?php

namespace App\Services\Marketplace;

use App\Models\MarketplaceInstalled;
use App\Models\Setting;

/**
 * ITSWEBER CMS – Marketplace Service
 *
 * HTTP client for the Marketplace registry API.
 * Uses PHP streams (no cURL dependency).
 * Caches registry responses in storage/cache/.
 */
class MarketplaceService
{
    private string $basePath;
    private string $baseUrl;
    private int $cacheTtl;

    public function __construct(string $basePath)
    {
        $this->basePath = $basePath;
        $this->baseUrl  = rtrim(Setting::get('marketplace_url', ''), '/');
        $this->cacheTtl = (int) Setting::get('marketplace_cache_ttl', '3600');
    }

    // ─────────────────────────────────────────────────────────
    // Public API
    // ─────────────────────────────────────────────────────────

    /**
     * Check if marketplace URL is configured.
     */
    public function isConfigured(): bool
    {
        return $this->baseUrl !== '';
    }

    /**
     * Return the full registry endpoint URL (base + /api/v1/registry).
     */
    public function getRegistryUrl(): string
    {
        return $this->baseUrl . '/api/v1/registry';
    }

    /**
     * Fetch the full package registry.
     *
     * @return array{success: bool, packages: array, from_cache: bool, error: ?string}
     */
    public function fetchRegistry(): array
    {
        if (!$this->isConfigured()) {
            return ['success' => false, 'packages' => [], 'from_cache' => false, 'error' => 'not_configured'];
        }

        // Try cache first
        $cached = $this->getFromCache('registry');
        if ($cached !== null) {
            return ['success' => true, 'packages' => $cached['packages'] ?? [], 'from_cache' => true, 'error' => null];
        }

        // Fetch from server
        $response = $this->httpGet($this->baseUrl . '/api/v1/registry');

        if ($response['error'] !== null) {
            // Fallback: try stale cache
            $stale = $this->getFromCache('registry', true);
            if ($stale !== null) {
                return [
                    'success'    => true,
                    'packages'   => $stale['packages'] ?? [],
                    'from_cache' => true,
                    'error'      => 'server_unreachable',
                ];
            }
            return ['success' => false, 'packages' => [], 'from_cache' => false, 'error' => $response['error']];
        }

        $data = json_decode($response['body'], true);
        if (!is_array($data) || empty($data['success'])) {
            return ['success' => false, 'packages' => [], 'from_cache' => false, 'error' => 'invalid_response'];
        }

        $packages = $data['data']['packages'] ?? [];
        $this->writeToCache('registry', ['packages' => $packages]);

        return ['success' => true, 'packages' => $packages, 'from_cache' => false, 'error' => null];
    }

    /**
     * Fetch detail for a single package.
     */
    public function fetchPackageDetail(string $slug): ?array
    {
        if (!$this->isConfigured()) {
            return null;
        }

        $cacheKey = 'detail_' . $slug;
        $cached = $this->getFromCache($cacheKey);
        if ($cached !== null) {
            return $cached;
        }

        $response = $this->httpGet($this->baseUrl . '/api/v1/registry/' . urlencode($slug));

        if ($response['error'] !== null) {
            $stale = $this->getFromCache($cacheKey, true);
            return $stale;
        }

        $data = json_decode($response['body'], true);
        if (!is_array($data) || empty($data['success'])) {
            return null;
        }

        $package = $data['data'] ?? null;
        if ($package !== null) {
            $this->writeToCache($cacheKey, $package);
        }

        return $package;
    }

    /**
     * Download a package ZIP to storage/temp/.
     *
     * @return array{success: bool, path: ?string, error: ?string}
     */
    public function downloadPackage(string $slug, string $version, string $licenseKey = ''): array
    {
        if (!$this->isConfigured()) {
            return ['success' => false, 'path' => null, 'error' => 'not_configured'];
        }

        $url = $this->baseUrl . '/api/v1/registry/' . urlencode($slug) . '/download'
             . '?v=' . urlencode($version);

        if ($licenseKey !== '') {
            $url .= '&key=' . urlencode($licenseKey);
        }

        $tempDir = $this->basePath . '/storage/temp';
        if (!is_dir($tempDir)) {
            mkdir($tempDir, 0755, true);
        }

        $destPath = $tempDir . '/mp_' . preg_replace('/[^a-z0-9\-]/', '', $slug) . '_' . bin2hex(random_bytes(4)) . '.zip';

        $ok = $this->httpDownload($url, $destPath, 120);
        if (!$ok) {
            @unlink($destPath);
            return ['success' => false, 'path' => null, 'error' => 'download_failed'];
        }

        // Basic validation: file exists and has content
        if (!is_file($destPath) || filesize($destPath) < 100) {
            @unlink($destPath);
            return ['success' => false, 'path' => null, 'error' => 'download_empty'];
        }

        // Check if server returned JSON error instead of ZIP
        $header = file_get_contents($destPath, false, null, 0, 2);
        if ($header !== 'PK') {
            // Read potential JSON error
            $content = file_get_contents($destPath);
            @unlink($destPath);
            $json = json_decode($content, true);
            $errorMsg = $json['error'] ?? 'invalid_download';
            return ['success' => false, 'path' => null, 'error' => $errorMsg];
        }

        return ['success' => true, 'path' => $destPath, 'error' => null];
    }

    /**
     * Check installed packages against registry for available updates.
     *
     * @return array List of packages with updates available
     */
    public function checkForUpdates(): array
    {
        $registry = $this->fetchRegistry();
        if (!$registry['success'] || empty($registry['packages'])) {
            return [];
        }

        $installed = MarketplaceInstalled::getInstalledMap();
        $updates = [];

        foreach ($registry['packages'] as $pkg) {
            $slug = $pkg['slug'] ?? '';
            if (!isset($installed[$slug])) {
                continue;
            }

            $localVersion  = $installed[$slug]['version'] ?? '0.0.0';
            $remoteVersion = $pkg['version'] ?? '0.0.0';

            if (version_compare($remoteVersion, $localVersion, '>')) {
                MarketplaceInstalled::setRemoteVersion($slug, $remoteVersion);
                $updates[] = array_merge($pkg, [
                    'installed_version' => $localVersion,
                ]);
            } else {
                // Clear stale update flag
                MarketplaceInstalled::setRemoteVersion($slug, null);
            }
        }

        return $updates;
    }

    /**
     * Clear all marketplace cache files.
     */
    public function clearCache(): void
    {
        $cacheDir = $this->basePath . '/storage/cache';
        if (!is_dir($cacheDir)) {
            return;
        }

        foreach (glob($cacheDir . '/marketplace_*.json') as $file) {
            @unlink($file);
        }
    }

    // ─────────────────────────────────────────────────────────
    // HTTP layer (private)
    // ─────────────────────────────────────────────────────────

    /**
     * GET request via PHP streams.
     *
     * @return array{status: int, body: string, error: ?string}
     */
    private function httpGet(string $url, int $timeout = 15): array
    {
        $context = stream_context_create([
            'http' => [
                'method'        => 'GET',
                'timeout'       => $timeout,
                'ignore_errors' => true,
                'header'        => "Accept: application/json\r\nUser-Agent: ITSWEBER-CMS/" . cms_version() . "\r\n",
            ],
            'ssl' => [
                'verify_peer'      => true,
                'verify_peer_name' => true,
            ],
        ]);

        $body = @file_get_contents($url, false, $context);

        if ($body === false) {
            return ['status' => 0, 'body' => '', 'error' => 'connection_failed'];
        }

        // Parse status code from $http_response_header
        $status = 200;
        if (isset($http_response_header) && is_array($http_response_header)) {
            foreach ($http_response_header as $header) {
                if (preg_match('/^HTTP\/[\d.]+ (\d{3})/', $header, $m)) {
                    $status = (int) $m[1];
                }
            }
        }

        if ($status >= 400) {
            return ['status' => $status, 'body' => $body, 'error' => 'http_error_' . $status];
        }

        return ['status' => $status, 'body' => $body, 'error' => null];
    }

    /**
     * Download a file to disk, preferring cURL for robust chunked streaming.
     * Falls back to PHP streams when cURL is not available.
     * Writes to temp file first, then renames (atomic).
     *
     * Progress is stored in storage/temp/mp_progress_{token}.json so the
     * admin UI can poll it (future SSE integration).
     */
    private function httpDownload(string $url, string $destPath, int $timeout = 120): bool
    {
        return extension_loaded('curl')
            ? $this->httpDownloadCurl($url, $destPath, $timeout)
            : $this->httpDownloadStream($url, $destPath, $timeout);
    }

    /**
     * cURL-based chunked download with progress tracking.
     */
    private function httpDownloadCurl(string $url, string $destPath, int $timeout): bool
    {
        $tempPath     = $destPath . '.tmp';
        $progressFile = $destPath . '.progress';
        $maxBytes     = 50 * 1024 * 1024; // 50 MB safety limit

        $dst = @fopen($tempPath, 'wb');
        if ($dst === false) {
            return false;
        }

        $written     = 0;
        $httpStatus  = 0;
        $errorBody   = '';
        $isErrorBody = false;

        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_TIMEOUT         => $timeout,
            CURLOPT_CONNECTTIMEOUT  => 10,
            CURLOPT_FOLLOWLOCATION  => true,
            CURLOPT_MAXREDIRS       => 3,
            CURLOPT_SSL_VERIFYPEER  => true,
            CURLOPT_SSL_VERIFYHOST  => 2,
            CURLOPT_USERAGENT       => 'ITSWEBER-CMS/' . cms_version(),
            CURLOPT_ENCODING        => '',
            CURLOPT_NOPROGRESS      => false,
            // Write header callback – captures HTTP status, detects error responses
            CURLOPT_HEADERFUNCTION  => function ($ch, $header) use (&$httpStatus, &$isErrorBody) {
                if (preg_match('/^HTTP\/[\d.]+ (\d{3})/', $header, $m)) {
                    $httpStatus = (int) $m[1];
                    $isErrorBody = $httpStatus >= 400;
                }
                return strlen($header);
            },
            // Write callback – switches to error-body buffering on 4xx/5xx
            CURLOPT_WRITEFUNCTION   => function ($ch, $chunk) use ($dst, &$written, &$errorBody, &$isErrorBody, $maxBytes) {
                if ($isErrorBody) {
                    $errorBody .= $chunk; // buffer small error JSON
                    return strlen($chunk);
                }
                $written += strlen($chunk);
                if ($written > $maxBytes) {
                    return 0; // abort transfer
                }
                fwrite($dst, $chunk);
                return strlen($chunk);
            },
            // Progress callback – writes lightweight progress file
            CURLOPT_PROGRESSFUNCTION => function ($ch, $dlTotal, $dlNow) use ($progressFile) {
                if ($dlTotal > 0) {
                    $pct = round(($dlNow / $dlTotal) * 100, 1);
                    @file_put_contents($progressFile, json_encode([
                        'total'   => $dlTotal,
                        'current' => $dlNow,
                        'pct'     => $pct,
                        'ts'      => time(),
                    ]));
                }
                return 0;
            },
        ]);

        $curlOk = curl_exec($ch);
        curl_close($ch);
        fclose($dst);
        @unlink($progressFile);

        if (!$curlOk || $written >= $maxBytes) {
            @unlink($tempPath);
            return false;
        }

        if ($isErrorBody || $httpStatus >= 400) {
            // Write error body for caller to parse
            @file_put_contents($destPath, $errorBody);
            @unlink($tempPath);
            return false;
        }

        if (is_file($destPath)) {
            @unlink($destPath);
        }
        return rename($tempPath, $destPath);
    }

    /**
     * PHP-stream fallback download (chunked, memory-efficient).
     */
    private function httpDownloadStream(string $url, string $destPath, int $timeout): bool
    {
        $context = stream_context_create([
            'http' => [
                'method'        => 'GET',
                'timeout'       => $timeout,
                'ignore_errors' => true,
                'header'        => "User-Agent: ITSWEBER-CMS/" . cms_version() . "\r\n",
            ],
            'ssl' => [
                'verify_peer'      => true,
                'verify_peer_name' => true,
            ],
        ]);

        $tempPath = $destPath . '.tmp';

        $src = @fopen($url, 'rb', false, $context);
        if ($src === false) {
            return false;
        }

        $status = 200;
        if (isset($http_response_header) && is_array($http_response_header)) {
            foreach ($http_response_header as $header) {
                if (preg_match('/^HTTP\/[\d.]+ (\d{3})/', $header, $m)) {
                    $status = (int) $m[1];
                }
            }
        }

        if ($status >= 400) {
            $errorBody = stream_get_contents($src, 4096);
            fclose($src);
            file_put_contents($destPath, $errorBody);
            return false;
        }

        $dst = @fopen($tempPath, 'wb');
        if ($dst === false) {
            fclose($src);
            return false;
        }

        $maxBytes = 50 * 1024 * 1024;
        $written  = 0;
        while (!feof($src) && $written < $maxBytes) {
            $chunk = fread($src, 65536);
            if ($chunk === false) {
                break;
            }
            fwrite($dst, $chunk);
            $written += strlen($chunk);
        }

        fclose($src);
        fclose($dst);

        if ($written >= $maxBytes) {
            @unlink($tempPath);
            return false;
        }

        if (is_file($destPath)) {
            @unlink($destPath);
        }
        return rename($tempPath, $destPath);
    }

    // ─────────────────────────────────────────────────────────
    // Cache layer (private)
    // ─────────────────────────────────────────────────────────

    /**
     * Get cached data for a key.
     *
     * @param bool $allowStale If true, return data even if TTL expired
     */
    private function getFromCache(string $key, bool $allowStale = false): ?array
    {
        $path = $this->getCachePath($key);
        if (!is_file($path)) {
            return null;
        }

        $raw = file_get_contents($path);
        $cache = json_decode($raw, true);

        if (!is_array($cache) || !isset($cache['_timestamp'])) {
            return null;
        }

        $age = time() - (int) $cache['_timestamp'];
        if (!$allowStale && $age > $this->cacheTtl) {
            return null;
        }

        unset($cache['_timestamp']);
        return $cache;
    }

    /**
     * Write data to cache.
     */
    private function writeToCache(string $key, array $data): void
    {
        $cacheDir = $this->basePath . '/storage/cache';
        if (!is_dir($cacheDir)) {
            mkdir($cacheDir, 0755, true);
        }

        $data['_timestamp'] = time();
        file_put_contents(
            $this->getCachePath($key),
            json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
        );
    }

    /**
     * Get the filesystem path for a cache key.
     */
    private function getCachePath(string $key): string
    {
        return $this->basePath . '/storage/cache/marketplace_' . md5($key) . '.json';
    }
}
