<?php

declare(strict_types=1);

namespace PpPhp\Modules\Settings\Controllers;

use PpPhp\Core\Config;
use PpPhp\Core\CoreConfig;
use PpPhp\Core\MeshResolver;
use PpPhp\Core\Security;

/**
 * The Core Configuration console — one owner-only screen where PPPHP shows what it
 * has auto-detected about this install (its canonical address, its readiness) and
 * lets the owner override an address or verify one live. It is deliberately thin:
 * it reads/detects, it never holds a secret, and every write goes through the same
 * validated settings the rest of the platform already trusts.
 */
class CoreConfigController
{
    public function __construct(private Config $config) {}

    /** Owner-only gate (JSON for API calls, redirect for the page). */
    private function gate(bool $json = false): void
    {
        Security::RequireAuth();
        if (!Security::IsOwner()) {
            if ($json) {
                header('Content-Type: application/json');
                http_response_code(403);
                echo json_encode(['success' => false, 'message' => 'Core Configuration is owner-only.']);
                exit;
            }
            redirect('/projects');
        }
    }

    public function index(): string
    {
        $this->gate();

        $baseUrl   = $this->config->get('base_url');
        $assetsUrl = $this->config->get('assets_url');
        $user      = Security::CurrentUserData();

        $detectedUrl = CoreConfig::detectSelfUrl();
        $overrideUrl = CoreConfig::override();
        $selfUrl     = CoreConfig::selfUrl();
        $audit       = CoreConfig::selfAudit();
        $perf        = CoreConfig::perfAudit();

        // The licence mesh, each address annotated with its live health (from the
        // shared DNS-like resolver) so the owner can see failover order at a glance.
        $mesh = [];
        if (class_exists(\PpPhp\Core\Licensing\LicenseConfig::class)) {
            $primary = \PpPhp\Core\Licensing\LicenseConfig::primary();
            $backups = \PpPhp\Core\Licensing\LicenseConfig::distributableBackups();
            $ordered = MeshResolver::order(array_merge([$primary], $backups));
            foreach ($ordered as $u) {
                $mesh[] = ['url' => $u, 'role' => $u === $primary ? 'primary' : 'backup',
                           'hairpin' => MeshResolver::isHairpin($u)];
            }
        }
        // Editable manual backup list (authority only) — lets the owner fix a typo
        // or duplicate. Auto-paired backups aren't shown here (managed via pairing).
        $meshMirrors = (!\PpPhp\Core\Licensing\LicenseGate::isClientEdition()
                        && class_exists(\PpPhp\Core\Licensing\LicenseConfig::class))
            ? \PpPhp\Core\Licensing\LicenseConfig::mirrors() : null;

        // Licensing / build policy is the VENDOR's to set — shown only on the
        // authority (a distributed client's owner is a customer, not the vendor).
        $isAuthority = !\PpPhp\Core\Licensing\LicenseGate::isClientEdition();
        $policy = null;
        if ($isAuthority) {
            $policy = [
                'trialDays'   => $this->trialDays(),
                'buildLimit'  => class_exists(\PpPhp\Core\Build\BuildMeter::class) ? \PpPhp\Core\Build\BuildMeter::limit() : 0,
                'buildPeriod' => class_exists(\PpPhp\Core\Build\BuildMeter::class) ? \PpPhp\Core\Build\BuildMeter::period() : 'day',
                'buildLimits' => class_exists(\PpPhp\Core\Build\BuildMeter::class) ? \PpPhp\Core\Build\BuildMeter::globalLimits() : ['hour' => 0, 'day' => 0, 'week' => 0, 'month' => 0],
                'watermark'   => class_exists(\PpPhp\Core\Build\BuildMeter::class) ? \PpPhp\Core\Build\BuildMeter::watermarkEnabled() : true,
                'httpsOnly'   => class_exists(\PpPhp\Core\Licensing\LicenseConfig::class) && \PpPhp\Core\Licensing\LicenseConfig::requireHttps(),
                'support'     => class_exists(\PpPhp\Core\Licensing\LicenseConfig::class) ? \PpPhp\Core\Licensing\LicenseConfig::supportContact() : '',
                'periods'     => class_exists(\PpPhp\Core\Build\BuildMeter::class) ? \PpPhp\Core\Build\BuildMeter::periods() : ['day', 'week', 'month'],
            ];
        }

        // Mesh resilience + rate-limit knobs (authority; mesh values ride the lease).
        $resil = null;
        if ($isAuthority && class_exists(\PpPhp\Core\Licensing\LicenseConfig::class)) {
            $lc = \PpPhp\Core\Licensing\LicenseConfig::class;
            $resil = [
                'graceHours'  => $lc::runtimeGraceHours(),
                'heartbeat'   => $lc::heartbeatMinutes(),
                'netTimeout'  => $lc::networkTimeout(),
                'selfHeal'    => $lc::selfHeal(),
                'rateLimit'   => $lc::rateLimit(),
                'rateWindow'  => $lc::rateWindow(),
            ];
        }

        // Build-engine performance tuning (authority only — server-side effect, immediate).
        $tuning = null;
        if ($isAuthority && class_exists(\PpPhp\Core\Build\BuildMeter::class)) {
            $tuning = [
                'delta'     => \PpPhp\Core\Build\BuildMeter::deltaBuilds(),
                'slim'      => \PpPhp\Core\Build\BuildMeter::assetSlimming(),
                'cacheDays' => \PpPhp\Core\Build\BuildMeter::cacheDays(),
                'cacheMax'  => \PpPhp\Core\Build\BuildMeter::cacheMax(),
            ];
        }

        // Owner-configurable platform brand (name + logo), applied everywhere via Brand.
        $brand      = \PpPhp\Core\PlatformSettings::brand();
        $brandFlash = isset($_GET['brand']) && $_GET['brand'] === 'saved';
        $brandError = (string) ($_GET['brand_error'] ?? '');

        ob_start();
        require __DIR__ . '/../Views/core-config.php';
        return (string) ob_get_clean();
    }

