<?php

namespace Velis\Mvc\Controller\Utils;

use Phalcon\Http\RequestInterface;
use Phalcon\Mvc\Dispatcher;
use ReflectionException;
use ReflectionMethod;
use ReflectionNamedType;
use ReflectionParameter;
use Velis\App;
use Velis\Dto\BaseDto;
use Velis\Dto\Exceptions\ValidationException;
use Velis\Filter;
use Velis\Output;

/**
 * This class is responsible for transforming the request body into DTOs.
 * It checks the controller method against using DTOs as params.
 * Then it creates a new DTO object from the request body data and injects it as the action's param.
 * The request body is extracted depending on the request method and filtered using @see Filter.
 *
 * @author Szymon Janaczek <szymon.janaczek@velistech.com>
 */
class DtoRequestTransformer
{
    public function __construct(private RequestInterface $request)
    {
    }

    /**
     * Extract the request body, depending on the request method.
     * For POST, PUT and PATCH, the request body can be encoded in the JSON format, so we have to decode it.
     * FOR GET remove _url param from query string.
     *
     * @return array|null
     */
    private function getRequestBody(): ?array
    {
        return match ($this->request->getMethod()) {
            'DELETE', 'POST' => $this->decodeRequestBody($this->request->getPost()),
            'GET' => call_user_func(function () {
                $body = $this->request->getQuery();
                unset($body['_url']);

                return $body;
            }),
            'PUT' => $this->decodeRequestBody($this->request->getPut()),
            'PATCH' => $this->decodeRequestBody($this->request->getPatch()),
        };
    }

    /**
     * If the request body is JSON, decode it, otherwise - return as is.
     *
     * @param array|string|null $data
     * @return array|null
     */
    private function decodeRequestBody(array|string|null $data): ?array
    {
        if ($data !== null && strtolower($this->request->getContentType()) === 'application/json') {
            return json_decode($this->request->getRawBody(), true);
        }

        return $data;
    }

    /**
     * Create a new DTO object from the request body data and inject it as the action's param.
     *
     * @throws ValidationException
     */
    private function createDto(ReflectionParameter $param): BaseDto
    {
        /** @var class-string<BaseDto> $dtoName */
        $dtoName = $param->getType()->getName();
        $requestBody = $this->getRequestBody();
        $filter = new Filter($requestBody, App::$config->settings->autoFiltering);

        return $dtoName::fromFilter($filter);
    }

    /**
     * Check controller method against using DTOs as params.
     * If so, create a new DTO object from the request body data and inject it as the action's param.
     * Otherwise, inject the request params as the action's param.
     *
     * @throws ReflectionException
     * @throws ValidationException
     */
    public function transformRequest(Dispatcher $dispatcher): void
    {
        $controller = $dispatcher->getControllerClass();
        $method = lcfirst(Output::toPascalCase($dispatcher->getActionName())) . 'Action';
        $reflectionMethod = new ReflectionMethod($controller, $method);
        $parameters = $reflectionMethod->getParameters();

        $dispatcherParams = $dispatcher->getParams();

        $processedParameters = [];
        foreach ($parameters as $param) {
            $type = $param->getType();

            /**
             * PHP Code Sniffer is not so happy about the multiline match expression, so we have to disable it,
             * because without splitting it into multiple lines, it's less readable.
             * phpcs:disable
             */
            $processedParameters[$param->getName()] = match (
                $type instanceof ReflectionNamedType
                && is_subclass_of($param->getType()->getName(), BaseDto::class)
            ) {
                true => $this->createDto($param),
                default => $dispatcher->getParam($param->getName())
            };
            // phpcs:enable
        }

        $dispatcher->setParams($processedParameters + $dispatcherParams);
    }
}
