<?php

declare(strict_types=1);

namespace Acms\Services\Response;

/**
 * レスポンスヘッダー・本文の送出（header() / echo / exit の副作用層）。
 *
 * ヘッダー値の組み立ては HeaderBuilder（純粋関数）に委譲する。
 */
class Emitter
{
    /**
     * @var HeaderBuilder
     */
    private $headerBuilder;

    /**
     * @param HeaderBuilder $headerBuilder
     */
    public function __construct(HeaderBuilder $headerBuilder)
    {
        $this->headerBuilder = $headerBuilder;
    }

    /**
     * すぐにリダイレクトし、同一プロセスのバックグラウンドで処理を実行
     *
     * @param string $url
     * @param int $statusCode リダイレクトの HTTP ステータスコード（既定: 301）
     */
    public function backgroundRedirect($url, $statusCode = 301)
    {
        ignore_user_abort(true);
        set_time_limit(0);
        session_write_close(); // セッションロックを解除する

        $out = '';
        while (ob_get_level()) {
            ob_end_clean();
        }
        for ($i = 0; $i < 99999; $i++) {
            $out .= ' ';
        }

        header("Location: " . $url, true, $statusCode);
        header("Content-Length: " . strlen($out));
        header("Connection: close");
        $this->addSecurityHeader();
        $this->clientCacheHeader(true);

        // 新しいバッファを開始
        if (ob_get_level() === 0) {
            ob_start();
        }

        // コンテンツ出力
        echo $out;

        sleep(2);

        // 環境に応じた終了処理
        if (function_exists('fastcgi_finish_request')) {
            // PHP-FPM環境
            fastcgi_finish_request();
        } elseif (function_exists('litespeed_finish_request')) {
            // LiteSpeed環境
            litespeed_finish_request();
        } else {
            // その他の環境
            ob_flush();
            flush();
            ob_end_flush();
        }
    }

    /**
     * セキュリティヘッダー
     */
    public function addSecurityHeader()
    {
        // クリックジャッキング対策
        if (config('x_frame_options') !== 'off') {
            if (config('x_frame_options') === 'DENY') {
                header('X-FRAME-OPTIONS: DENY');
            } else {
                header('X-FRAME-OPTIONS: SAMEORIGIN');
            }
        }
        // X-XSS-Protection
        if (config('x_xss_protection') !== 'off') {
            header('X-XSS-Protection: 1; mode=block');
        }
        // X-Content-Type-Options
        if (config('x_content_type_options') !== 'off') {
            header('X-Content-Type-Options: nosniff');
        }
        // Strict-Transport-Security(HSTS)
        if (SSL_ENABLE && FULLTIME_SSL_ENABLE && config('strict_transport_security') !== 'off') {
            header('Strict-Transport-Security: ' . config('strict_transport_security', 'max-age=86400; includeSubDomains'));
        }
        // Content-Security-Policy
        $csp = config('content_security_policy');
        if (!empty($csp) && $csp !== 'off') {
            header('Content-Security-Policy: ' . $csp);
        }
        // Referrer-Policy
        $referrerPolicy = config('referrer_policy', 'strict-origin-when-cross-origin');
        if (
            in_array(
                $referrerPolicy,
                [
                    'no-referrer',
                    'no-referrer-when-downgrade',
                    'origin',
                    'origin-when-cross-origin',
                    'same-origin',
                    'strict-origin',
                    'strict-origin-when-cross-origin',
                    'unsafe-url'
                ],
                true
            )
        ) {
            header('Referrer-Policy: ' . $referrerPolicy);
        }
    }

    /**
     * キャッシュ無効で安全なレスポンスヘッダーを組み立てます。
     *
     * @return void
     */
    public function safeHeaders(int $code = 200, string $mime = 'text/html'): void
    {
        http_response_code($code);
        header("Content-type: {$mime}; charset=" . config('charset', 'UTF-8'));
        $this->addSecurityHeader();
        $this->clientCacheHeader(true);
    }

    /**
     * セキュリティヘッダーを追加
     *
     * @param bool $noCache
     * @return void
     */
    public function clientCacheHeader(bool $noCache = false): void
    {
        foreach ($this->headerBuilder->resolveClientCacheHeaders($noCache) as $header) {
            header($header);
        }
    }

    /**
     * JSON を送出してリクエストを終了する。
     *
     * @param mixed $data
     * @return never
     */
    public function json($data): never
    {
        header('Content-Type: application/json; charset=utf-8');
        $this->addSecurityHeader();
        $this->clientCacheHeader(true);
        echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        die();
    }
}
