<?php

namespace MintyPHP\Http;

use MintyPHP\Repository\Support\RepoQuery;

// Holds per-request metadata (id, channel, method, path, ip) as a static singleton.
// Must be initialized once at the entry point (web, api, cli) via start().
final class RequestContext
{
    /** @var array<string, mixed>|null */
    private static ?array $context = null;

    public static function start(array $overrides = []): void
    {
        if (self::$context === null) {
            self::$context = self::buildDefaultContext();
        }

        self::applyOverrides($overrides);
    }

    public static function ensureStarted(): void
    {
        if (self::$context !== null) {
            return;
        }

        self::$context = self::buildDefaultContext();
    }

    // Returns the request ID, generating one if missing. Use this when you need a guaranteed value.
    public static function id(): string
    {
        self::ensureStarted();
        $requestId = trim((string) (self::$context['request_id'] ?? ''));
        if ($requestId === '') {
            $requestId = RepoQuery::uuidV4();
            self::$context['request_id'] = $requestId;
        }
        return $requestId;
    }

    // Returns the ID only if already set — safe to call before start() (e.g. in error handlers).
    public static function currentId(): ?string
    {
        if (self::$context === null) {
            return null;
        }

        $requestId = trim((string) (self::$context['request_id'] ?? ''));
        return $requestId !== '' ? $requestId : null;
    }

    public static function setChannel(string $channel): void
    {
        self::ensureStarted();
        $normalized = self::normalizeChannel($channel);
        if ($normalized !== '') {
            self::$context['channel'] = $normalized;
        }
    }

    public static function setPath(string $path): void
    {
        self::ensureStarted();
        $path = trim($path);
        if ($path === '') {
            return;
        }

        self::$context['path'] = substr($path, 0, 255);
    }

    public static function context(): array
    {
        self::ensureStarted();

        return self::$context ?? [];
    }

    public static function requestHeaderValue(): string
    {
        return self::id();
    }

    // HMAC when a key is configured (e.g. hashing IPs for audit logs) — plain SHA256 as fallback.
    public static function hashValue(string $value): string
    {
        $secret = defined('APP_CRYPTO_KEY') ? trim((string) APP_CRYPTO_KEY) : '';
        if ($secret === '') {
            return hash('sha256', $value);
        }

        return hash_hmac('sha256', $value, $secret);
    }

    public static function resetForTests(): void
    {
        self::$context = null;
    }

    /**
     * @return array<string, mixed>
     */
    private static function buildDefaultContext(): array
    {
        $method = strtoupper(trim((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')));
        if ($method === '') {
            $method = PHP_SAPI === 'cli' ? 'CLI' : 'GET';
        }

        $uri = (string) ($_SERVER['REQUEST_URI'] ?? '');
        $path = parse_url($uri, PHP_URL_PATH);
        $path = is_string($path) ? trim($path) : '';
        if ($path === '' && PHP_SAPI === 'cli') {
            $path = (string) ($_SERVER['argv'][0] ?? 'cli');
        }

        $requestId = self::requestIdFromHeader();
        if ($requestId === null) {
            $requestId = RepoQuery::uuidV4();
        }

        return [
            'request_id' => $requestId,
            'channel' => self::detectChannel($path),
            'method' => substr($method, 0, 8),
            'path' => substr($path, 0, 255),
            'ip' => self::normalizeIp((string) ($_SERVER['REMOTE_ADDR'] ?? '')),
            'user_agent' => self::normalizeUserAgent((string) ($_SERVER['HTTP_USER_AGENT'] ?? '')),
        ];
    }

    private static function applyOverrides(array $overrides): void
    {
        foreach ($overrides as $key => $value) {
            if (!is_string($key)) {
                continue;
            }

            if ($key === 'request_id') {
                $requestId = trim((string) $value);
                if (self::isValidUuid($requestId)) {
                    self::$context['request_id'] = $requestId;
                }
                continue;
            }

            if ($key === 'channel') {
                $channel = self::normalizeChannel((string) $value);
                if ($channel !== '') {
                    self::$context['channel'] = $channel;
                }
                continue;
            }

            if ($key === 'method') {
                $method = strtoupper(trim((string) $value));
                if ($method !== '') {
                    self::$context['method'] = substr($method, 0, 8);
                }
                continue;
            }

            if ($key === 'path') {
                $path = trim((string) $value);
                if ($path !== '') {
                    self::$context['path'] = substr($path, 0, 255);
                }
                continue;
            }
        }
    }

    private static function detectChannel(string $path): string
    {
        if (PHP_SAPI === 'cli') {
            return 'cli';
        }
        if (defined('MINTY_API_REQUEST')) {
            return 'api';
        }

        $normalizedPath = ltrim(strtolower(trim($path)), '/');
        if (str_starts_with($normalizedPath, 'api/')) {
            return 'api';
        }

        return 'web';
    }

    private static function normalizeChannel(string $channel): string
    {
        $channel = strtolower(trim($channel));
        return in_array($channel, ['web', 'api', 'scheduler', 'cli'], true) ? $channel : '';
    }

    // Accept a caller-supplied request ID for cross-service trace correlation.
    // Only trusted if it's a valid UUID format — arbitrary strings are ignored.
    private static function requestIdFromHeader(): ?string
    {
        $header = trim((string) ($_SERVER['HTTP_X_REQUEST_ID'] ?? ''));
        if (self::isValidUuid($header)) {
            return strtolower($header);
        }

        return null;
    }

    private static function isValidUuid(string $value): bool
    {
        return preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i', $value) === 1;
    }

    private static function normalizeIp(string $ip): string
    {
        $ip = trim($ip);
        if ($ip === '') {
            return '';
        }
        return substr($ip, 0, 45);
    }

    private static function normalizeUserAgent(string $userAgent): string
    {
        $userAgent = trim($userAgent);
        if ($userAgent === '') {
            return '';
        }
        return substr($userAgent, 0, 255);
    }
}
