<?php

declare(strict_types=1);

namespace PpPhp\Core;

/**
 * The ONE place that knows how this install is deployed — its canonical public
 * address and the health of its own setup — so every other subsystem (licensing,
 * updates, analytics, the mesh) can stop hardcoding or re-storing that fact.
 *
 * The design goal, in the owner's words: "automatically read things during
 * installation and place itself in a good shape." So the address is DETECTED from
 * the live request by default (always correct for the real deployment, with or
 * without a /public front-controller folder), the owner may override it, and a
 * self-audit surfaces the setup mistakes that used to only show up later as a
 * cryptic 404 (wrong doc root, exposed source, no HTTPS).
 *
 * This is a read/detect facade over PlatformSettings — it holds no secrets and
 * ships to clients unchanged.
 */
final class CoreConfig
{
    /** Owner override for the canonical self URL, when auto-detection isn't wanted. */
    public const SETTING_SELF_URL = 'platform.self_url';

    /**
     * The public base URL this install is ACTUALLY served under, read straight from
     * the current request. `dirname(SCRIPT_NAME)` is the web path to the front
     * controller: '' when the document root is the app's public/ folder, or
     * '/public' (or '/sub/public') when it is not — i.e. always the real base,
     * whichever way the doc root is pointed. No guessing, nothing to type.
     *
     * @param array<string,mixed>|null $server test seam; defaults to $_SERVER
     */
    public static function detectSelfUrl(?array $server = null): string
    {
        $s = $server ?? $_SERVER;
        $host = strtolower(trim((string) ($s['HTTP_HOST'] ?? $s['SERVER_NAME'] ?? '')));
        if ($host === '') return '';                       // CLI / no request context

        $https = (!empty($s['HTTPS']) && strtolower((string) $s['HTTPS']) !== 'off')
            || ((int) ($s['SERVER_PORT'] ?? 0) === 443)
            || (strtolower((string) ($s['HTTP_X_FORWARDED_PROTO'] ?? '')) === 'https');

        $script = str_replace('\\', '/', (string) ($s['SCRIPT_NAME'] ?? ''));
        $base = (string) preg_replace('#/[^/]*$#', '', $script);   // dirname of SCRIPT_NAME
        $base = rtrim($base, '/');
        if ($base !== '' && $base[0] !== '/') $base = '/' . $base;

        return ($https ? 'https' : 'http') . '://' . $host . $base;
    }

    /**
     * The canonical self URL, resolved: an explicit owner override wins; otherwise
     * the address detected from the request; otherwise '' (a CLI/cron context with
     * no override — callers fall back to their own default).
     */
    public static function selfUrl(): string
    {
        $override = self::override();
        if ($override !== '') return $override;
        return rtrim(self::detectSelfUrl(), '/');
    }

    /** The owner's explicit override, or '' if they've left it on auto-detect. */
    public static function override(): string
    {
        try {
            $v = trim((string) (PlatformSettings::get(self::SETTING_SELF_URL)['value'] ?? ''));
            return rtrim($v, '/');
        } catch (\Throwable $e) {
            return '';
        }
    }

    /** Set (or clear, with '') the owner override for the self URL. */
    public static function setOverride(string $url): bool
    {
        try {
            return PlatformSettings::set(self::SETTING_SELF_URL, ['value' => rtrim(trim($url), '/')]);
        } catch (\Throwable $e) {
            return false;
        }
    }

