<?php

namespace SimplyBook\Http\Endpoints;

use Throwable;
use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;
use SimplyBook\Traits\HasRestAccess;
use SimplyBook\Support\Helpers\Storage;
use SimplyBook\Exceptions\FormException;
use SimplyBook\Traits\HasAllowlistControl;
use SimplyBook\Exceptions\RestDataException;
use SimplyBook\Http\Entities\AbstractEntity;
use SimplyBook\Interfaces\MultiEndpointInterface;

abstract class AbstractCrudEndpoint implements MultiEndpointInterface
{
    use HasRestAccess;
    use HasAllowlistControl;

    /**
     * The entity that this endpoint uses to process requests.
     */
    protected AbstractEntity $entity;

    public function __construct(AbstractEntity $entity)
    {
        $this->entity = $entity;
    }

    /**
     * Only enable this endpoint if the user has access to the admin area
     */
    public function enabled(): bool
    {
        return $this->adminAccessAllowed();
    }

    /**
     * @inheritDoc
     */
    public function registerRoutes(): array
    {
        $route = $this->entity->getInternalEndpoint();

        return [
            $route => [
                'methods' => WP_REST_Server::READABLE . ',' . WP_REST_Server::CREATABLE,
                'callback' => [$this, 'handleCollectionRequest'],
            ],
            $route . '/(?P<id>[0-9]+)' => [
                'methods' => WP_REST_Server::READABLE . ',' . WP_REST_Server::CREATABLE . ',' . WP_REST_Server::EDITABLE . ',' . WP_REST_Server::DELETABLE,
                'callback' => [$this, 'handleSingleRequest'],
            ],
        ];
    }

    /**
     * Handle entity collection requests.
     * @internal Override this method to process the collection request.
     */
    public function handleCollectionRequest(WP_REST_Request $request): WP_REST_Response
    {
        $requestStorage = new Storage($request->get_params());

        switch ($request->get_method()) {
            case 'GET':
                return $this->getAllEntities();
            case 'POST':
                return $this->createItem($requestStorage);
            default:
                return $this->sendHttpResponse([], false, __('Method not allowed', 'simplybook'), 405);
        }
    }

    /**
     * Return all entities as a WP_REST_Response.
     * @internal Override this method if you want to customize the response.
     */
    protected function getAllEntities(): WP_REST_Response
    {
        return $this->sendHttpResponse(
            $this->entity->all()
        );
    }

    /**
     * Create a new entity based on the request parameters. It will catch any
     * validation errors or exceptions thrown during the creation process.
     * @internal Override this method if you want to customize the logic.
     */
    protected function createItem(Storage $request): WP_REST_Response
    {
        if ($request->isEmpty()) {
            return $this->sendHttpResponse([], false, esc_html__('Could not create entity, no data provided.', 'simplybook'), 400);
        }

        try {
            $this->entity->create(
                $request->all()
            );
        } catch (Throwable $e) {
            return $this->processRequestThrowable($e, 'create');
        }

        // translators: %s is either 'Service' or 'Service Provider'
        $successMessage = sprintf(
            __('%s successfully saved!', 'simplybook'),
            $this->entity->getName()
        );

        return $this->sendHttpResponse($this->entity->attributes(), true, $successMessage);
    }

    /**
     * Handle single entity requests.
     * @internal Override this method to handle GET, PUT, PATCH, POST, DELETE
     * requests for a single entity.
     */
    public function handleSingleRequest(WP_REST_Request $request): WP_REST_Response
    {
        $requestStorage = new Storage($request->get_params());

        switch ($request->get_method()) {
            case 'GET':
                return $this->findEntity($requestStorage);
            case 'PUT':
            case 'PATCH':
            case 'POST':
                return $this->updateEntity($requestStorage);
            case 'DELETE':
                return $this->deleteEntity($requestStorage);
            default:
                return $this->sendHttpResponse([], false, __('Method not allowed', 'simplybook'), 405);
        }
    }

    /**
     * Get a single entity based on the request parameters. It will catch any
     * validation errors or exceptions thrown during the retrieval process.
     * @internal Override this method if you want to customize the logic.
     */
    protected function findEntity(Storage $request): WP_REST_Response
    {
        try {
            $this->entity = $this->entity->find(
                $request->getString('id')
            );
        } catch (Throwable $e) {
            return $this->sendHttpResponse([
                'error' => $e->getMessage()
            ], false, __('Entity not found!', 'simplybook'), 404);
        }

        return $this->sendHttpResponse($this->entity->attributes());
    }

    /**
     * Update an entity based on the request parameters. It will catch any
     * validation errors or exceptions thrown during the update process.
     * @internal Override this method if you want to customize the logic.
     */
    protected function updateEntity(Storage $request): WP_REST_Response
    {
        try {
            $this->entity->fill($request->all());
            $this->entity->update();
        } catch (Throwable $e) {
            return $this->processRequestThrowable($e, 'update');
        }

        // translators: %s is either 'Service' or 'Service Provider'
        $successMessage = sprintf(
            __('%s successfully saved!', 'simplybook'),
            $this->entity->getName()
        );

        return $this->sendHttpResponse($this->entity->attributes(), true, $successMessage);
    }

