<?php

namespace Velis\Api\HttpExceptionHandler\Map;

use DomainException;
use InvalidArgumentException;
use Phalcon\Http\Message\ResponseStatusCodeInterface as ResponseStatusCode;
use Phalcon\Mvc\Dispatcher\Exception as DispatcherException;
use Throwable;
use Velis\Db\Exception as DbException;
use Velis\Dto\Exceptions\ValidationException;
use Velis\Mvc\Controller\AccessException;
use Velis\Mvc\Controller\Exception\BadRequestException;
use Velis\Mvc\Controller\Exception\ForbiddenException;
use Velis\Mvc\Controller\Exception\NotFoundException;
use Velis\RateLimiter\TooManyAttemptsException;
use Velis\RestrictionException;

/**
 * Here we can define our exception map. Order is important - first matched exception will be used.
 * We can match exception in two ways:
 * - as exact hit (when our caught exception class is the same as the one in the map)
 * - as instance (when our caught exception is an instance of the one in the map)
 * In the Map we can set all the values we want to use in the response and define onCatch and formatter closures
 * used to successively:
 * - add action on catch (e.g. report exception to somewhere else)
 * - and format the response (e.g. modify error message depending on details):
 *
 * @author Szymon Janaczek <szymon.janaczek@velistech.com>
 */
class DefaultApiMap implements MapInterface
{
    public function load(): iterable
    {
        yield new MapItem(
            parent: [InvalidArgumentException::class],
            message: 'Bad Request',
            httpCode: ResponseStatusCode::STATUS_BAD_REQUEST,
        );

        yield new MapItem(
            parent: [BadRequestException::class],
            message: 'Bad request',
            httpCode: ResponseStatusCode::STATUS_BAD_REQUEST,
            formatter: function (MapItem $item, Throwable $exception): void {
                $item->message = $exception->getMessage() ?: $item->message;
            }
        );

        yield new MapItem(
            parent: [AccessException::class],
            message: 'Forbidden.',
            httpCode: ResponseStatusCode::STATUS_FORBIDDEN,
            formatter: function (MapItem $item, Throwable $exception): void {
                $item->message = $exception->getMessage() ?: $item->message;
            }
        );

        yield new MapItem(
            parent: [ForbiddenException::class],
            message: 'Forbidden.',
            httpCode: ResponseStatusCode::STATUS_FORBIDDEN,
            formatter: function (MapItem $item, Throwable $exception): void {
                $item->message = $exception->getMessage() ?: $item->message;
            }
        );

        yield new MapItem(
            parent: [NotFoundException::class],
            message: 'Not found',
            httpCode: ResponseStatusCode::STATUS_NOT_FOUND,
            formatter: function (MapItem $item, Throwable $exception): void {
                $item->message = $exception->getMessage() ?: $item->message;
            }
        );

        yield new MapItem(
            parent: [RestrictionException::class,],
            httpCode: ResponseStatusCode::STATUS_UNPROCESSABLE_ENTITY,
            formatter: function (MapItem $item, Throwable $exception): void {
                $item->message = $exception->getMessage() ?: $item->message;
            }
        );

        yield new MapItem(
            parent: [ValidationException::class],
            message: 'Validation error',
            httpCode: ResponseStatusCode::STATUS_UNPROCESSABLE_ENTITY,
            error: null,
            formatter: function (MapItem $item, Throwable $exception): void {
                if ($exception instanceof ValidationException) {
                    $item->details = $exception->getErrors();
                }
            }
        );

        yield new MapItem(
            parent: [DomainException::class],
            httpCode: ResponseStatusCode::STATUS_UNPROCESSABLE_ENTITY,
            formatter: function (MapItem $item, Throwable $exception): void {
                $item->message = $exception->getMessage() ?: $item->message;
            }
        );

        yield new MapItem(
            parent: [TooManyAttemptsException::class],
            httpCode: ResponseStatusCode::STATUS_TOO_MANY_REQUESTS,
        );

        yield new MapItem(
            exceptions: [DispatcherException::class],
            message: 'Not found',
            httpCode: ResponseStatusCode::STATUS_NOT_FOUND,
        );

        yield new MapItem(
            exceptions: [DbException::class],
            message: 'Internal Server Error',
            httpCode: ResponseStatusCode::STATUS_INTERNAL_SERVER_ERROR,
        );
    }
}
