<?php

declare(strict_types=1);

namespace PpPhp\Core\Update;

use PpPhp\Core\PlatformSettings;

/**
 * The list of update MIRRORS — public https bases that each host a signed
 * `index.json` (and the bundles it points at). Every install, server AND client,
 * tries them in order and uses the first that answers with a valid signed index,
 * so no single host being down can stop updates.
 *
 * Two layers, combined in order:
 *   1. Owner-managed list (setting `platform.update_mirrors`) — the owner adds
 *      hosts here as they stand up more mirrors; edited from the update console.
 *   2. Baked-in DEFAULTS — shipped in source so a FRESH client already has
 *      several mirrors to try before it has ever talked to the console.
 *
 * A mirror is just a base URL; the index lives at `<base>/index.json` and the
 * signed index carries absolute bundle URLs, so a mirror is self-describing. The
 * signature is verified OFFLINE against the pinned key (\PpPhp\Core\Update\
 * UpdateTrust / LicenseTrust), so an untrusted mirror can never feed a bad build.
 */
final class UpdateMirrors
{
    public const SETTING = 'platform.update_mirrors';

    /**
     * Baked-in mirrors, tried after the owner's own list. Keep these pointed at
     * hosts you control. Every shipped bundle carries this list.
     */
    public const DEFAULTS = [
        'https://ppphp-updates.ppradhan.com',
        'https://ppphp-updates.ppradhan.com.np',
        'https://app.ppradhan.com/01updates',
    ];

    /** Owner-configured base URLs (may be empty), normalised. @return array<int,string> */
    public static function owner(): array
    {
        try {
            $v = PlatformSettings::get(self::SETTING)['value'] ?? [];
        } catch (\Throwable $e) {
            return [];   // no DB (fresh/CLI) — defaults still apply
        }
        return self::normalise(is_array($v) ? $v : []);
    }

    /** The full ordered list actually used: owner mirrors first, then the baked defaults. @return array<int,string> */
    public static function bases(): array
    {
        return self::normalise(array_merge(self::owner(), self::DEFAULTS));
    }

    /** Every index URL to try, in order. @return array<int,string> */
    public static function indexUrls(): array
    {
        return array_map(static fn(string $b): string => $b . '/index.json', self::bases());
    }

    /**
     * Persist the owner's mirror list (base URLs, one per entry). Blank entries are
     * dropped; each must be an http(s) URL. Returns {ok, errors}.
     *
     * @param array<int,string> $urls
     * @return array{ok:bool, errors:array<int,string>}
     */
    public static function save(array $urls): array
    {
        $clean = [];
        $errors = [];
        foreach ($urls as $u) {
            $u = rtrim(trim((string) $u), '/');
            if ($u === '') continue;
            if (!preg_match('#^https?://[^\s/]+#i', $u)) {
                $errors[] = 'Not a valid http(s) URL: ' . $u;
                continue;
            }
            if (!in_array($u, $clean, true)) $clean[] = $u;
        }
        if ($errors !== []) return ['ok' => false, 'errors' => $errors];
        PlatformSettings::set(self::SETTING, ['value' => $clean]);
        return ['ok' => true, 'errors' => []];
    }

    /** For the console form. @return array{owner:array<int,string>, defaults:array<int,string>, effective:array<int,string>} */
    public static function forDisplay(): array
    {
        return [
            'owner'     => self::owner(),
            'defaults'  => self::DEFAULTS,
            'effective' => self::bases(),
        ];
    }

    /** @param array<int,string> $in @return array<int,string> */
    private static function normalise(array $in): array
    {
        $out = [];
        foreach ($in as $u) {
            $u = rtrim(trim((string) $u), '/');
            if ($u !== '' && preg_match('#^https?://#i', $u) && !in_array($u, $out, true)) {
                $out[] = $u;
            }
        }
        return $out;
    }
}
