<?php

namespace Velis\Api\HttpExceptionHandler\Map;

use Closure;
use Throwable;
use Velis\Api\HttpExceptionHandler\HttpException;
use Velis\Api\HttpExceptionHandler\HttpExceptionInterface;

/**
 * This class is responsible for formatting exceptions to the proper response, using formatter property.
 * @author Szymon Janaczek <szymon.janaczek@velistech.com>
 */
class MapItem
{
    /**
     * Caught exception to parse.
     */
    private ?Throwable $exception = null;
    /**
     * Base HttpException used for reporting (and deciding about reporting).
     * @var class-string<HttpException>
     */
    protected string $httpExceptionClass = HttpException::class;
    protected HttpExceptionInterface $httpException;

    /**
     * Big constructors are bad, but in this case, it's the best way to define all the properties.
     * To keep it clean, using named parameters is recommended.
     *
     * @param class-string<Throwable>[] $exceptions Match exceptions as the given class.
     * @param class-string<Throwable>[] $parent Match exceptions that being instances of the given class.
     * @param Closure(Throwable):bool|bool|null $shouldReport If set, overwrites shouldReport() method from the
     *                                                        HttpException.
     * @param Closure(Throwable):void|null $onReport If set, overwrites report() method from HttpException.
     * @param Closure(Throwable):void|null $onCatch If set, will be called after setting the exception.
     * @param Closure(MapItem, Throwable):void|null $formatter If set, will be called after setting the exception,
     *                                                         used to mapping/formatting for response.
     * @param class-string<HttpExceptionInterface>|null $httpException If set, will be used as a base for reporting.
     */
    public function __construct(
        public array $exceptions = [],
        public array $parent = [],
        public ?string $message = null,
        public ?int $code = null,
        public ?int $httpCode = null,
        public ?string $error = null,
        public ?array $details = null,
        public Closure|bool|null $shouldReport = null,
        public ?Closure $onReport = null,
        public ?Closure $onCatch = null,
        public ?Closure $formatter = null,
        ?string $httpException = null,
    ) {
        if ($httpException !== null) {
            $this->setHttpExceptionClass($httpException);
        }
    }

    /**
     * @param class-string<HttpExceptionInterface $httpExceptionClass
     */
    protected function setHttpExceptionClass(string $httpExceptionClass): void
    {
        if (class_exists($httpExceptionClass) && is_subclass_of($httpExceptionClass, HttpExceptionInterface::class)) {
            $this->httpExceptionClass = $httpExceptionClass;
        }
    }

    /**
     * Check whether the given exception matches to this MapItem.
     */
    public function matchException(Throwable $exception): bool
    {
        if (in_array(get_class($exception), $this->exceptions)) {
            $this->setException($exception);
            return true;
        }

        foreach ($this->parent as $parent) {
            if ($exception instanceof $parent) {
                $this->setException($exception);
                return true;
            }
        }

        return false;
    }

    /**
     * Run formatter on the given exception.
     */
    protected function format(): void
    {
        if (is_callable($this->formatter)) {
            ($this->formatter)($this, $this->exception);
        }
    }

    /**
     * Run onCatch callback on the given exception.
     */
    protected function onCatch(): void
    {
        if (is_callable($this->onCatch)) {
            ($this->onCatch)($this->exception);
        }
    }

    /**
     * Set exception and run formatter and onCatch callbacks.
     */
    public function setException(Throwable $throwable): MapItem
    {
        $this->exception = $throwable;
        $this->format();
        $this->onCatch();

        // If already passed an HTTP Exception, just set it.
        if ($throwable instanceof HttpExceptionInterface) {
            $this->httpException = $throwable;
            return $this;
        }

        $this->httpException = new $this->httpExceptionClass(
            message: $this->message,
            code: $this->code,
            error: $this->error,
            details: $this->details,
            httpCode: $this->httpCode
        );

        return $this;
    }

    /**
     * Decide if we should report the exception to our Error Tracking system.
     */
    public function shouldReport(): bool
    {
        // If shouldReport is a boolean (because was manually set), we can use it directly.
        if (is_bool($this->shouldReport)) {
            return $this->shouldReport;
        }

        // If shouldReport is callable, use it.
        if (is_callable($this->shouldReport)) {
            return ($this->shouldReport)($this->exception);
        }

        // Otherwise, use the default setting of HttpException.
        return $this->httpException->shouldReport();
    }

    /**
     * Report an exception to our Error Tracking system.
     */
    public function report(): void
    {
        // If defined a custom onReport callback, use it.
        if (is_callable($this->onReport)) {
            ($this->onReport)($this->exception);
            return;
        }

        // Otherwise, use the default report method of HttpException.
        $this->httpException->report($this->exception);
    }

    public function getHttpException(): HttpExceptionInterface
    {
        return $this->httpException;
    }
}
