<?php

namespace MintyPHP\Http;

use MintyPHP\Debugger;

final class ErrorDataCollector
{
    private const CODE_CONTEXT_LINES = 10;
    private const MAX_TRACE_FRAMES = 50;

    private const REDACTED_HEADER_PATTERNS = [
        'authorization',
        'cookie',
        'x-api-key',
    ];

    private const REDACTED_KEY_PATTERNS = [
        'password',
        'secret',
        'token',
        'key',
        'csrf',
    ];

    private const SAFE_ENV_KEYS = [
        'APP_DEBUG',
        'APP_ENV',
        'APP_NAME',
        'APP_TIMEZONE',
        'APP_LOCALE',
        'APP_URL',
        'APP_VENDOR_PHP_ISSUES_MODE',
        'TENANT_SCOPE_STRICT',
    ];

    /**
     * @return array{exception: array<string, mixed>, request: array<string, mixed>, environment: array<string, mixed>, queries: array<string, mixed>}
     */
    public static function collect(\Throwable $exception): array
    {
        return [
            'exception' => self::collectException($exception),
            'request' => self::collectRequest(),
            'environment' => self::collectEnvironment(),
            'queries' => self::collectQueries(),
        ];
    }

    /**
     * @return array<string, mixed>
     */
    private static function collectException(\Throwable $exception): array
    {
        $data = [
            'class' => get_class($exception),
            'message' => $exception->getMessage(),
            'code' => $exception->getCode(),
            'file' => $exception->getFile(),
            'line' => $exception->getLine(),
            'trace' => self::collectStackFrames($exception),
        ];

        $previous = $exception->getPrevious();
        if ($previous !== null) {
            $data['previous'] = self::collectException($previous);
        }

        return $data;
    }

    /**
     * @return list<array<string, mixed>>
     */
    private static function collectStackFrames(\Throwable $exception): array
    {
        $frames = [];

        // Frame 0 is the exception origin itself.
        $frames[] = [
            'file' => $exception->getFile(),
            'line' => $exception->getLine(),
            'function' => null,
            'class' => null,
            'code_snippet' => self::extractCodeSnippet($exception->getFile(), $exception->getLine()),
        ];

        foreach (array_slice($exception->getTrace(), 0, self::MAX_TRACE_FRAMES) as $frame) {
            $file = $frame['file'] ?? null;
            $line = $frame['line'] ?? null;
            $function = $frame['function'];
            $class = $frame['class'] ?? null;

            $frames[] = [
                'file' => $file,
                'line' => $line,
                'function' => $function,
                'class' => $class,
                'args_summary' => self::summarizeArgs($frame['args'] ?? []),
                'code_snippet' => ($file !== null && $line !== null)
                    ? self::extractCodeSnippet($file, $line)
                    : [],
            ];
        }

        return $frames;
    }

    /**
     * @return array<int, string> Line number => code line
     */
    private static function extractCodeSnippet(string $file, int $line): array
    {
        try {
            if (!is_file($file) || !is_readable($file)) {
                return [];
            }

            $lines = @file($file, FILE_IGNORE_NEW_LINES);
            if ($lines === false) {
                return [];
            }

            $start = max(0, $line - self::CODE_CONTEXT_LINES - 1);
            $end = min(count($lines), $line + self::CODE_CONTEXT_LINES);

            $snippet = [];
            for ($i = $start; $i < $end; $i++) {
                $snippet[$i + 1] = $lines[$i];
            }

            return $snippet;
        } catch (\Throwable) {
            return [];
        }
    }

    /**
     * @param list<mixed> $args
     * @return list<string>
     */
    private static function summarizeArgs(array $args): array
    {
        $summary = [];
        foreach ($args as $arg) {
            $summary[] = match (true) {
                is_null($arg) => 'null',
                is_bool($arg) => $arg ? 'true' : 'false',
                is_int($arg) => 'int',
                is_float($arg) => 'float',
                is_string($arg) => 'string(' . strlen($arg) . ')',
                is_array($arg) => 'array(' . count($arg) . ')',
                is_object($arg) => get_class($arg),
                is_resource($arg) => 'resource',
                default => 'unknown',
            };
        }

        return $summary;
    }