    /** Configured free-trial length in days (setting → shipped default). */
    private function trialDays(): int
    {
        try {
            $d = (int) (\PpPhp\Core\PlatformSettings::get('platform.trial_days')['value'] ?? 0);
            if ($d > 0) return $d;
        } catch (\Throwable $e) {}
        return class_exists(\PpPhp\Core\Licensing\LicenseProtocol::class)
            ? (int) round(\PpPhp\Core\Licensing\LicenseProtocol::GRACE_PERIOD / 86400) : 3;
    }

    /**
     * Save the vendor's licensing / build policy. Authority-only (a distributed
     * client's owner never sees this). Applies each field that is present.
     */
    public function savePolicy(): void
    {
        $this->gate(true);
        $in = $this->input();
        $this->checkToken($in);
        header('Content-Type: application/json');
        if (\PpPhp\Core\Licensing\LicenseGate::isClientEdition()) {
            echo json_encode(['success' => false, 'message' => 'Licensing policy is set on the licence server.']);
            return;
        }
        $done = [];
        if (array_key_exists('trial_days', $in)) {
            $d = max(1, min(3650, (int) $in['trial_days']));
            if (\PpPhp\Core\PlatformSettings::set('platform.trial_days', ['value' => $d])) $done[] = 'trial';
        }
        if (class_exists(\PpPhp\Core\Build\BuildMeter::class)) {
            $bm = \PpPhp\Core\Build\BuildMeter::class;
            // New four-window global defaults (hour/day/week/month). Legacy single
            // build_limit/build_period still honoured for older UIs.
            if (array_key_exists('build_limits', $in) && is_array($in['build_limits'])
                && $bm::setGlobalLimits($in['build_limits']))                                                $done[] = 'limits';
            if (array_key_exists('build_limit', $in)  && $bm::setLimit(max(0, (int) $in['build_limit'])))  $done[] = 'limit';
            if (array_key_exists('build_period', $in) && $bm::setPeriod((string) $in['build_period']))       $done[] = 'period';
            if (array_key_exists('watermark', $in)    && $bm::setWatermark((bool) $in['watermark']))          $done[] = 'watermark';
            // Build-engine performance tuning.
            if (array_key_exists('delta', $in)        && $bm::setDeltaBuilds((bool) $in['delta']))            $done[] = 'delta';
            if (array_key_exists('slim', $in)         && $bm::setAssetSlimming((bool) $in['slim']))           $done[] = 'slim';
            if (array_key_exists('cache_days', $in)   && $bm::setCacheDays((int) $in['cache_days']))          $done[] = 'cacheDays';
            if (array_key_exists('cache_max', $in)    && $bm::setCacheMax((int) $in['cache_max']))            $done[] = 'cacheMax';
        }
        if (class_exists(\PpPhp\Core\Licensing\LicenseConfig::class)) {
            $lc = \PpPhp\Core\Licensing\LicenseConfig::class;
            if (array_key_exists('https_only', $in))   { $lc::setRequireHttps((bool) $in['https_only']); $done[] = 'https'; }
            if (array_key_exists('support', $in) && trim((string) $in['support']) !== '') { $lc::setSupportContact(trim((string) $in['support'])); $done[] = 'support'; }
            // Mesh resilience + rate limiting.
            if (array_key_exists('runtime_grace', $in) && $lc::setRuntimeGraceHours((int) $in['runtime_grace'])) $done[] = 'grace';
            if (array_key_exists('heartbeat', $in)     && $lc::setHeartbeatMinutes((int) $in['heartbeat']))      $done[] = 'heartbeat';
            if (array_key_exists('net_timeout', $in)   && $lc::setNetworkTimeout((int) $in['net_timeout']))      $done[] = 'timeout';
            if (array_key_exists('self_heal', $in))    { $lc::setSelfHeal((bool) $in['self_heal']); $done[] = 'selfHeal'; }
            if (array_key_exists('rate_limit', $in)    && $lc::setRateLimit((int) $in['rate_limit']))            $done[] = 'rateLimit';
            if (array_key_exists('rate_window', $in)   && $lc::setRateWindow((int) $in['rate_window']))          $done[] = 'rateWindow';
        }
        echo json_encode(['success' => $done !== [], 'message' => $done !== [] ? 'Policy saved.' : 'Nothing to save.', 'saved' => $done]);
    }

