<?php

declare(strict_types=1);

namespace Apexl\Io\Logger\Services;

use Apexl\Io\Logger\Dtos\HandlerConfig;
use Apexl\Io\Logger\Dtos\StreamConfig;
use Apexl\Io\Logger\Enums\Handler;
use Apexl\Io\Logger\Interfaces\LoggerInterface;
use Apexl\Io\Logger\Services\HandlerCreator\HandlerCreatorFactory;
use Exception;
use Monolog\Handler\HandlerInterface;
use Monolog\Processor\PsrLogMessageProcessor;
use Psr\Log\AbstractLogger;
use Stringable;
use Throwable;
use UnexpectedValueException;
use ValueError;

class Logger extends AbstractLogger implements LoggerInterface
{
    private readonly \Monolog\Logger $driver;

    /**
     * @throws Exception
     */
    public function __construct(
        private readonly string $stream,
        private readonly StreamConfig $config,
        private readonly HandlerCreatorFactory $handlerCreatorFactory
    ) {
        $driver = new \Monolog\Logger($stream);
        $driver->setExceptionHandler(
            (function (Throwable $throwable): void {
                $this->handleThrowable($throwable);
            })(...)
        );

        $driver->setHandlers($this->handlers());
        $driver->pushProcessor(new PsrLogMessageProcessor(removeUsedContextFields: true));

        $this->driver = $driver;
    }

    private function handleThrowable(Throwable $throwable): void
    {
        error_log(
            self::formatThrowable($throwable, 'Error handling log message'),
            E_USER_WARNING
        );
    }

    private static function formatThrowable(Throwable $throwable, string $prepend = ''): string
    {
        $message = sprintf('(%d) %s', $throwable->getCode(), $throwable->getMessage());

        if ($prepend !== '' && $prepend !== '0') {
            $message = sprintf('%s: %s', $prepend, $message);
        }

        return $message;
    }

    /**
     * @return HandlerInterface[]
     * @throws Exception
     */
    private function handlers(): array
    {
        $handlers = [];
        foreach ($this->config->handlers as $handler) {
            $handlers[] = $this->handler($handler);
        }

        // A handler we couldn't build is null; Monolog would fatal on it.
        return array_values(array_filter($handlers));
    }

    /**
     * @throws Exception
     */
    private function handler(HandlerConfig $handlerConfig): ?HandlerInterface
    {
        try {
            $destination = Handler::from($handlerConfig->destination);

            return $this->handlerCreatorFactory->make(
                $destination,
            )->create(
                $this->stream,
                $handlerConfig,
                $this->config,
            );
        } catch (UnexpectedValueException|ValueError $e) {
            $this->unknownHandler($handlerConfig->destination, $e->getMessage());
        }

        return null;
    }

    private function unknownHandler(string $handler, string $append = ''): void
    {
        $message = sprintf('Unknown Handler \'%s\'', $handler);

        if ($append !== '' && $append !== '0') {
            $message = sprintf('%s: %s', $message, $append);
        }

        error_log(
            $message,
            E_USER_WARNING
        );
    }

    public function log($level, string|Stringable $message, array $context = []): void
    {
        $this->driver->log($level, $message, $context);
    }
}