    /**
     * A cheap, network-free self-audit of the deployment: the setup facts that,
     * when wrong, surface later as confusing failures. Each check returns
     * ok|warn|bad + a plain-language note, so the console can show a green/amber/red
     * readiness panel and the installer can refuse to finish in a bad shape.
     *
     * Live checks that need HTTP (is the licence API reachable, is the signing key
     * returning 403 over the web) are layered on top of this in the console — this
     * method stays fast enough to call on any page load.
     *
     * @param array<string,mixed>|null $server test seam
     * @return list<array{id:string,level:string,label:string,note:string}>
     */
    public static function selfAudit(?array $server = null): array
    {
        $s = $server ?? $_SERVER;
        $base = defined('BASE_PATH') ? rtrim(str_replace('\\', '/', (string) constant('BASE_PATH')), '/') : '';
        $docroot = rtrim(str_replace('\\', '/', (string) realpath((string) ($s['DOCUMENT_ROOT'] ?? '')) ?: (string) ($s['DOCUMENT_ROOT'] ?? '')), '/');
        $out = [];

        // 1. Is source / config / storage kept OUT of the web root? Two supported
        //    layouts pass:
        //      (a) the document root IS the app's public/ folder (ideal); or
        //      (b) the document root is the PROJECT ROOT, but the root .htaccess
        //          funnels every request into public/ AND the private dirs carry
        //          deny-all guards. This is the sanctioned setup for hosts that will
        //          not let you move the document root (many cPanel / shared plans),
        //          so it must read GREEN, not as a misconfiguration.
        $publicDir = $base !== '' ? rtrim((string) (realpath($base . '/public') ?: ($base . '/public')), '/') : '';
        if ($docroot !== '' && $publicDir !== '') {
            $atPublic = ($docroot === $publicDir);

            // Detect layout (b): the root .htaccess funnel + private-dir guards.
            $rootHt  = $base . '/.htaccess';
            $funnel  = is_file($rootHt)
                && (bool) preg_match('~RewriteRule[^\r\n]*\bpublic/(?:\$1|%\{REQUEST_URI\})~i', (string) @file_get_contents($rootHt));
            $guarded = is_file($base . '/config/.htaccess') && is_file($base . '/storage/.htaccess');
            // Symlink-safe compare — cPanel homes are sometimes symlinked (/home vs
            // /home2), so realpath both sides before deciding "docroot IS the root".
            $baseReal = rtrim(str_replace('\\', '/', (string) (realpath($base) ?: $base)), '/');
            $atRoot  = ($docroot === $base) || ($docroot === $baseReal);

            if ($atPublic) {
                $out[] = ['id' => 'docroot', 'level' => 'ok',
                    'label' => 'Document root points at public/',
                    'note'  => 'The web root is the app public/ folder — source and config are private.'];
            } elseif ($atRoot && $funnel && $guarded) {
                $out[] = ['id' => 'docroot', 'level' => 'ok',
                    'label' => 'Requests routed into public/ by .htaccess',
                    'note'  => 'The web root is the project root, but the root .htaccess sends every request into public/ and the private folders (config, storage, …) are locked down with deny-all rules. This is the supported setup when the host will not let you move the web root.'];
            } elseif ($atRoot && $funnel) {
                $out[] = ['id' => 'docroot', 'level' => 'warn',
                    'label' => 'Requests routed into public/ by .htaccess',
                    'note'  => 'Requests are funneled into public/, but the deny-all guards on config/ and storage/ are missing. Open any page once so the app can recreate them (PrivatePathGuard), or add a deny-all .htaccess to config/ and storage/.'];
            } else {
                $out[] = ['id' => 'docroot', 'level' => 'bad',
                    'label' => 'Document root points at public/',
                    'note'  => 'The web root is NOT public/ (' . $docroot . '). Point it at ' . $publicDir
                        . ' — or, if your host will not allow that, keep the project root as the web root and add the platform root .htaccess: it funnels every request into public/ and locks down the private folders.'];
            }
        }

        // 2. HTTPS on this request?
        $https = (!empty($s['HTTPS']) && strtolower((string) $s['HTTPS']) !== 'off')
            || (strtolower((string) ($s['HTTP_X_FORWARDED_PROTO'] ?? '')) === 'https');
        $out[] = ['id' => 'https', 'level' => $https ? 'ok' : 'warn',
            'label' => 'Served over HTTPS',
            'note'  => $https ? 'Encrypted — activation keys and leases travel safely.'
                : 'This request is plain HTTP. Use HTTPS in production; activation carries the licence key.'];

        // 3. Storage writable (licence state, build cache, queues live here).
        $storage = $base !== '' ? $base . '/storage' : '';
        if ($storage !== '') {
            $wok = is_dir($storage) && is_writable($storage);
            $out[] = ['id' => 'storage', 'level' => $wok ? 'ok' : 'bad',
                'label' => 'storage/ is writable',
                'note'  => $wok ? 'The app can persist licence state, caches and queues.'
                    : 'storage/ is not writable — licensing, caching and queues will fail.'];
        }

        // 4. Detected self URL, echoed so the owner sees exactly what the app will
        //    hand out in activation codes and leases.
        $self = self::detectSelfUrl($s);
        $out[] = ['id' => 'selfurl', 'level' => $self !== '' ? 'ok' : 'warn',
            'label' => 'Canonical address detected',
            'note'  => $self !== '' ? 'This install serves at ' . $self . ' — used for activation codes and the mesh.'
                : 'Could not detect the address from this request; set it explicitly in Core Configuration.'];

        return $out;
    }

    /**
     * A network-free PERFORMANCE posture check — the server-level wins that decide
     * whether the platform runs at full speed. Read-only advice; the biggest lever
     * (OPcache) commonly gives a 3–5× response-time improvement when enabled.
     *
     * @return list<array{id:string,level:string,label:string,note:string}>
     */
    public static function perfAudit(): array
    {
        $out = [];

        // 1. OPcache — precompiled bytecode. The single biggest PHP speed win.
        $opOn = false; $opNote = 'OPcache is off — turn it on in php.ini for a 3–5× speed-up (opcache.enable=1).';
        if (function_exists('opcache_get_status')) {
            $st = @opcache_get_status(false);
            $opOn = is_array($st) && !empty($st['opcache_enabled']);
            if ($opOn) {
                $mem = $st['memory_usage'] ?? [];
                $usedMb = isset($mem['used_memory']) ? round(((int) $mem['used_memory']) / 1048576) : null;
                $opNote = 'Bytecode is cached in memory — fastest path.' . ($usedMb !== null ? " (~{$usedMb} MB used)" : '');
            }
        } elseif (ini_get('opcache.enable')) {
            $opOn = true; $opNote = 'OPcache is enabled.';
        }
        $out[] = ['id' => 'opcache', 'level' => $opOn ? 'ok' : 'warn', 'label' => 'OPcache bytecode caching', 'note' => $opNote];

        // 2. Gzip capability — can responses/assets be compressed on the wire?
        $gz = function_exists('gzencode');
        $out[] = ['id' => 'gzip', 'level' => $gz ? 'ok' : 'warn', 'label' => 'Gzip compression available',
            'note' => $gz ? 'Responses and assets can be compressed (zlib present).'
                : 'zlib is missing — install it so large payloads compress on the wire.'];

        // 3. PHP version currency.
        $modern = version_compare(PHP_VERSION, '8.1.0', '>=');
        $out[] = ['id' => 'php', 'level' => $modern ? 'ok' : 'warn', 'label' => 'Modern PHP',
            'note' => 'Running PHP ' . PHP_VERSION . ($modern ? ' — good.' : ' — upgrade to 8.1+ for a large speed and security gain.')];

        return $out;
    }
}