    /**
     * Save the mesh backup list. Authority-only. setMirrors() de-duplicates,
     * validates each URL, and drops the primary — so a typo/duplicate is cleaned up.
     */
    public function saveMesh(): void
    {
        $this->gate(true);
        $in = $this->input();
        $this->checkToken($in);
        header('Content-Type: application/json');
        if (\PpPhp\Core\Licensing\LicenseGate::isClientEdition() || !class_exists(\PpPhp\Core\Licensing\LicenseConfig::class)) {
            echo json_encode(['success' => false, 'message' => 'Mesh addresses are set on the licence server.']);
            return;
        }
        // Accept a newline/comma-separated list.
        $raw = (string) ($in['mirrors'] ?? '');
        $lines = array_filter(array_map('trim', preg_split('/[\r\n,]+/', $raw) ?: []));
        $res = \PpPhp\Core\Licensing\LicenseConfig::setMirrors($lines);
        $saved = $res['saved'] ?? [];
        $rejected = $res['rejected'] ?? [];
        $msg = 'Saved ' . count($saved) . ' backup' . (count($saved) === 1 ? '' : 's') . '.';
        if ($rejected !== []) $msg .= ' Skipped ' . count($rejected) . ' invalid or duplicate: ' . implode(', ', array_slice($rejected, 0, 3)) . '.';
        echo json_encode(['success' => true, 'message' => $msg, 'mirrors' => array_values($saved)]);
    }

    /**
     * Remove ONE address from the mesh, routing to wherever it lives:
     *  • a manual mirror → dropped from the mirror list;
     *  • a paired backup's (stale/broken) address → cleared from the mesh, the
     *    pairing itself untouched (fully unpair on Servers & keys);
     *  • the primary → refused (change it in the address card).
     */
    public function removeMeshEntry(): void
    {
        $this->gate(true);
        $in = $this->input();
        $this->checkToken($in);
        header('Content-Type: application/json');
        if (\PpPhp\Core\Licensing\LicenseGate::isClientEdition() || !class_exists(\PpPhp\Core\Licensing\LicenseConfig::class)) {
            echo json_encode(['success' => false, 'message' => 'The mesh is managed on the licence server.']);
            return;
        }
        $lc  = \PpPhp\Core\Licensing\LicenseConfig::class;
        $url = \PpPhp\Core\Licensing\LicenseConfig::clean((string) ($in['url'] ?? ''));
        if ($url === '') { echo json_encode(['success' => false, 'message' => 'No address given.']); return; }

        if ($url === $lc::primary()) {
            echo json_encode(['success' => false, 'message' => 'That is the primary — change it in “This install’s address”, not here.']);
            return;
        }
        // A manual mirror?
        $mirrors = $lc::mirrors();
        if (in_array($url, $mirrors, true)) {
            $lc::setMirrors(array_values(array_diff($mirrors, [$url])));
            echo json_encode(['success' => true, 'message' => 'Removed the backup address.']);
            return;
        }
        // Otherwise a paired backup's advertised address — clear it, keep the pairing.
        if (class_exists(\PpPhp\Core\Licensing\LicenseSync::class)) {
            $n = (new \PpPhp\Core\Licensing\LicenseSync())->forgetPeerAddress($url);
            if ($n > 0) {
                echo json_encode(['success' => true,
                    'message' => 'Removed the paired backup’s stale address from the mesh. It stays paired and keeps syncing — fully unpair it on Servers & keys if you want it gone.']);
                return;
            }
        }
        echo json_encode(['success' => false, 'message' => 'That address is no longer in the mesh — reload the page.']);
    }

