<?php

declare(strict_types=1);

namespace PpPhp\Core\Projects;

use PpPhp\Core\PlatformSettings;
use PpPhp\Core\Update\UpdateProtocol;

/**
 * CLIENT side of the remote template gallery. A licensed customer install fetches
 * the signed template catalog from the authority (primary/secondary), verifies it
 * OFFLINE against the pinned public key (LicenseTrust), and caches it under
 * storage/template-cache/. Individual template packages are downloaded on demand
 * (when the user actually uses one) and verified by sha256 from the catalogue.
 *
 * Mirrors core/Update/UpdateClient (same endpoints, credentials, Ed25519 verify,
 * failover, transport) but with a domain-separated prefix (PPT1) so a template
 * catalogue can never be confused with an update feed. Never throws from catalog()
 * — a build with no key, no reachable authority, or an unverifiable answer falls
 * back to the last verified cache so the gallery still shows something.
 */
final class TemplateClient
{
    private const PREFIX  = 'PPT1';
    private const MAX_AGE = 86400;   // catalogue freshness window (24h); cache used beyond it too

    private string $root;
    private string $cacheDir;

    public function __construct(?string $root = null)
    {
        $this->root = rtrim($root ?? (defined('BASE_PATH') ? (string) constant('BASE_PATH') : getcwd()), '/');
        $this->cacheDir = $this->root . '/storage/template-cache';
    }

    public function cacheDir(): string
    {
        return $this->cacheDir;
    }

    /** True when the cached catalogue is missing or older than $seconds (time to refresh). */
    public function isStale(int $seconds): bool
    {
        $f = $this->catalogFile();
        if (!is_file($f)) {
            return true;
        }
        $d = json_decode((string) file_get_contents($f), true);
        $at = is_array($d) ? (int) ($d['fetched_at'] ?? 0) : 0;
        return (time() - $at) > max(0, $seconds);
    }

    // ── endpoints / key / credentials (identical resolution to UpdateClient) ──

    private function endpoints(): array
    {
        $list = [];
        $env = getenv('PP_TEMPLATE_AUTHORITY');
        if (is_string($env) && trim($env) !== '') $list[] = rtrim(trim($env), '/');
        try {
            $o = trim((string) (PlatformSettings::get('platform.update_authority')['value'] ?? ''));
            if ($o !== '') $list[] = rtrim($o, '/');
        } catch (\Throwable) { /* no DB */ }
        if (class_exists('\PpPhp\Core\Licensing\LicenseClient')) {
            try {
                foreach ((new \PpPhp\Core\Licensing\LicenseClient($this->root))->endpoints() as $u) {
                    $u = rtrim(trim((string) $u), '/');
                    if ($u !== '') $list[] = $u;
                }
            } catch (\Throwable) { /* not set up */ }
        }
        return array_slice(array_values(array_unique(array_filter($list))), 0, 4);
    }

    private function trustedKey(): string
    {
        $env = getenv('PP_UPDATE_TRUST_KEY');
        if (is_string($env) && $env !== '') {
            $raw = base64_decode(trim($env), true);
            if (is_string($raw) && strlen($raw) === SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES) return $raw;
        }
        if (class_exists('\PpPhp\Core\Licensing\LicenseTrust')) {
            $k = \PpPhp\Core\Licensing\LicenseTrust::key();
            if ($k !== '') return $k;
        }
        if (class_exists('\PpPhp\Core\Licensing\LicenseKeys')) {
            try {
                $keys = new \PpPhp\Core\Licensing\LicenseKeys();
                if ($keys->exists()) return $keys->publicKey();
            } catch (\Throwable) { /* no key */ }
        }
        return '';
    }

    private function credentials(): array
    {
        if (!class_exists('\PpPhp\Core\Licensing\LicenseClient')) return [];
        try {
            $c = (new \PpPhp\Core\Licensing\LicenseClient($this->root))->updateCredentials();
            $id = (string) ($c['instanceId'] ?? '');
            return $id !== '' ? ['instanceId' => $id, 'secret' => (string) ($c['secret'] ?? '')] : [];
        } catch (\Throwable) {
            return [];
        }
    }

    // ── public API ──

