<?php

declare(strict_types=1);

namespace Acms\Services\Response;

/**
 * レスポンスヘッダーの値を組み立てる純粋関数群。
 *
 * header() の副作用は持たず、送出は Emitter が担う。
 */
class HeaderBuilder
{
    /**
     * POST リクエストのボディサイズが PHP の post_max_size を超過しているか判定する。
     *
     * post_max_size を超える POST では PHP が $_POST を空にし ACMS_POST も復元できないため、
     * 通常の POST ディスパッチに乗らない。リクエスト段階でこの判定を行い 413 応答へ振り分ける。
     * ini_get / $_SERVER への依存を持たない純粋な比較として切り出し、単体テスト可能にしている。
     *
     * @param string $requestMethod リクエストメソッド（$_SERVER['REQUEST_METHOD']）
     * @param int|null $contentLength Content-Length（ヘッダ不在時は null）
     * @param int $postMaxBytes post_max_size をバイトに換算した値（0 以下は無制限）
     * @return bool 超過していれば true
     */
    public function isPostSizeExceeded(string $requestMethod, ?int $contentLength, int $postMaxBytes): bool
    {
        if (strtoupper($requestMethod) !== 'POST') {
            return false;
        }
        if ($postMaxBytes <= 0) {
            return false; // post_max_size=0 は無制限のため超過なし
        }
        if ($contentLength === null) {
            return false;
        }
        return $contentLength > $postMaxBytes;
    }

    /**
     * clientCacheHeader() が送出する Cache-Control 等のヘッダー文字列を組み立てる。
     *
     * header() 呼び出しから分離した純粋関数（テスト可能性のため）。
     *
     * @param bool $noCache
     * @return string[]
     */
    public function resolveClientCacheHeaders(bool $noCache = false): array
    {
        $cacheExpireClient = intval(config('cache_expire_client'));
        if (
            (!defined('ACMS_POST') || !ACMS_POST) &&
            ('200' == substr(httpStatusCode(), 0, 3)) &&
            (!defined('ACMS_SID') || !ACMS_SID) &&
            $cacheExpireClient > 0 &&
            !$noCache
        ) {
            return $this->buildCacheableHeaders($cacheExpireClient);
        }

        return [
            'Cache-Control: no-store, max-age=0', // HTTP/1.1 以降（HTTP/2・HTTP/3 含む）はこちらを解釈する
            'Pragma: no-cache', // HTTP/1.0 専用の後方互換。HTTP/1.1 以降・HTTP/2・HTTP/3 のクライアントは無視する
            'Expires: 0',
        ];
    }

    /**
     * クライアント・中間キャッシュ向けの Cache-Control 系ヘッダーを組み立てる純粋関数。
     *
     * キャッシュを許可してよいか（ACMS_SID の有無、subscriber_cache 等）の判定は呼び出し側の
     * 責務とし、ここでは行わない。resolveClientCacheHeaders()（動的レンダリング時）と
     * main.php のページキャッシュ配信時の双方から呼ばれる共通実装（CMS-7765 で no-cache を
     * max-age=0 に修正した際、この構築ロジックが2箇所に重複していたため片方が追従できて
     * いなかった。再発防止のためヘッダー構築部分を一本化する）。
     *
     * @param int $cacheExpireClient
     * @param int|null $lastModifiedTime Last-Modified に使う時刻。null の場合 REQUEST_TIME を使う
     * @return string[]
     */
    public function buildCacheableHeaders(int $cacheExpireClient, ?int $lastModifiedTime = null): array
    {
        $lastModifiedTime = $lastModifiedTime ?? REQUEST_TIME;
        if (config('disable_browser_cache', 'on') === 'off') {
            // ブラウザにキャッシュさせる場合
            return [
                'Cache-Control: public, max-age=' . $cacheExpireClient,
                'Last-Modified: ' . getRFC2068Time($lastModifiedTime),
                'Expires: ' . getRFC2068Time(REQUEST_TIME + $cacheExpireClient),
            ];
        }

        // 中間キャッシュ（CDNなど）にはキャッシュさせるが、ブラウザにはキャッシュさせない場合
        // no-cache は共有キャッシュにも再検証を強制し（RFC 9111 §5.2.2.4）、CDN によっては
        // s-maxage によるオフロードが効かなくなる（CloudFront は毎回オリジンへ再検証に行き、
        // Cloudflare は既定でキャッシュしない）。s-maxage は共有キャッシュで max-age より
        // 優先される（RFC 9111 §5.2.2.10）ため、max-age=0 と組み合わせることで
        // 「ブラウザには実質キャッシュさせず、CDN には s-maxage の期間キャッシュを許可する」を実現する。
        // Last-Modified は必須（バリデータの無い max-age=0 応答は再検証手段が無く、キャッシュを
        // 使うか再取得するかが実装依存になる。未ログイン時にキャッシュされた内容がログイン後も
        // 再取得されないまま表示され続ける不具合が実際に発生したため付与する）。
        return [
            'Cache-Control: public, max-age=0, s-maxage=' . $cacheExpireClient,
            'Last-Modified: ' . getRFC2068Time($lastModifiedTime),
            'Expires: 0',
            'Pragma: no-cache', // HTTP/1.0 専用の後方互換。HTTP/1.1 以降・HTTP/2・HTTP/3 のクライアントは無視する
        ];
    }
}