    /** CSRF guard for the JSON POST endpoints (mirrors the licensing console). */
    private function checkToken(array $in): void
    {
        if (!Security::CSRF()->Check((string) ($in['_token'] ?? ''))) {
            header('Content-Type: application/json');
            http_response_code(400);
            echo json_encode(['success' => false, 'ok' => false, 'message' => 'Invalid security token. Reload the page and try again.']);
            exit;
        }
    }

    /** Save (or clear, with an empty value) the owner's self-URL override. */
    public function save(): void
    {
        $this->gate(true);
        $in = $this->input();
        $this->checkToken($in);
        header('Content-Type: application/json');
        $url = trim((string) ($in['self_url'] ?? ''));
        // Empty clears the override → back to auto-detect. A non-empty must be a URL.
        if ($url !== '' && !preg_match('#^https?://[^\s/]+#i', $url)) {
            echo json_encode(['success' => false, 'message' => 'Enter a full https:// address, or leave blank to auto-detect.']);
            return;
        }
        $ok = CoreConfig::setOverride($url);
        echo json_encode(['success' => $ok,
            'message'  => $ok ? ($url === '' ? 'Cleared — the address is auto-detected again.' : 'Saved.') : 'Could not save.',
            'selfUrl'  => CoreConfig::selfUrl()]);
    }

    /**
     * Live-verify an address: is it reachable, and does the PPPHP licence API answer
     * there? Catches the wrong-address / missing-/public mistake at config time.
     */
    public function verify(): void
    {
        $this->gate(true);
        $in  = $this->input();
        $this->checkToken($in);
        header('Content-Type: application/json');
        $url = rtrim(trim((string) ($in['url'] ?? '')), '/');
        if ($url === '' || !preg_match('#^https?://[^\s/]+#i', $url)) {
            echo json_encode(['ok' => false, 'message' => 'Enter a full https:// address to check.']);
            return;
        }
        $res = $this->probe($url);
        echo json_encode($res);
    }

    /**
     * Probe <url>/api/license/activate: a PPPHP licence server answers with JSON
     * (even to an empty body — "No pairing code…"), a 404 means the address is wrong
     * or the doc root isn't public/. Returns a friendly verdict.
     *
     * @return array{ok:bool,api:bool,code:int,message:string}
     */
    private function probe(string $url): array
    {
        if (!function_exists('curl_init')) {
            return ['ok' => false, 'api' => false, 'code' => 0, 'message' => 'cURL is disabled on this server.'];
        }
        $ch = curl_init(rtrim($url, '/') . '/api/license/activate');
        curl_setopt_array($ch, [
            CURLOPT_POST => true, CURLOPT_POSTFIELDS => '{}',
            CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT => 6, CURLOPT_TIMEOUT => 12, CURLOPT_FOLLOWLOCATION => false,
            CURLOPT_USERAGENT => 'PPPHP-CoreConfig/1',
        ]);
        $raw  = curl_exec($ch);
        $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $err  = trim((string) curl_error($ch));
        unset($ch);

        if ($code === 0) {
            return ['ok' => false, 'api' => false, 'code' => 0,
                'message' => 'Could not connect' . ($err !== '' ? ' — ' . $err : '.')];
        }
        if ($code === 404) {
            return ['ok' => false, 'api' => false, 'code' => 404,
                'message' => 'Reached the host, but no licence API here (404). If the doc root is the app root, add /public — better, point the doc root at public/.'];
        }
        $isApi = is_string($raw) && ($json = json_decode($raw, true)) !== null && is_array($json);
        return ['ok' => true, 'api' => $isApi, 'code' => $code,
            'message' => $isApi ? 'Reachable — PPPHP licence API confirmed here.'
                : 'Reachable (HTTP ' . $code . '), but the response was not the PPPHP licence API.'];
    }

    /** @return array<string,mixed> */
    private function input(): array
    {
        $raw = file_get_contents('php://input');
        $d = json_decode((string) $raw, true);
        return is_array($d) ? $d : $_POST;
    }
}
