<?php

declare(strict_types=1);

namespace App\Framework\Logging;

use App\Framework\Attributes\Singleton;
use App\Framework\Logging\ValueObjects\LogContext;
use DateMalformedStringException;
use DateTimeImmutable;
use DateTimeZone;

/**
 * Einfacher Logger für das Framework.
 */
#[Singleton]
final readonly class DefaultLogger implements Logger, SupportsChannels
{
    private ChannelLoggerRegistry $channelRegistry;

    /**
     * @param LogLevel $minLevel Minimales Level, das geloggt werden soll
     * @param array<LogHandler> $handlers Array von Log-Handlern
     * @param ProcessorManager $processorManager Processor Manager für die Verarbeitung
     * @param LogContextManager|null $contextManager Optional: Context Manager für automatische Kontext-Anreicherung
     */
    public function __construct(
        private LogLevel           $minLevel = LogLevel::DEBUG,
        /** @var LogHandler[] */
        private array              $handlers = [],
        private ProcessorManager   $processorManager = new ProcessorManager(),
        private ?LogContextManager $contextManager = null,
    ) {
        // Channel-Logger-Registry initialisieren
        $this->channelRegistry = new ChannelLoggerRegistry($this);
    }

    public function debug(string $message, ?LogContext $context = null): void
    {
        $this->log(LogLevel::DEBUG, $message, $context);
    }

    public function info(string $message, ?LogContext $context = null): void
    {
        $this->log(LogLevel::INFO, $message, $context);
    }

    public function notice(string $message, ?LogContext $context = null): void
    {
        $this->log(LogLevel::NOTICE, $message, $context);
    }

    public function warning(string $message, ?LogContext $context = null): void
    {
        $this->log(LogLevel::WARNING, $message, $context);
    }

    public function error(string $message, ?LogContext $context = null): void
    {
        $this->log(LogLevel::ERROR, $message, $context);
    }

    public function critical(string $message, ?LogContext $context = null): void
    {
        $this->log(LogLevel::CRITICAL, $message, $context);
    }

    public function alert(string $message, ?LogContext $context = null): void
    {
        $this->log(LogLevel::ALERT, $message, $context);
    }

    public function emergency(string $message, ?LogContext $context = null): void
    {
        $this->log(LogLevel::EMERGENCY, $message, $context);
    }

    /**
     * Log-Nachricht mit beliebigem Level erstellen
     *
     * @param LogLevel $level Log-Level
     * @param string $message Log-Nachricht
     * @param LogContext|null $context Strukturierter LogContext
     * @throws DateMalformedStringException
     */
    public function log(LogLevel $level, string $message, ?LogContext $context = null): void
    {
        // Wenn kein Context übergeben, leeren Context erstellen
        if ($context === null) {
            $context = LogContext::empty();
        }

        // Prüfen ob Level hoch genug ist
        if ($level->isLowerThan($this->minLevel)) {
            return;
        }

        // LogContext automatisch mit aktuellem Context anreichern
        $finalContext = $this->enrichWithCurrentContext($context);

        // Log-Record erstellen
        $record = new LogRecord(
            message: $message,
            context: $finalContext,
            level: $level,
            timestamp: new DateTimeImmutable(timezone: new DateTimeZone('Europe/Berlin')),
        );

        // Record durch alle Processors verarbeiten
        $processedRecord = $this->processorManager->processRecord($record);

        // Alle Handler durchlaufen
        foreach ($this->handlers as $handler) {
            if ($handler->isHandling($processedRecord)) {
                $handler->handle($processedRecord);
            }
        }
    }

    /**
     * Loggt in einen spezifischen Channel
     *
     * @internal Wird von ChannelLogger verwendet
     */
    public function logToChannel(LogChannel $channel, LogLevel $level, string $message, ?LogContext $context = null): void
    {
        // Wenn kein Context übergeben, leeren Context erstellen
        if ($context === null) {
            $context = LogContext::empty();
        }

        // Prüfen, ob Level hoch genug ist
        if ($level->isLowerThan($this->minLevel)) {
            return;
        }

        // LogContext automatisch mit aktuellem Context anreichern
        $finalContext = $this->enrichWithCurrentContext($context);

        // Log-Record erstellen mit Channel
        $record = new LogRecord(
            message: $message,
            context: $finalContext,
            level: $level,
            timestamp: new DateTimeImmutable(timezone: new DateTimeZone('Europe/Berlin')),
            channel: $channel->value
        );

        // Record durch alle Processors verarbeiten
        $processedRecord = $this->processorManager->processRecord($record);

        // Alle Handler durchlaufen
        foreach ($this->handlers as $handler) {
            if ($handler->isHandling($processedRecord)) {
                $handler->handle($processedRecord);
            }
        }
    }

    /**
     * Reichert den übergebenen Context mit dem aktuellen Context vom LogContextManager an
     */
    private function enrichWithCurrentContext(LogContext $context): LogContext
    {
        // Wenn kein ContextManager verfügbar ist, Context unverändert zurückgeben
        if ($this->contextManager === null) {
            return $context;
        }

        $currentContext = $this->contextManager->getCurrentContext();

        // Wenn der übergebene Context ein LogContext ist, mit aktuellem Context mergen
        return $currentContext->merge($context);
    }

    /**
     * Konvertiert LogContext zu Array für Legacy-Kompatibilität
     * @deprecated
     */
    private function convertLogContextToArray(LogContext $logContext): array
    {
        $context = $logContext->structured;

        // Tags hinzufügen
        if ($logContext->hasTags()) {
            $context['_tags'] = $logContext->tags;
        }

        // Trace-Informationen hinzufügen
        if ($logContext->trace !== null) {
            $context['_trace_id'] = $logContext->trace->getTraceId();
            if ($activeSpan = $logContext->trace->getActiveSpan()) {
                $context['_span_id'] = $activeSpan->spanId;
            }
        }

        // User-Kontext hinzufügen
        if ($logContext->user !== null) {
            $context['_user_id'] = $logContext->user->userId ?? null;
        }

        // Request-Kontext hinzufügen
        if ($logContext->request !== null) {
            $context['_request_id'] = $logContext->request->requestId ?? null;
        }

        return array_merge($context, $logContext->metadata);
    }

    /**
     * Reichert LogRecord mit strukturierten Daten aus LogContext an
     */
    private function enrichRecordWithLogContext(LogRecord $record, LogContext $logContext): LogRecord
    {
        // Tags als Extra hinzufügen
        if ($logContext->hasTags()) {
            $record->addExtra('structured_tags', $logContext->tags);
        }

        // Trace-Kontext als Extra hinzufügen
        if ($logContext->trace !== null) {
            $record->addExtra('trace_context', [
                'trace_id' => $logContext->trace->getTraceId(),
                'active_span' => $logContext->trace->getActiveSpan()?->toArray(),
            ]);
        }

        // User-Kontext als Extra hinzufügen
        if ($logContext->user !== null) {
            $record->addExtra('user_context', $logContext->user->toArray());
        }

        // Request-Kontext als Extra hinzufügen
        if ($logContext->request !== null) {
            $record->addExtra('request_context', $logContext->request->toArray());
        }

        // Metadaten als Extra hinzufügen
        if (! empty($logContext->metadata)) {
            $record->addExtras($logContext->metadata);
        }

        return $record;
    }

    /**
     * Holt einen ChannelLogger für einen spezifischen Channel
     */
    public function channel(LogChannel|string $channel): Logger&HasChannel
    {
        return $this->channelRegistry->get($channel);
    }

    /**
     * Gibt die aktuelle Konfiguration des Loggers zurück
     */
    public function getConfiguration(): array
    {
        return [
            'minLevel' => $this->minLevel->value,
            'handlers' => array_map(fn (LogHandler $h) => get_class($h), $this->handlers),
            'processors' => $this->processorManager->getProcessorList(),
            'registeredChannels' => $this->channelRegistry->getRegisteredChannels(),
        ];
    }
}
