<?php
/**
 * BookiFlex Core
 *
 * @package    BookiFlex
 * @subpackage Core
 * @author     BookiFlex
 * @copyright  Copyright (c) 2025 BookiFlex
 * @license    GPL-2.0-or-later
 *
 * @link       https://bookiflex.com
 */

declare(strict_types=1);

namespace BookiFlex\Core\Api\Controller;

use BookiFlex\Core\Api\Request\Account\CancelReservationRequest;
use BookiFlex\Core\Api\Request\Account\GetReservationRequest;
use BookiFlex\Core\Api\Transformer\PublicReservationGroupTransformer;
use BookiFlex\Core\Exception\Exception as CoreException;
use BookiFlex\Core\Model\Reservation\CancellationActor;
use BookiFlex\Core\Model\Reservation\Reservation;
use BookiFlex\Core\Model\Reservation\ReservationStatus;
use BookiFlex\Core\Provider\LocalTimeZone;
use BookiFlex\Core\Service\Reservation\ReservationDetails;
use BookiFlex\Core\Service\Reservation\ReservationStatusManager;
use DateTimeImmutable;
use Throwable;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;

defined( 'ABSPATH' ) || exit;

class AccountController extends AbstractController
{
    public const API_ENDPOINT_NAME = 'account';
    private const DEFAULT_DATE_FILTER_OFFSET_HOURS = 72;

    private PublicReservationGroupTransformer $publicReservationGroupTransformer;

    public function __construct(
        private readonly ReservationDetails $reservationDetails,
        private readonly ReservationStatusManager $reservationStatusManager,
        string $namespace = 'bflex/v1'
    ) {
        parent::__construct($namespace);
        $this->publicReservationGroupTransformer = new PublicReservationGroupTransformer();
    }

    /**
     * Register API routes
     */
    public function registerRoutes(): void
    {
        // Public endpoint: retrieves reservation details by unique SID (UUID string) without exposing personal data (no authentication required)
        register_rest_route($this->namespace, '/' . self::API_ENDPOINT_NAME . '/reservation', [
            [
                'methods' => 'POST',
                'callback' => [$this, 'getReservation'],
                'permission_callback' => '__return_true',
                'args' => GetReservationRequest::getValidationRules(),
            ],
        ]);

        // Public endpoint: allows cancellation with unique SID (UUID string) and cancellation code (no authentication required)
        register_rest_route($this->namespace, '/' . self::API_ENDPOINT_NAME . '/reservation/cancel', [
            [
                'methods' => 'POST',
                'callback' => [$this, 'cancelReservation'],
                'permission_callback' => '__return_true',
                'args' => CancelReservationRequest::getValidationRules(),
            ],
        ]);
    }

    /**
     * POST /account/reservation
     * Return information about reservation(s) by SID
     */
    public function getReservation(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        try {
            $getRequest = GetReservationRequest::fromWpRestRequest($request);

            $reservations = $this->reservationDetails->getReservationGroup(
                $getRequest->getSidAsString()
            );

            if (!$this->isValidReservationGroup($reservations)) {
                return $this->error(
                    'reservation_not_found',
                    __('Reservation not found or cancelled.', 'bookiflex'),
                    404
                );
            }

            $paymentAllocations = $this->reservationDetails->getPaymentAllocations($reservations);

            $transformedData = $this->publicReservationGroupTransformer->transform(
                $reservations,
                $this->reservationDetails->getRatePlanNames(),
                $this->reservationDetails->getAccommodationTypes(),
                $paymentAllocations,
            );

            return $this->success([
                'status' => 'success',
                'result' => [
                    'data' => $transformedData,
                ],
            ]);
        } catch (CoreException $e) {
            $context = $e->getContext();

            // For validation exceptions with errors array
            if (isset($context['errors']) && is_array($context['errors'])) {
                return $this->error(
                    'validation_failed',
                    $e->getMessage(),
                    $e->getCode() ?: 400,
                    ['params' => $context['errors']]
                );
            }

            // For other exceptions, use errorCode
            $errorCode = $context['errorCode'] ?? 'unknown_error';
            return $this->error(
                'validation_failed',
                $e->getMessage(),
                $e->getCode() ?: 400,
                [
                    'params' => [
                        $errorCode => $e->getMessage()
                    ]
                ]
            );
        } catch (Throwable $e) {
            return $this->handleError(
                $e,
                'account_get_reservation_failed',
                __('Failed to get reservation.', 'bookiflex')
            );
        }
    }

    /**
     * POST /account/reservation/cancel
     * Cancel reservation by SID and confirmation code
     */
    public function cancelReservation(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        try {
            $cancelRequest = CancelReservationRequest::fromWpRestRequest($request);

            $this->reservationStatusManager->cancelReservation(
                $cancelRequest->getSidAsString(),
                $cancelRequest->getCode(),
                true,
                CancellationActor::Guest,
            );

            return $this->success([
                'status' => 'success',
                'result' => [
                    'successful' => true,
                ],
            ]);
        } catch (CoreException $e) {
            $context = $e->getContext();
            $errorCode = $context['errorCode'] ?? 'unknown_error';

            // For public negative scenarios (invalid code or not found), return successful: false
            if (in_array($errorCode, ['invalid_cancellation_code', 'reservation_not_found'])) {
                return $this->success([
                    'status' => 'success',
                    'result' => [
                        'successful' => false,
                        'message' => __('Invalid reservation code or reservation not found', 'bookiflex'),
                    ],
                ]);
            }

            // For validation exceptions with errors array
            if (isset($context['errors']) && is_array($context['errors'])) {
                return $this->error(
                    'validation_failed',
                    $e->getMessage(),
                    $e->getCode() ?: 400,
                    ['params' => $context['errors']]
                );
            }

            // For other exceptions, use errorCode
            return $this->error(
                'validation_failed',
                $e->getMessage(),
                $e->getCode() ?: 400,
                [
                    'params' => [
                        $errorCode => $e->getMessage()
                    ]
                ]
            );
        } catch (Throwable $e) {
            return $this->handleError(
                $e,
                'account_cancel_reservation_failed',
                __('Failed to cancel reservation.', 'bookiflex')
            );
        }
    }

    /**
     * Check if reservation group is valid
     */
    private function isValidReservationGroup(?array $reservations): bool
    {
        if ($reservations === null || empty($reservations)) {
            return false;
        }

        $filteredReservations = $this->filterByDate($reservations);
        if (empty($filteredReservations)) {
            return false;
        }

        $firstReservation = $reservations[0];
        return !$firstReservation->getStatus()->isEquals(ReservationStatus::STATUS_CANCELLED);
    }

    /**
     * Filter reservations by check-out date
     * Exclude reservations where check-out date + offset hours has passed
     */
    private function filterByDate(array $reservations, int $offsetHours = self::DEFAULT_DATE_FILTER_OFFSET_HOURS): array
    {
        $now = new DateTimeImmutable('now', LocalTimeZone::get());

        return array_values(
            array_filter(
                $reservations,
                fn (Reservation $reservation) =>
                    $reservation->getMaxPossibleCheckOutDateTime()?->modify('+' . $offsetHours . ' hours') >= $now
            )
        );
    }
}