    /**
     * Fetch + verify the signed catalogue for $role, cache it, and return:
     *   {ok, licensed, configured, templates, cached, message}
     * Falls back to the last verified cache on any failure.
     */
    public function catalog(string $role): array
    {
        $cached = $this->readCachedList();
        $base = ['ok' => false, 'licensed' => false, 'configured' => false, 'templates' => $cached, 'cached' => true, 'message' => ''];

        $key = $this->trustedKey();
        if ($key === '') return $base + ['message' => 'Templates are not configured for this build.'];
        $endpoints = $this->endpoints();
        if ($endpoints === []) return $base + ['message' => 'No template server address is known for this installation.'];

        try {
            $reply = $this->postAny($endpoints, '/api/templates/catalog', ['role' => $role] + $this->credentials());
        } catch (\Throwable $e) {
            return $base + ['message' => 'Could not reach the template server. ' . $e->getMessage()];
        }
        if (empty($reply['licensed'])) {
            return $base + ['configured' => (bool) ($reply['configured'] ?? true), 'message' => (string) ($reply['message'] ?? 'A valid licence is required to load templates.')];
        }
        $claims = UpdateProtocol::verify((string) ($reply['catalog'] ?? ''), $key, self::PREFIX);
        if ($claims === null) {
            return $base + ['configured' => true, 'message' => 'The template server reply could not be verified — showing cached templates.'];
        }
        $templates = array_values((array) ($claims['templates'] ?? []));
        $this->writeCache($templates);
        return ['ok' => true, 'licensed' => true, 'configured' => true, 'templates' => $templates, 'cached' => false, 'message' => 'Loaded ' . count($templates) . ' template(s).'];
    }

    /**
     * Ensure a template's package (+ content seed if it has one) is downloaded to
     * the cache and integrity-verified. Returns ['package_path'=>…, 'seed_path'=>…|null].
     */
    public function fetch(string $id, string $role): array
    {
        $entry = $this->cachedEntry($id);
        if ($entry === null) {
            foreach ($this->catalog($role)['templates'] as $t) {
                if (($t['id'] ?? '') === $id) { $entry = $t; break; }
            }
        }
        if ($entry === null) throw new \RuntimeException('That template is not in your catalogue.');

        @mkdir($this->cacheDir, 0775, true);
        $pkg = $this->cacheDir . '/' . $this->safe($id) . '.ppphp';
        $this->ensureFile($pkg, $id, 'pkg', (array) ($entry['pkg'] ?? []), $role);

        $seedPath = null;
        if (!empty($entry['seed'])) {
            $seedPath = $this->cacheDir . '/' . $this->safe($id) . '-seed.sql';
            $this->ensureFile($seedPath, $id, 'seed', (array) $entry['seed'], $role);
        }
        return ['package_path' => $pkg, 'seed_path' => $seedPath];
    }

    // ── cache ──

    private function catalogFile(): string
    {
        return $this->cacheDir . '/catalog.json';
    }

