<?php

namespace MintyPHP\Http;

final class ErrorLogger
{
    private const LOG_DIR = 'storage/logs/errors';
    private const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
    private const MAX_ROTATED_FILES = 5;
    private const REDACTION_MARKER = '[REDACTED]';

    public static function log(\Throwable $exception, array $collectedData): void
    {
        try {
            $logDir = self::ensureLogDirectory();
            if ($logDir === null) {
                return;
            }

            $logFile = $logDir . '/error-' . date('Y-m-d') . '.log';
            self::rotateIfNeeded($logFile);

            $entry = self::buildLogEntry($exception, $collectedData);
            $json = json_encode($entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
            if ($json === false) {
                $json = json_encode(['error' => 'Failed to encode log entry', 'request_id' => $entry['request_id'] ?? 'unknown']);
            }

            file_put_contents($logFile, $json . "\n", FILE_APPEND | LOCK_EX);
        } catch (\Throwable) {
            // Logging must never cause secondary failures.
        }
    }

    private static function ensureLogDirectory(): ?string
    {
        $dir = self::LOG_DIR;
        if (is_dir($dir)) {
            return $dir;
        }

        if (!@mkdir($dir, 0o775, true) && !is_dir($dir)) {
            return null;
        }

        return $dir;
    }

    private static function rotateIfNeeded(string $filePath): void
    {
        if (!is_file($filePath)) {
            return;
        }

        $size = @filesize($filePath);
        if ($size === false || $size < self::MAX_FILE_SIZE) {
            return;
        }

        // Find the next available rotation slot.
        for ($i = self::MAX_ROTATED_FILES; $i >= 2; $i--) {
            $older = $filePath . '.' . $i;
            $newer = $filePath . '.' . ($i - 1);
            if (is_file($newer)) {
                @rename($newer, $older);
            }
        }

        @rename($filePath, $filePath . '.1');
    }

    /**
     * @param array{exception?: array, request?: array, environment?: array, queries?: array} $collectedData
     * @return array<string, mixed>
     */
    private static function buildLogEntry(\Throwable $exception, array $collectedData): array
    {
        $exceptionData = $collectedData['exception'] ?? [];
        $requestData = $collectedData['request'] ?? [];
        $envData = $collectedData['environment'] ?? [];
        $exceptionMessage = (string) ($exceptionData['message'] ?? $exception->getMessage());

        $traceSummary = '';
        $frames = $exceptionData['trace'] ?? [];
        foreach (array_slice($frames, 0, 10) as $i => $frame) {
            $file = $frame['file'] ?? '?';
            $line = $frame['line'] ?? '?';
            $traceSummary .= "#{$i} {$file}:{$line} ";
        }

        return [
            'timestamp' => date('c'),
            'level' => 'error',
            'request_id' => $requestData['request_id'] ?? null,
            'channel' => $requestData['channel'] ?? 'web',
            'exception' => [
                'class' => $exceptionData['class'] ?? get_class($exception),
                'message' => self::REDACTION_MARKER,
                'message_hash' => self::hashMessage($exceptionMessage),
                'code' => $exceptionData['code'] ?? $exception->getCode(),
                'file' => self::shortenPath($exceptionData['file'] ?? $exception->getFile()),
                'line' => $exceptionData['line'] ?? $exception->getLine(),
            ],
            'trace_summary' => trim($traceSummary),
            'request' => [
                'method' => $requestData['method'] ?? null,
                'path' => self::normalizeRequestPath($requestData['url'] ?? null),
                'ip_hash' => self::hashIp($requestData['ip'] ?? null),
                'user_id' => $requestData['user_id'] ?? null,
                'tenant_id' => $requestData['tenant_id'] ?? null,
            ],
            'php_version' => $envData['php_version'] ?? PHP_VERSION,
            'memory_peak_mb' => round(($envData['memory_peak'] ?? memory_get_peak_usage(true)) / 1024 / 1024, 1),
        ];
    }

    private static function hashIp(?string $ip): ?string
    {
        if ($ip === null || $ip === '') {
            return null;
        }

        return 'sha256:' . hash('sha256', $ip);
    }

    private static function hashMessage(string $message): ?string
    {
        $message = trim($message);
        if ($message === '') {
            return null;
        }

        return 'sha256:' . hash('sha256', $message);
    }

    private static function normalizeRequestPath(mixed $rawUrl): ?string
    {
        if (!is_string($rawUrl) || trim($rawUrl) === '') {
            return null;
        }

        $path = parse_url($rawUrl, PHP_URL_PATH);
        if (!is_string($path) || trim($path) === '') {
            return '/';
        }

        $path = preg_replace('/[[:cntrl:]]+/', '', $path) ?? '';
        $path = trim($path);
        if ($path === '') {
            return '/';
        }

        if (!str_starts_with($path, '/')) {
            $path = '/' . $path;
        }

        return substr($path, 0, 500);
    }

    private static function shortenPath(string $path): string
    {
        $root = getcwd();
        if ($root !== false && str_starts_with($path, $root . '/')) {
            return substr($path, strlen($root) + 1);
        }

        return $path;
    }
}
