<?php

namespace MintyPHP\Http;

use MintyPHP\Service\Security\RateLimiterService;
use MintyPHP\Service\Settings\SettingsApiPolicyGateway;

// Entry point bootstrapper called once at the top of every API action.
// Handles: request context, CORS, audit start, rate limiting, and optional Bearer auth.
class ApiBootstrap
{
    // Two separate rate-limit buckets: one per IP (broad), one per token+IP (fine-grained).
    private const API_RATE_SCOPE_TOKEN = 'api.token_ip';
    private const API_RATE_SCOPE_IP = 'api.ip';
    private const API_RATE_LIMIT_TOKEN = 300;
    private const API_RATE_LIMIT_IP = 120;
    private const API_RATE_WINDOW_SECONDS = 60;
    private const API_RATE_BLOCK_SECONDS = 120;

    private static bool $initialized = false;
    private static bool $shutdownRegistered = false;
    /** @var (callable(): object)|null Resolver for API audit service (provided by audit module) */
    private static $apiAuditServiceResolver = null;
    /** @var (callable(): SettingsApiPolicyGateway)|null */
    private static $settingsApiPolicyGatewayResolver = null;
    /** @var (callable(): RateLimiterService)|null */
    private static $rateLimiterServiceResolver = null;
    /** @var (callable(): object)|null Resolver for API system audit reporter (provided by audit module) */
    private static $apiSystemAuditReporterResolver = null;

    public static function configure(
        ?callable $apiAuditServiceResolver,
        callable $settingsApiPolicyGatewayResolver,
        callable $rateLimiterServiceResolver,
        ?callable $apiSystemAuditReporterResolver
    ): void {
        self::$apiAuditServiceResolver = $apiAuditServiceResolver;
        self::$settingsApiPolicyGatewayResolver = $settingsApiPolicyGatewayResolver;
        self::$rateLimiterServiceResolver = $rateLimiterServiceResolver;
        self::$apiSystemAuditReporterResolver = $apiSystemAuditReporterResolver;
    }

    /**
     * Initialize the API request context.
     *
     * Call this at the top of every API page action.
     * Sets MINTY_ALLOW_OUTPUT, CORS headers and optional Bearer auth.
     */
    public static function init(bool $requireAuth = true): void
    {
        if (self::$initialized) {
            return;
        }
        self::$initialized = true;

        if (!defined('MINTY_ALLOW_OUTPUT')) {
            define('MINTY_ALLOW_OUTPUT', true);
        }

        RequestContext::start([
            'channel' => 'api',
            'method' => strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')),
            'path' => (string) parse_url((string) ($_SERVER['REQUEST_URI'] ?? ''), PHP_URL_PATH),
        ]);
        header('X-Request-Id: ' . RequestContext::requestHeaderValue());

        self::registerShutdownHandler();
        self::tryStartAudit();
        self::startSystemAuditReporter();
        self::setCorsHeaders();

        if (strtoupper($_SERVER['REQUEST_METHOD'] ?? '') === 'OPTIONS') {
            http_response_code(204);
            die();
        }

        self::enforceRateLimit();

        if ($requireAuth) {
            $authenticated = ApiAuth::authenticate();
            if (!$authenticated) {
                ApiResponse::unauthorized();
            }
        }
    }

    private static function setCorsHeaders(): void
    {
        $rawOrigin = trim((string) ($_SERVER['HTTP_ORIGIN'] ?? ''));
        if ($rawOrigin === '') {
            return;
        }
        $origin = self::normalizeOrigin($rawOrigin);
        if ($origin === null) {
            return;
        }

        $allowedOrigins = array_fill_keys(self::settingsApiPolicy()->getApiCorsAllowedOrigins(), true);
        if (!isset($allowedOrigins[$origin])) {
            return;
        }

        header('Vary: Origin');
        header('Access-Control-Allow-Origin: ' . $origin);
        header('Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS');
        header('Access-Control-Allow-Headers: Authorization, Content-Type, Accept');
        header('Access-Control-Max-Age: 86400');
    }