    /** Persist the verified catalogue: catalog.json (with hashes) + an index.json in BuiltinTemplates shape. */
    private function writeCache(array $templates): void
    {
        @mkdir($this->cacheDir, 0775, true);
        file_put_contents($this->catalogFile(), json_encode(['fetched_at' => time(), 'templates' => $templates], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));

        // A BuiltinTemplates-shaped index the cache-aware loader can read directly.
        $index = ['templates' => array_map(function (array $t): array {
            return [
                'id' => $t['id'] ?? '', 'name' => $t['name'] ?? '', 'slug' => $t['slug'] ?? ($t['id'] ?? ''),
                'description' => $t['description'] ?? '', 'category' => $t['category'] ?? 'Business',
                'tags' => $t['tags'] ?? [], 'icon' => $t['icon'] ?? 'stack', 'color' => $t['color'] ?? '#4f46e5',
                'package' => $this->safe((string) ($t['id'] ?? '')) . '.ppphp',
                'seed' => !empty($t['seed']) ? $this->safe((string) ($t['id'] ?? '')) . '-seed.sql' : null,
                'has_content' => (bool) ($t['has_content'] ?? false), 'code_files' => (int) ($t['code_files'] ?? 1),
                'roles' => $t['roles'] ?? ['user'],
            ];
        }, $templates)];
        file_put_contents($this->cacheDir . '/index.json', json_encode($index, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n");
    }

    private function readCachedList(): array
    {
        $f = $this->catalogFile();
        if (!is_file($f)) return [];
        $d = json_decode((string) file_get_contents($f), true);
        return is_array($d) ? array_values((array) ($d['templates'] ?? [])) : [];
    }

    private function cachedEntry(string $id): ?array
    {
        foreach ($this->readCachedList() as $t) {
            if (($t['id'] ?? '') === $id) return $t;
        }
        return null;
    }

    private function safe(string $id): string
    {
        return preg_replace('/[^A-Za-z0-9._-]/', '_', $id) ?: 'template';
    }

    // ── download with integrity check ──

    private function ensureFile(string $dest, string $id, string $which, array $meta, string $role): void
    {
        $sha  = (string) ($meta['sha256'] ?? '');
        $size = (int) ($meta['size'] ?? 0);
        if (is_file($dest) && $sha !== '' && hash_equals($sha, (string) hash_file('sha256', $dest))) {
            return;   // already cached + verified
        }
        $lastErr = '';
        $ok = false;
        foreach ($this->endpoints() as $b) {
            try {
                $this->postDownload(rtrim($b, '/') . '/api/templates/fetch/' . rawurlencode($id), ['which' => $which, 'role' => $role] + $this->credentials(), $dest);
                $ok = true;
                break;
            } catch (\Throwable $e) {
                $lastErr = $e->getMessage();
            }
        }
        if (!$ok) throw new \RuntimeException('Could not download the template. ' . $lastErr);
        if ($size > 0 && filesize($dest) !== $size) {
            @unlink($dest);
            throw new \RuntimeException('Downloaded template is the wrong size — the download may be truncated.');
        }
        if ($sha !== '' && !hash_equals($sha, (string) hash_file('sha256', $dest))) {
            @unlink($dest);
            throw new \RuntimeException('Downloaded template failed its integrity check. Nothing was used.');
        }
    }

    // ── transport (mirrors UpdateClient) ──

    private function postAny(array $endpoints, string $path, array $payload): array
    {
        $lastError = null;
        foreach ($endpoints as $base) {
            try {
                return $this->post(rtrim($base, '/') . $path, $payload);
            } catch (\RuntimeException $e) {
                $lastError = $e;
            }
        }
        throw $lastError ?? new \RuntimeException('Could not reach any template server.');
    }

    private function post(string $url, array $payload): array
    {
        if (!function_exists('curl_init')) throw new \RuntimeException('cURL is required.');
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => (string) json_encode($payload, JSON_UNESCAPED_SLASHES),
            CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Accept: application/json'],
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT => 5,
            CURLOPT_TIMEOUT => 20,
            CURLOPT_FOLLOWLOCATION => false,
            CURLOPT_USERAGENT => 'PPPHP-Templates/1',
        ]);
        $raw = curl_exec($ch);
        $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $err = curl_error($ch);
        unset($ch);
        if ($raw === false || $code === 0) {
            throw new \RuntimeException('Could not reach ' . $url . ($err !== '' ? " ({$err})" : '.'));
        }
        $data = json_decode((string) $raw, true);
        if (!is_array($data)) throw new \RuntimeException('There is no PPPHP template server at ' . $url . '.');
        return $data;
    }

    private function postDownload(string $url, array $payload, string $dest): void
    {
        if (!function_exists('curl_init')) throw new \RuntimeException('cURL is required.');
        $fh = @fopen($dest, 'wb');
        if ($fh === false) throw new \RuntimeException('Cannot open the download file for writing.');
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => (string) json_encode($payload, JSON_UNESCAPED_SLASHES),
            CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
            CURLOPT_FILE => $fh,
            CURLOPT_CONNECTTIMEOUT => 6,
            CURLOPT_TIMEOUT => 120,
            CURLOPT_FOLLOWLOCATION => false,
            CURLOPT_FAILONERROR => true,
            CURLOPT_USERAGENT => 'PPPHP-Templates/1',
        ]);
        $ok = curl_exec($ch);
        $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $err = curl_error($ch);
        unset($ch);
        fclose($fh);
        if ($ok === false || $code >= 400) {
            @unlink($dest);
            throw new \RuntimeException('Download failed from ' . $url . ($err !== '' ? " ({$err})" : " (HTTP {$code})"));
        }
    }
}