    /**
     * @return array<string, mixed>
     */
    private static function collectRequest(): array
    {
        $data = [
            'request_id' => null,
            'channel' => 'web',
            'method' => $_SERVER['REQUEST_METHOD'] ?? 'GET',
            'url' => ($_SERVER['REQUEST_URI'] ?? ''),
            'ip' => $_SERVER['REMOTE_ADDR'] ?? null,
            'user_agent' => substr((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 255),
            'headers' => self::collectHeaders(),
            'get' => $_GET,
            'post' => self::redactParams($_POST),
            'session_keys' => [],
            'user_id' => null,
            'tenant_id' => null,
        ];

        // Safe request context access.
        try {
            $data['request_id'] = RequestContext::currentId() ?? RequestContext::id();
        } catch (\Throwable) {
            // RequestContext may not be initialized.
        }

        try {
            $ctx = RequestContext::context();
            $data['channel'] = $ctx['channel'] ?? 'web';
        } catch (\Throwable) {
        }

        // Session data — keys and types only, never values.
        if (session_status() === PHP_SESSION_ACTIVE) {
            $data['session_keys'] = self::describeSessionKeys($_SESSION ?? []);
            $data['user_id'] = $_SESSION['user']['id'] ?? $_SESSION['user_id'] ?? null;
            $data['tenant_id'] = $_SESSION['current_tenant']['id'] ?? $_SESSION['tenant_id'] ?? null;
        }

        return $data;
    }

    /**
     * @return array<string, string>
     */
    private static function collectHeaders(): array
    {
        $headers = [];
        foreach ($_SERVER as $key => $value) {
            if (!str_starts_with($key, 'HTTP_')) {
                continue;
            }

            $name = strtolower(str_replace('_', '-', substr($key, 5)));
            $headers[$name] = self::shouldRedactHeader($name) ? '[REDACTED]' : (string) $value;
        }

        return $headers;
    }

    private static function shouldRedactHeader(string $name): bool
    {
        $lower = strtolower($name);
        foreach (self::REDACTED_HEADER_PATTERNS as $pattern) {
            if (str_contains($lower, $pattern)) {
                return true;
            }
        }

        return false;
    }

    /**
     * @param array<string, mixed> $params
     * @return array<string, mixed>
     */
    private static function redactParams(array $params): array
    {
        $result = [];
        foreach ($params as $key => $value) {
            $result[$key] = self::shouldRedactParam((string) $key) ? '[REDACTED]' : $value;
        }

        return $result;
    }

    private static function shouldRedactParam(string $key): bool
    {
        $lower = strtolower($key);
        foreach (self::REDACTED_KEY_PATTERNS as $pattern) {
            if (str_contains($lower, $pattern)) {
                return true;
            }
        }

        return false;
    }

    /**
     * @return array<string, string>
     */
    private static function describeSessionKeys(array $session): array
    {
        $result = [];
        foreach ($session as $key => $value) {
            $result[(string) $key] = get_debug_type($value);
        }

        return $result;
    }

    /**
     * @return array<string, mixed>
     */
    private static function collectEnvironment(): array
    {
        $data = [
            'php_version' => PHP_VERSION,
            'php_sapi' => PHP_SAPI,
            'extensions' => get_loaded_extensions(),
            'memory_current' => memory_get_usage(true),
            'memory_peak' => memory_get_peak_usage(true),
            'env' => self::collectSafeEnv(),
            'modules' => [],
        ];

        // Active modules — may fail if container is unavailable.
        try {
            if (function_exists('app')) {
                $registry = app(\MintyPHP\App\Module\ModuleRegistry::class);
                $modules = $registry->getModules();
                $data['modules'] = array_map(
                    fn (object $m): string => $m->id ?? (string) $m,
                    $modules,
                );
            }
        } catch (\Throwable) {
        }

        return $data;
    }

    /**
     * @return array<string, string>
     */
    private static function collectSafeEnv(): array
    {
        $result = [];

        foreach (self::SAFE_ENV_KEYS as $key) {
            $value = $_ENV[$key] ?? $_SERVER[$key] ?? null;
            if ($value !== null) {
                $result[$key] = (string) $value;
            }
        }

        return $result;
    }

    /**
     * @return array<string, mixed>
     */
    private static function collectQueries(): array
    {
        try {
            if (!class_exists(Debugger::class, false) || !Debugger::$enabled) {
                return ['available' => false, 'queries' => [], 'start' => null];
            }

            $queries = Debugger::get('queries');
            $start = Debugger::get('start');

            if (!is_array($queries)) {
                $queries = [];
            }

            return [
                'available' => true,
                'queries' => $queries,
                'start' => is_float($start) ? $start : null,
                'count' => count($queries),
            ];
        } catch (\Throwable) {
            return ['available' => false, 'queries' => [], 'start' => null];
        }
    }
}