    // Guarantees audit records are written even on uncaught fatal errors.
    private static function registerShutdownHandler(): void
    {
        if (self::$shutdownRegistered) {
            return;
        }
        self::$shutdownRegistered = true;

        register_shutdown_function(static function (): void {
            $error = error_get_last();
            if (!is_array($error)) {
                $statusCode = (int) http_response_code();
                if ($statusCode <= 0) {
                    $statusCode = 200;
                }
                self::tryFinishAudit($statusCode);
                self::finishSystemAuditReporter($statusCode);
                return;
            }

            $type = (int) $error['type'];
            $fatalTypes = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR];
            if (!in_array($type, $fatalTypes, true)) {
                $statusCode = (int) http_response_code();
                if ($statusCode <= 0) {
                    $statusCode = 200;
                }
                self::tryFinishAudit($statusCode);
                self::finishSystemAuditReporter($statusCode);
                return;
            }

            $statusCode = (int) http_response_code();
            if ($statusCode < 400) {
                $statusCode = 500;
            }
            self::tryFinishAudit($statusCode, 'fatal_error');
            self::finishSystemAuditReporter($statusCode, 'fatal_error');
        });
    }

    private static function normalizeOrigin(string $origin): ?string
    {
        $origin = rtrim(trim($origin), '/');
        if ($origin === '') {
            return null;
        }

        $parsed = parse_url($origin);
        if (!is_array($parsed)) {
            return null;
        }

        $scheme = strtolower((string) ($parsed['scheme'] ?? ''));
        $host = strtolower((string) ($parsed['host'] ?? ''));
        $port = isset($parsed['port']) ? (int) $parsed['port'] : null;

        if (!in_array($scheme, ['http', 'https'], true) || $host === '') {
            return null;
        }

        if (
            isset($parsed['path'])
            || isset($parsed['query'])
            || isset($parsed['fragment'])
            || isset($parsed['user'])
            || isset($parsed['pass'])
        ) {
            return null;
        }

        $defaultPort = ($scheme === 'https') ? 443 : 80;
        $portSuffix = ($port !== null && $port !== $defaultPort) ? ':' . $port : '';
        return $scheme . '://' . $host . $portSuffix;
    }

    private static function enforceRateLimit(): void
    {
        $ip = trim((string) ($_SERVER['REMOTE_ADDR'] ?? ''));
        if ($ip === '') {
            $ip = 'unknown';
        }

        $ipResult = self::rateLimiter()->hit(
            self::API_RATE_SCOPE_IP,
            $ip,
            self::API_RATE_LIMIT_IP,
            self::API_RATE_WINDOW_SECONDS,
            self::API_RATE_BLOCK_SECONDS
        );
        if (!($ipResult['allowed'] ?? true)) {
            ApiResponse::tooManyRequests(max(1, (int) ($ipResult['retry_after'] ?? self::API_RATE_BLOCK_SECONDS)));
        }

        $selector = self::extractBearerSelector();
        if ($selector === '') {
            return;
        }

        $tokenResult = self::rateLimiter()->hit(
            self::API_RATE_SCOPE_TOKEN,
            strtolower($selector) . '|' . $ip,
            self::API_RATE_LIMIT_TOKEN,
            self::API_RATE_WINDOW_SECONDS,
            self::API_RATE_BLOCK_SECONDS
        );

        if (!($tokenResult['allowed'] ?? true)) {
            ApiResponse::tooManyRequests(max(1, (int) ($tokenResult['retry_after'] ?? self::API_RATE_BLOCK_SECONDS)));
        }
    }

    // API tokens are formatted as "selector:verifier". The selector (24-char hex) is used
    // as the rate-limit key so a single token can't bypass the per-IP bucket by rotating IPs.
    private static function extractBearerSelector(): string
    {
        $token = ApiAuth::extractBearerToken();
        if ($token === '') {
            return '';
        }

        $parts = explode(':', $token, 2);
        $selector = trim($parts[0]);
        if ($selector === '' || !preg_match('/^[a-f0-9]{24}$/i', $selector)) {
            return '';
        }

        return $selector;
    }

    private static function settingsApiPolicy(): SettingsApiPolicyGateway
    {
        return self::resolveDependency(
            self::$settingsApiPolicyGatewayResolver,
            SettingsApiPolicyGateway::class,
            'ApiBootstrap'
        );
    }

    private static function rateLimiter(): RateLimiterService
    {
        return self::resolveDependency(self::$rateLimiterServiceResolver, RateLimiterService::class, 'ApiBootstrap');
    }

    private static function tryStartAudit(): void
    {
        try {
            if (is_callable(self::$apiAuditServiceResolver)) {
                $service = (self::$apiAuditServiceResolver)();
                if (method_exists($service, 'startRequestContext')) {
                    $service->startRequestContext();
                }
            }
        } catch (\Throwable) {
            // fail-open
        }
    }

    private static function tryFinishAudit(int $statusCode, ?string $errorCode = null): void
    {
        try {
            if (is_callable(self::$apiAuditServiceResolver)) {
                $service = (self::$apiAuditServiceResolver)();
                if (method_exists($service, 'finish')) {
                    $service->finish($statusCode, $errorCode);
                }
            }
        } catch (\Throwable) {
            // fail-open
        }
    }

    private static function startSystemAuditReporter(): void
    {
        try {
            if (is_callable(self::$apiSystemAuditReporterResolver)) {
                $service = (self::$apiSystemAuditReporterResolver)();
                if (method_exists($service, 'start')) {
                    $service->start();
                }
            }
        } catch (\Throwable) {
            // fail-open
        }
    }

    private static function finishSystemAuditReporter(int $statusCode, ?string $errorCode = null): void
    {
        try {
            if (is_callable(self::$apiSystemAuditReporterResolver)) {
                $service = (self::$apiSystemAuditReporterResolver)();
                if (method_exists($service, 'finish')) {
                    $service->finish($statusCode, $errorCode);
                }
            }
        } catch (\Throwable) {
            // fail-open
        }
    }

    private static function resolveDependency(mixed $resolver, string $expectedClass, string $consumer): mixed
    {
        if (!is_callable($resolver)) {
            throw new \RuntimeException($consumer . ' is not configured for dependency: ' . $expectedClass);
        }

        $service = $resolver();
        if (!$service instanceof $expectedClass) {
            throw new \RuntimeException($consumer . ' resolver returned invalid dependency: ' . $expectedClass);
        }

        return $service;
    }
}
