<?php

declare(strict_types=1);

namespace App\Framework\Logging\ValueObjects;

/**
 * Exception Context Value Object für strukturiertes Exception-Logging
 *
 * Erfasst alle relevanten Exception-Daten in strukturierter Form:
 * - Exception Class, Message, Code
 * - File, Line
 * - Stack Trace (strukturiert)
 * - Previous Exceptions (Chain)
 * - Custom Exception Data
 */
final readonly class ExceptionContext implements \JsonSerializable
{
    /**
     * @param StackFrame[] $stackTrace
     */
    public function __construct(
        public string $class,
        public string $message,
        public int|string $code,
        public string $file,
        public int $line,
        public array $stackTrace,
        public ?self $previous = null,
        public array $customData = []
    ) {
    }

    /**
     * Erstellt ExceptionContext aus Throwable
     */
    public static function fromThrowable(\Throwable $throwable): self
    {
        return new self(
            class: get_class($throwable),
            message: $throwable->getMessage(),
            code: $throwable->getCode(),
            file: $throwable->getFile(),
            line: $throwable->getLine(),
            stackTrace: self::parseStackTrace($throwable),
            previous: $throwable->getPrevious() !== null
                ? self::fromThrowable($throwable->getPrevious())
                : null,
            customData: self::extractCustomData($throwable)
        );
    }

    /**
     * Parst Stack Trace zu strukturierten StackFrames
     *
     * @return StackFrame[]
     */
    private static function parseStackTrace(\Throwable $throwable): array
    {
        $frames = [];

        foreach ($throwable->getTrace() as $frame) {
            $frames[] = StackFrame::fromArray($frame);
        }

        return $frames;
    }

    /**
     * Extrahiert Custom-Daten aus Exception (wenn vorhanden)
     *
     * @return array<string, mixed>
     */
    private static function extractCustomData(\Throwable $throwable): array
    {
        $data = [];

        // Für Custom Exceptions mit zusätzlichen Properties
        $reflection = new \ReflectionClass($throwable);

        foreach ($reflection->getProperties() as $property) {
            $name = $property->getName();

            // Skip Standard-Exception-Properties
            if (in_array($name, ['message', 'code', 'file', 'line', 'trace', 'previous'], true)) {
                continue;
            }

            if (!$property->isPublic()) {
                continue;
            }

            try {
                $value = $property->getValue($throwable);

                // Nur serialisierbare Daten
                if (is_scalar($value) || is_array($value)) {
                    $data[$name] = $value;
                }
            } catch (\Throwable) {
                // Property nicht verfügbar - überspringen
            }
        }

        return $data;
    }

    /**
     * Gibt kurze Exception-Klasse zurück (ohne Namespace)
     */
    public function getShortClass(): string
    {
        $parts = explode('\\', $this->class);
        return end($parts);
    }

    /**
     * Gibt kurzen File-Pfad zurück
     */
    public function getShortFile(): string
    {
        $projectRoot = dirname(__DIR__, 4);

        if (str_starts_with($this->file, $projectRoot)) {
            return substr($this->file, strlen($projectRoot) + 1);
        }

        return $this->file;
    }

    /**
     * Zählt Anzahl der Previous Exceptions (Chain Length)
     */
    public function getChainLength(): int
    {
        $count = 0;
        $current = $this->previous;

        while ($current !== null) {
            $count++;
            $current = $current->previous;
        }

        return $count;
    }

    /**
     * Gibt alle Exceptions in der Chain zurück
     *
     * @return self[]
     */
    public function getChain(): array
    {
        $chain = [$this];
        $current = $this->previous;

        while ($current !== null) {
            $chain[] = $current;
            $current = $current->previous;
        }

        return $chain;
    }

    /**
     * Gibt erste X Frames des Stack Trace zurück
     *
     * @return StackFrame[]
     */
    public function getTopFrames(int $limit = 5): array
    {
        return array_slice($this->stackTrace, 0, $limit);
    }

    /**
     * Gibt lesbare Exception-Zusammenfassung zurück
     */
    public function getSummary(): string
    {
        return sprintf(
            '%s: %s in %s:%d',
            $this->getShortClass(),
            $this->message,
            $this->getShortFile(),
            $this->line
        );
    }

    /**
     * Konvertiert zu Array für Serialisierung
     *
     * @return array<string, mixed>
     */
    public function toArray(bool $includeStackTrace = true, bool $includePrevious = true): array
    {
        $data = [
            'class' => $this->class,
            'short_class' => $this->getShortClass(),
            'message' => $this->message,
            'code' => $this->code,
            'file' => $this->file,
            'short_file' => $this->getShortFile(),
            'line' => $this->line,
        ];

        if ($includeStackTrace) {
            $data['stack_trace'] = array_map(
                fn(StackFrame $frame) => $frame->toArray(),
                $this->stackTrace
            );
            $data['stack_trace_count'] = count($this->stackTrace);
        }

        if ($includePrevious && $this->previous !== null) {
            $data['previous'] = $this->previous->toArray(
                includeStackTrace: false, // Nur Top-Level mit vollem Trace
                includePrevious: true
            );
            $data['chain_length'] = $this->getChainLength();
        }

        if (!empty($this->customData)) {
            $data['custom_data'] = $this->customData;
        }

        return $data;
    }

    /**
     * Gibt kompakte Darstellung für Logs zurück (ohne Stack Trace)
     *
     * @return array<string, mixed>
     */
    public function toCompactArray(): array
    {
        $data = [
            'class' => $this->getShortClass(),
            'message' => $this->message,
            'location' => sprintf('%s:%d', $this->getShortFile(), $this->line),
        ];

        if ($this->code !== 0 && $this->code !== '') {
            $data['code'] = $this->code;
        }

        if ($this->previous !== null) {
            $data['previous'] = $this->previous->toCompactArray();
        }

        return $data;
    }

    public function jsonSerialize(): array
    {
        return $this->toArray();
    }

    public function __toString(): string
    {
        return $this->getSummary();
    }
}