    /**
     * Delete an entity based on the request parameters. It will catch any
     * validation errors or exceptions thrown during the deletion process.
     * @internal Override this method if you want to customize the logic.
     */
    protected function deleteEntity(Storage $request): WP_REST_Response
    {
        try {
            $this->entity->setPrimaryValue($request->getString('id'));
            $this->entity->delete();
        } catch (Throwable $e) {
            return $this->sendHttpResponse([
                'error' => $e->getMessage()
            ], false, esc_html__('Something went wrong while deleting.', 'simplybook'), 500);
        }

        return $this->sendHttpResponse();
    }

    /**
     * Generically process any throwable that is caught while doing requests.
     * Child classes could overwrite this method to specifically handle
     * the throwable.
     */
    protected function processRequestThrowable(Throwable $exception, string $action = ''): WP_REST_Response
    {
        if ($exception instanceof RestDataException) {
            return $this->processRestDataException($exception, $action);
        }

        if ($exception instanceof FormException) {
            return new WP_REST_Response([
                'message' => $exception->getMessage(),
                'errors' => $exception->getErrors()
            ], 400);
        }

        return $this->sendHttpResponse([], false, esc_html__('An unknown error occurred. Please try again later.', 'simplybook'), 500);
    }

    /**
     * Default behavior for processing {@see RestDataException}. Child classes
     * should overwrite for specific handling.
     */
    protected function processRestDataException(RestDataException $exception, string $action): WP_REST_Response
    {
        switch ($action) {
            case 'update':
            case 'create':
                return $this->processAttributesException($exception);
            default:
                return $this->sendHttpResponse($exception->getData(), false, $exception->getMessage(), $exception->getResponseCode());
        }
    }

    /**
     * Method specifically for handling an attribute exceptions. It should create
     * translated, user-friendly, error messages based on the faulty attributes
     * that we receive in the SimplyBook response. Errors format should be
     * consistent with the entity validation method:
     * {@see \SimplyBook\Http\Entities\AbstractEntity::validate}
     */
    protected function processAttributesException(RestDataException $exception): WP_REST_Response
    {
        $exceptionData = $exception->getData();
        if (empty($exceptionData['data'])) {
            return new WP_REST_Response([
                'message' => esc_html__('An unknown error occurred while saving, please try again.', 'simplybook'),
            ], 500);
        }

        $faultyFields = $exceptionData['data'];
        $translatedErrors = $this->buildTranslatedErrors($faultyFields);

        return new WP_REST_Response([
            'message' => __('An error occurred while saving, please try again.', 'simplybook'),
            'errors' => $translatedErrors,
        ], 500);
    }

    /**
     * Build a per-attribute list of translated, known, error messages from raw
     * validation errors. We do this because we want to show friendly,
     * translated, messages only for errors we recognize, grouped by attribute.
     *
     * Steps:
     * 1) Iterate the faulty fields; skip attributes without a known mapping.
     * 2) For each error string, scan the attribute's known
     *    "needle" => "translation" pairs.
     * 3) If a needle appears (case-insensitive), collect its translation once
     *    per attribute.
     * 4) Only include attributes that ended up with at least one translation.
     */
    protected function buildTranslatedErrors(array $faultyFields, array $knownErrors = []): array
    {
        $translatedByAttribute = [];

        if (empty($knownErrors)) {
            $knownErrors = $this->entity->getKnownErrors();
        }

        foreach ($faultyFields as $attribute => $errors) {
            if (!is_array($errors)) {
                continue;
            }

            // Keep untranslated errors for unknown errors
            if (!array_key_exists($attribute, $knownErrors)) {
                $translatedByAttribute[$attribute] = $errors;
                continue;
            }

            $translations = $this->translateAttributeErrors($errors, $knownErrors[$attribute]);

            if ($translations !== []) {
                $translatedByAttribute[$attribute] = $translations;
            }
        }

        return $translatedByAttribute;
    }

    /**
     * Translate a list of error strings for a single attribute using its
     * known error mappings.
     */
    private function translateAttributeErrors(array $errors, array $knownAttributeErrors): array
    {
        $translations = [];
        $seen = [];

        foreach ($errors as $key => $error) {
            if (!is_string($error) || $error === '') {
                continue;
            }

            $translated = $this->translateError($error, $knownAttributeErrors);

            if ($translated === null) {
                // No known needle matched — keep the untranslated error
                $translations[$key] = $error;
                continue;
            }

            if (isset($seen[$translated])) {
                // Duplicate translation — skip this error entirely
                continue;
            }

            $seen[$translated] = true;
            $translations[$key] = $translated;
        }

        return $translations;
    }

    /**
     * Match a single error string against known needles and return its
     * translation, or null when no needle matches.
     */
    private function translateError(string $error, array $knownAttributeErrors): ?string
    {
        foreach ($knownAttributeErrors as $needle => $translation) {
            if (empty($needle)) {
                continue;
            }

            if (stripos($error, (string) $needle) === false) {
                continue;
            }

            return $translation;
        }

        return null;
    }
}
