<?php

declare(strict_types=1);

namespace Acms\Services\Url;

use Acms\Services\Facades\Database as DB;
use SQL;
use Uri\WhatWg\Url as WhatWgUrl;

/**
 * 管理ドメイン（ブログ・エイリアスの blog_domain / alias_domain）の判定・取得を担う。
 * DB 依存を隔離し、リクエスト内でメモ化する。
 */
class ManagedDomainRegistry
{
    /**
     * 管理ドメインリスト（キャッシュ）
     *
     * @var array<string>|null
     */
    private ?array $managedDomainsCache = null;

    /**
     * 管理ドメインのリストを取得（キャッシュあり）
     *
     * @param array<string> $additionalDomains 追加で許可するドメインのリスト
     * @return array<string>
     */
    public function getManagedDomains(array $additionalDomains = []): array
    {
        if ($this->managedDomainsCache === null) {
            $domains = [];

            $sql = SQL::newSelect('blog');
            $sql->setSelect('blog_domain', null, null, 'DISTINCT');
            $domains = array_merge($domains, DB::query($sql->get(dsn()), 'list'));

            $sql = SQL::newSelect('alias');
            $sql->setSelect('alias_domain', null, null, 'DISTINCT');
            $sql->addWhereOpr('alias_status', 'open');
            $domains = array_merge($domains, DB::query($sql->get(dsn()), 'list'));

            $this->managedDomainsCache = array_unique($domains);
        }

        $domains = $this->managedDomainsCache;
        if (count($additionalDomains) > 0) {
            $domains = array_merge($domains, $additionalDomains);
            $domains = array_unique($domains);
        }

        return $domains;
    }

    /**
     * メモ化した管理ドメイン一覧を破棄し、次回参照時に DB から読み直させる。
     *
     * Why not コンテナから作り直させる（forgetInstance）: このレジストリは 'url.delivery-rewriter' /
     * 'url' からもコンストラクタ注入で参照されており、forgetInstance('url.managed-domain') だけでは
     * それらが掴んでいる旧インスタンスが残ってメモ化が分裂する。共有インスタンス自身のキャッシュを
     * 落とすことで、どの参照経路からも読み直しが効く。
     *
     * @return void
     */
    public function clearCache(): void
    {
        $this->managedDomainsCache = null;
    }

    /**
     * 指定されたドメインが管理ドメインかどうかを判定
     *
     * @param string $domain チェックするドメイン
     * @param array<string> $additionalDomains 追加で許可するドメインのリスト
     * @return bool
     */
    public function isManagedDomain(string $domain, array $additionalDomains = []): bool
    {
        if ($domain === '') {
            return false;
        }
        return in_array($domain, $this->getManagedDomains($additionalDomains), true);
    }

    /**
     * window.postMessage の origin 検証に使う最小限のオリジン集合を、
     * scheme 付きオリジン（scheme://host[:port]）の配列として取得する
     *
     * 以前は getManagedDomains()（全ブログ・全エイリアスのドメイン全件）をそのまま公開していたが、
     * 実運用で問題が発覚したため最小集合に変更した。
     * - qs（クライアント側のクエリパーサー）の既定 arrayLimit=20 を超えると配列でなくプレーン
     *   オブジェクトへ変質し、呼び出し側の Array メソッド呼び出しが丸ごと例外を投げる
     * - blog_domain が空文字の行が1件でも混ざると不正な origin を生成し postMessage が例外を投げる
     * - 未ログインの共有ビューアーにまで全ブログ・全エイリアスの一覧を開示してしまう
     * 必要なのは「今このリクエストが乗っている origin」と「プレビュー対象ブログ（BID）の origin」
     * の高々2件だけなので、その2件に絞る。
     *
     * @return string[]
     */
    public function getManagedOrigins(): array
    {
        return self::buildPreviewAllowedOrigins(HTTP_HOST, blogDomain(BID), HTTPS, HTTP_PORT_VALUE);
    }

    /**
     * getManagedOrigins() の組み立てロジックを、グローバル定数（HTTP_HOST 等）に依存しない
     * 純粋関数として切り出したもの。テストで各入力パターン（空文字・null・大文字混在・ポート有無・
     * スキーム）を直接検証できるようにする意図。
     *
     * isUrlFromManagedDomain() / isSafeUrl() と同じく strtolower() を通し、event.origin
     * （ブラウザは必ず小文字で通知する）との比較を成立させる。
     *
     * @param string $ownHost 現在のリクエストのホスト名（HTTP_HOST 相当）
     * @param string|null $targetDomain プレビュー対象ブログのドメイン（blogDomain(BID) 相当。未設定なら空文字/null）
     * @param bool $isSecure 現在のリクエストが https か（HTTPS 相当）
     * @param string $portValue ポート文字列（":8080" 形式。既定ポートなら空文字。HTTP_PORT_VALUE 相当）
     * @return string[]
     */
    public static function buildPreviewAllowedOrigins(
        string $ownHost,
        ?string $targetDomain,
        bool $isSecure,
        string $portValue
    ): array {
        $scheme = $isSecure ? 'https' : 'http';
        $domains = array_filter(
            array_unique([$ownHost, $targetDomain]),
            static function ($domain): bool {
                return is_string($domain) && trim($domain) !== '';
            }
        );

        return array_values(array_map(
            static function (string $domain) use ($scheme, $portValue): string {
                return $scheme . '://' . strtolower($domain) . $portValue;
            },
            $domains
        ));
    }

    /**
     * a-blog cms で管理しているドメインのURLかチェックする
     *
     * @param string $url
     * @return bool
     */
    public function isSafeUrl($url)
    {
        $parsed = WhatWgUrl::parse($url);
        if ($parsed === null) {
            return false;
        }
        // スキームが http or https であること
        if (!in_array($parsed->getScheme(), ['http', 'https'], true)) {
            return false;
        }
        // ホストが自サービスのドメインであること（ASCII IDN に正規化して比較）
        $host = $parsed->getAsciiHost();
        if ($host === null) {
            return false;
        }

        return in_array($host, array_map('strtolower', $this->getManagedDomains([HTTP_HOST])), true);
    }
}
