<?php

declare(strict_types=1);

namespace App\Framework\Logging\Processors;

use App\Framework\Logging\LogProcessor;
use App\Framework\Logging\LogRecord;
use Throwable;

/**
 * Enriches log records with detailed exception information
 */
final readonly class ExceptionEnrichmentProcessor implements LogProcessor
{
    public function __construct(
        private int $maxStackTraceDepth = 20,
        private bool $includeArgs = false
    ) {
    }

    public function processRecord(LogRecord $record): LogRecord
    {
        $context = $record->getContext();

        // Check for exception in context
        $exception = $this->findExceptionInContext($context);
        if ($exception === null) {
            return $record;
        }

        // Enrich record with exception details
        return $record->addExtras([
            'exception_class' => get_class($exception),
            'exception_message' => $exception->getMessage(),
            'exception_code' => $exception->getCode(),
            'exception_file' => $exception->getFile(),
            'exception_line' => $exception->getLine(),
            'stack_trace' => $this->formatStackTrace($exception),
            'stack_trace_short' => $this->getShortStackTrace($exception),
            'previous_exceptions' => $this->getPreviousExceptions($exception),
            'exception_hash' => $this->generateExceptionHash($exception),
            'exception_severity' => $this->categorizeExceptionSeverity($exception),
        ]);
    }

    public function getPriority(): int
    {
        return 100; // High priority - run early
    }

    public function getName(): string
    {
        return 'exception_enrichment';
    }

    /**
     * Find exception in context data
     */
    private function findExceptionInContext(array $context): ?Throwable
    {
        // Direct exception key
        if (isset($context['exception']) && $context['exception'] instanceof Throwable) {
            return $context['exception'];
        }

        // Search in nested context
        foreach ($context as $value) {
            if ($value instanceof Throwable) {
                return $value;
            }
        }

        return null;
    }

    /**
     * Format complete stack trace
     */
    private function formatStackTrace(Throwable $exception): array
    {
        $trace = $exception->getTrace();
        $formatted = [];

        foreach (array_slice($trace, 0, $this->maxStackTraceDepth) as $index => $frame) {
            $formatted[] = [
                'index' => $index,
                'file' => $frame['file'] ?? '[internal]',
                'line' => $frame['line'] ?? null,
                'function' => $this->formatFunction($frame),
                'class' => $frame['class'] ?? null,
                'type' => $frame['type'] ?? null,
                'args' => $this->includeArgs ? $this->formatArgs($frame['args'] ?? []) : null,
            ];
        }

        return $formatted;
    }

    /**
     * Get short stack trace (top 5 frames)
     */
    private function getShortStackTrace(Throwable $exception): string
    {
        $trace = $exception->getTrace();
        $short = [];

        foreach (array_slice($trace, 0, 5) as $frame) {
            $file = basename($frame['file'] ?? '[internal]');
            $line = $frame['line'] ?? '?';
            $function = $this->formatFunction($frame);

            $short[] = "{$file}:{$line} {$function}()";
        }

        return implode(' → ', $short);
    }

    /**
     * Get all previous exceptions in chain
     */
    private function getPreviousExceptions(Throwable $exception): array
    {
        $previous = [];
        $current = $exception->getPrevious();

        while ($current !== null) {
            $previous[] = [
                'class' => get_class($current),
                'message' => $current->getMessage(),
                'code' => $current->getCode(),
                'file' => $current->getFile(),
                'line' => $current->getLine(),
            ];

            $current = $current->getPrevious();

            // Prevent infinite loops
            if (count($previous) > 10) {
                break;
            }
        }

        return $previous;
    }

    /**
     * Generate unique hash for exception pattern
     */
    private function generateExceptionHash(Throwable $exception): string
    {
        $signature = get_class($exception) .
                    $exception->getFile() .
                    $exception->getLine() .
                    $exception->getMessage();

        return substr(md5($signature), 0, 8);
    }

    /**
     * Categorize exception severity
     */
    private function categorizeExceptionSeverity(Throwable $exception): string
    {
        return match(true) {
            $exception instanceof \Error => 'fatal',
            $exception instanceof \ParseError => 'fatal',
            $exception instanceof \TypeError => 'high',
            $exception instanceof \InvalidArgumentException => 'medium',
            $exception instanceof \LogicException => 'high',
            $exception instanceof \RuntimeException => 'medium',
            default => 'unknown'
        };
    }

    /**
     * Format function name with class context
     */
    private function formatFunction(array $frame): string
    {
        if (isset($frame['class'])) {
            $class = $this->shortenClassName($frame['class']);
            $type = $frame['type'] ?? '::';
            $function = $frame['function'] ?? 'unknown';

            return "{$class}{$type}{$function}";
        }

        return $frame['function'] ?? 'unknown';
    }

    /**
     * Shorten class name for readability
     */
    private function shortenClassName(string $className): string
    {
        $parts = explode('\\', $className);

        if (count($parts) <= 2) {
            return $className;
        }

        // Keep first and last part: App\...\Controller
        return $parts[0] . '\\...\\' . end($parts);
    }

    /**
     * Format function arguments safely
     */
    private function formatArgs(array $args): array
    {
        return array_map(function ($arg) {
            return match(true) {
                is_object($arg) => get_class($arg) . '{}',
                is_array($arg) => 'Array[' . count($arg) . ']',
                is_string($arg) => '"' . substr($arg, 0, 50) . (strlen($arg) > 50 ? '...' : '') . '"',
                is_numeric($arg) => (string) $arg,
                is_bool($arg) => $arg ? 'true' : 'false',
                is_null($arg) => 'null',
                default => gettype($arg)
            };
        }, $args);
    }
}
