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

declare(strict_types=1);

namespace BookiFlex\Extensions\Cart\Api\Controller;

use BookiFlex\Core\Api\Controller\AbstractController;
use BookiFlex\Core\Exception\Exception as CoreException;
use BookiFlex\Extensions\Cart\Api\Request\AddCartExtraRequest;
use BookiFlex\Extensions\Cart\Api\Request\AddToCartRequest;
use BookiFlex\Extensions\Cart\Api\Request\ChangePaymentTypeRequest;
use BookiFlex\Extensions\Cart\Api\Request\ConfirmCartRequest;
use BookiFlex\Extensions\Cart\Api\Request\RemoveFromCartRequest;
use BookiFlex\Extensions\Cart\Api\Request\UpdateCartItemRequest;
use BookiFlex\Extensions\Cart\Api\Transformer\CartTransformer;
use BookiFlex\Extensions\Cart\Api\Transformer\CartTotalsTransformer;
use BookiFlex\Extensions\Cart\Model\AccommodationCartItem;
use BookiFlex\Extensions\Cart\Model\ExtraCartItem;
use BookiFlex\Extensions\Cart\Service\CartCheckoutService;
use BookiFlex\Extensions\Cart\Service\CartService;
use BookiFlex\Extensions\Cart\Service\CartInitService;
use BookiFlex\Core\Model\Reservation\Reservation;
use BookiFlex\Core\Service\SessionService;
use Throwable;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;

defined('ABSPATH') || exit;

class CartController extends AbstractController
{
    public const API_ENDPOINT_NAME = 'cart';

    public function __construct(
        private readonly CartInitService $initService,
        private readonly CartService $cartService,
        private readonly CartCheckoutService $checkoutService,
        private readonly SessionService $sessionService,
        private readonly CartTransformer $cartTransformer,
        private readonly CartTotalsTransformer $cartTotalsTransformer,
        string $namespace = 'bflex/v1'
    ) {
        parent::__construct($namespace);
    }

    public function registerRoutes(): void
    {
        // Public endpoint: initialize cart and get settings. This endpoint is intentionally public
        // and does not use WordPress nonces to support guest checkout (users can book without
        // WordPress accounts). Cart binding: Each user is identified via a secure session cookie
        // (bflex_cart_session_id) managed by SessionService. The cookie contains a cryptographically
        // secure session ID (bflex_cart_ + 64 hex chars) with httponly, samesite=Lax, and secure flags.
        register_rest_route(
            $this->namespace, '/' . self::API_ENDPOINT_NAME . '/init', [
            [
                'methods' => 'POST',
                'callback' => [$this, 'getInitData'],
                'permission_callback' => '__return_true',
            ],
            ]
        );

        // Public endpoint: get/add cart contents. These endpoints are intentionally public and does not use
        // WordPress nonces to support guest checkout. Cart binding via session cookie (bflex_cart_session_id).
        register_rest_route(
            $this->namespace, '/' . self::API_ENDPOINT_NAME, [
            [
                'methods' => 'GET',
                'callback' => [$this, 'getCart'],
                'permission_callback' => '__return_true',
            ],
            [
                'methods' => 'POST',
                'callback' => [$this, 'addToCart'],
                'permission_callback' => '__return_true',
                'args' => AddToCartRequest::getValidationRules(),
            ],
            ]
        );

        // Public endpoint: clear cart. This endpoint is intentionally public and does not use
        // WordPress nonces to support guest checkout. Cart binding via session cookie (bflex_cart_session_id).
        register_rest_route(
            $this->namespace, '/' . self::API_ENDPOINT_NAME . '/delete', [
            [
                'methods' => 'POST',
                'callback' => [$this, 'clearCart'],
                'permission_callback' => '__return_true',
            ]
            ]
        );

        // Public endpoint: update cart item quantity. This endpoint is intentionally public and does
        // not use WordPress nonces to support guest checkout. Cart binding via session cookie (bflex_cart_session_id).
        register_rest_route(
            $this->namespace, '/' . self::API_ENDPOINT_NAME . '/items/(?P<hash>[a-zA-Z0-9]+)/update', [
            [
                'methods' => 'POST',
                'callback' => [$this, 'updateItem'],
                'permission_callback' => '__return_true',
                'args' => UpdateCartItemRequest::getValidationRules()
            ],
            ]
        );

        // Public endpoint: remove cart item. This endpoint is intentionally public and does not use
        // WordPress nonces to support guest checkout. Cart binding via session cookie (bflex_cart_session_id).
        register_rest_route(
            $this->namespace, '/' . self::API_ENDPOINT_NAME . '/items/(?P<hash>[a-zA-Z0-9]+)/delete', [
            [
                'methods' => 'POST',
                'callback' => [$this, 'removeItem'],
                'permission_callback' => '__return_true',
                'args' => RemoveFromCartRequest::getValidationRules(),
            ],
            ]
        );

        // Public endpoint: change payment type. This endpoint is intentionally public and does not
        // use WordPress nonces to support guest checkout. Cart binding via session cookie (bflex_cart_session_id).
        register_rest_route(
            $this->namespace, '/' . self::API_ENDPOINT_NAME . '/payment-type/update', [
            [
                'methods' => 'POST',
                'callback' => [$this, 'changePaymentType'],
                'permission_callback' => '__return_true',
                'args' => ChangePaymentTypeRequest::getValidationRules(),
            ]
            ]
        );

        // Public endpoint: confirm cart and create reservations. This endpoint is intentionally public
        // and does not use WordPress nonces to support guest checkout. Cart binding via session cookie
        // (bflex_cart_session_id).
        register_rest_route(
            $this->namespace, '/' . self::API_ENDPOINT_NAME . '/confirm', [
            [
                'methods' => 'POST',
                'callback' => [$this, 'confirmCart'],
                'permission_callback' => '__return_true',
                'args' => ConfirmCartRequest::getValidationRules(),
            ]
            ]
        );

        // Public endpoint: add extra service to cart. Bound to a parent accommodation cart item
        // via parentHash. Available only when an extras provider is registered (i.e. the Extras
        // extension is active); otherwise returns 404.
        register_rest_route(
            $this->namespace, '/' . self::API_ENDPOINT_NAME . '/extras', [
            [
                'methods' => 'POST',
                'callback' => [$this, 'addExtra'],
                'permission_callback' => '__return_true',
                'args' => AddCartExtraRequest::getValidationRules(),
            ],
            ]
        );

        // Public endpoint: calculate cart totals. This endpoint is intentionally public and does not
        // use WordPress nonces to support guest checkout. Cart binding via session cookie (bflex_cart_session_id).
        register_rest_route(
            $this->namespace, '/' . self::API_ENDPOINT_NAME . '/calculate', [
            [
                'methods' => 'POST',
                'callback' => [$this, 'calculateTotal'],
                'permission_callback' => '__return_true',
            ]
            ]
        );
    }

    /**
     * POST /cart/init - Get initial cart data
     *
     * Security: This endpoint is intentionally public and does not use WordPress nonces to support
     * guest checkout flow - users can browse and book without WordPress accounts. User identification
     * and cart binding is achieved through a secure session cookie (bflex_cart_session_id) managed by
     * SessionService. The session cookie contains a cryptographically secure ID (bflex_cart_ + 64 hex
     * characters) with httponly, samesite=Lax, and secure (SSL) flags, valid for 1 year.
     *
     * @return WP_REST_Response|WP_Error
     */
    public function getInitData(): WP_REST_Response|WP_Error
    {
        try {
            $sessionId = $this->sessionService->getSessionId();
            $this->cartService->setSessionId($sessionId);

            // Get initialization settings
            $settings = $this->initService->getSettings();

            // Get current cart state
            $cart = $this->cartService->getCart(true);
            $totals = $this->cartService->calculateTotal();
            $transformedCart = $this->cartTransformer->transform($cart, $totals);

            return $this->success(
                [
                'status' => 'success',
                'result' => [
                    'cart'     => $transformedCart,
                    'settings' => $settings,
                ],
                ]
            );
        } catch (Throwable $e) {
            return $this->handleError($e, 'cart_operation_failed', __('Failed to initialize cart.', 'bookiflex'));
        }
    }

    /**
     * GET /cart - Get current cart contents
     *
     * Security: This endpoint is intentionally public and does not use WordPress nonces to support
     * guest checkout flow - users can browse and book without WordPress accounts. User identification
     * and cart binding is achieved through a secure session cookie (bflex_cart_session_id) managed by
     * SessionService. The session cookie contains a cryptographically secure ID (bflex_cart_ + 64 hex
     * characters) with httponly, samesite=Lax, and secure (SSL) flags, valid for 1 year.
     *
     * @return WP_REST_Response|WP_Error
     */
    public function getCart(): WP_REST_Response|WP_Error
    {
        try {
            $this->cartService->setSessionId($this->sessionService->getSessionId());

            // Always get or create cart
            $cart = $this->cartService->getCart(true);
            $totals = $this->cartService->calculateTotal();
            $transformed = $this->cartTransformer->transform($cart, $totals);

            return $this->success(
                [
                'status' => 'success',
                'result' => [
                    'cart' => $transformed,
                ],
                ]
            );
        } catch (Throwable $e) {
            return $this->handleError($e, 'cart_operation_failed', __('Failed to fetch cart.', 'bookiflex'));
        }
    }

    /**
     * POST /cart - Add item to cart
     *
     * Security: This endpoint is intentionally public and does not use WordPress nonces to support
     * guest checkout flow - users can browse and book without WordPress accounts. User identification
     * and cart binding is achieved through a secure session cookie (bflex_cart_session_id) managed by
     * SessionService. The session cookie contains a cryptographically secure ID (bflex_cart_ + 64 hex
     * characters) with httponly, samesite=Lax, and secure (SSL) flags, valid for 1 year.
     *
     * @param  WP_REST_Request $request
     * @return WP_REST_Response|WP_Error
     */
    public function addToCart(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        try {
            $addRequest = AddToCartRequest::fromWpRestRequest($request);

            $this->cartService->setSessionId($this->sessionService->getSessionId());
            $this->cartService->addItem($addRequest->toArray());

            if ($addRequest->getPromoCode() !== null) {
                $this->cartService->setPromoCode($addRequest->getPromoCode());
            }

            $cart = $this->cartService->getCart();
            $totals = $this->cartService->calculateTotal();
            $transformed = $this->cartTransformer->transform($cart, $totals);

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

            // For CartValidationException, use the errors array as params
            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, 'cart_operation_failed', __('Failed to add item to cart', 'bookiflex'));
        }
    }

    /**
     * POST /cart/items/{hash}/update - Update item quantity
     *
     * Security: This endpoint is intentionally public and does not use WordPress nonces to support
     * guest checkout flow - users can browse and book without WordPress accounts. User identification
     * and cart binding is achieved through a secure session cookie (bflex_cart_session_id) managed by
     * SessionService. The session cookie contains a cryptographically secure ID (bflex_cart_ + 64 hex
     * characters) with httponly, samesite=Lax, and secure (SSL) flags, valid for 1 year.
     *
     * @param  WP_REST_Request $request
     * @return WP_REST_Response|WP_Error
     */
    public function updateItem(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        try {
            $updateRequest = UpdateCartItemRequest::fromWpRestRequest($request);

            $this->cartService->setSessionId($this->sessionService->getSessionId());
            $this->cartService->updateItemQuantity($updateRequest->getHash(), $updateRequest->getQuantity());

            // Always return current cart state (whether update succeeded or not)
            $cart = $this->cartService->getCart(true);
            $totals = $this->cartService->calculateTotal();
            $transformed = $this->cartTransformer->transform($cart, $totals);

            return $this->success(
                [
                'status' => 'success',
                'result' => [
                    'cart' => $transformed,
                ],
                ]
            );
        } catch (CoreException $e) {
            $context = $e->getContext();
            $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, 'cart_operation_failed', __('Failed to update cart item', 'bookiflex'));
        }
    }

    /**
     * POST /cart/items/{hash}/delete - Remove item from cart
     *
     * Security: This endpoint is intentionally public and does not use WordPress nonces to support
     * guest checkout flow - users can browse and book without WordPress accounts. User identification
     * and cart binding is achieved through a secure session cookie (bflex_cart_session_id) managed by
     * SessionService. The session cookie contains a cryptographically secure ID (bflex_cart_ + 64 hex
     * characters) with httponly, samesite=Lax, and secure (SSL) flags, valid for 1 year.
     *
     * @param  WP_REST_Request $request
     * @return WP_REST_Response|WP_Error
     */
    public function removeItem(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        try {
            $removeRequest = RemoveFromCartRequest::fromWpRestRequest($request);

            $this->cartService->setSessionId($this->sessionService->getSessionId());
            $this->cartService->removeItem($removeRequest->getHash());

            // Always return current cart state (whether removal succeeded or not)
            $cart = $this->cartService->getCart(true);
            $totals = $this->cartService->calculateTotal();
            $transformed = $this->cartTransformer->transform($cart, $totals);

            return $this->success(
                [
                'status' => 'success',
                'result' => [
                    'cart' => $transformed,
                ],
                ]
            );
        } catch (CoreException $e) {
            $context = $e->getContext();
            $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, 'cart_operation_failed', __('Failed to remove item from cart', 'bookiflex'));
        }
    }

    /**
     * POST /cart/delete - Clear entire cart
     *
     * Security: This endpoint is intentionally public and does not use WordPress nonces to support
     * guest checkout flow - users can browse and book without WordPress accounts. User identification
     * and cart binding is achieved through a secure session cookie (bflex_cart_session_id) managed by
     * SessionService. The session cookie contains a cryptographically secure ID (bflex_cart_ + 64 hex
     * characters) with httponly, samesite=Lax, and secure (SSL) flags, valid for 1 year.
     *
     * @return WP_REST_Response|WP_Error
     */
    public function clearCart(): WP_REST_Response|WP_Error
    {
        try {
            $this->cartService->setSessionId($this->sessionService->getSessionId());
            $this->cartService->clear();

            $cart = $this->cartService->getCart(true);
            $totals = $this->cartService->calculateTotal();
            $transformed = $this->cartTransformer->transform($cart, $totals);

            return $this->success(
                [
                'status' => 'success',
                'result' => [
                    'cart' => $transformed,
                ],
                ]
            );
        } catch (Throwable $e) {
            return $this->handleError($e, 'cart_operation_failed', __('Failed to clear cart.', 'bookiflex'));
        }
    }

    /**
     * POST /cart/payment-type/update - Change payment type
     *
     * Security: This endpoint is intentionally public and does not use WordPress nonces to support
     * guest checkout flow - users can browse and book without WordPress accounts. User identification
     * and cart binding is achieved through a secure session cookie (bflex_cart_session_id) managed by
     * SessionService. The session cookie contains a cryptographically secure ID (bflex_cart_ + 64 hex
     * characters) with httponly, samesite=Lax, and secure (SSL) flags, valid for 1 year.
     *
     * @param  WP_REST_Request $request
     * @return WP_REST_Response|WP_Error
     */
    public function changePaymentType(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        try {
            $changeRequest = ChangePaymentTypeRequest::fromWpRestRequest($request);

            $this->cartService->setSessionId($this->sessionService->getSessionId());
            $this->cartService->changePaymentType(
                $changeRequest->getItemHash(),
                $changeRequest->getPaymentTypeId()
            );

            $cart = $this->cartService->getCart();
            $totals = $this->cartService->calculateTotal();
            $transformed = $this->cartTransformer->transform($cart, $totals);

            return $this->success(
                [
                'status' => 'success',
                'result' => [
                    'cart' => $transformed,
                ],
                ]
            );
        } catch (CoreException $e) {
            $context = $e->getContext();
            $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, 'cart_operation_failed', __('Failed to change payment type', 'bookiflex'));
        }
    }

    /**
     * POST /cart/confirm - Confirm cart and create reservations
     *
     * Security: This endpoint is intentionally public and does not use WordPress nonces to support
     * guest checkout flow - users can browse and book without WordPress accounts. User identification
     * and cart binding is achieved through a secure session cookie (bflex_cart_session_id) managed by
     * SessionService. The session cookie contains a cryptographically secure ID (bflex_cart_ + 64 hex
     * characters) with httponly, samesite=Lax, and secure (SSL) flags, valid for 1 year.
     *
     * @param  WP_REST_Request $request
     * @return WP_REST_Response|WP_Error
     */
    public function confirmCart(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        try {
            $confirmRequest = ConfirmCartRequest::fromWpRestRequest($request);

            $this->cartService->setSessionId($this->sessionService->getSessionId());

            // Prepare cart for checkout
            $checkoutData = $confirmRequest->toCheckoutPreparationDTO();
            $this->cartService->prepareForCheckout($checkoutData);

            // Process checkout (validation happens inside checkout service)
            $result = $this->checkoutService->checkout(
                $this->cartService->getCart()
            );

            // Check if checkout was successful
            if (!$result->isSuccess()) {
                return $this->error(
                    'validation_failed',
                    __('Checkout validation failed', 'bookiflex'),
                    400,
                    ['params' => $result->getErrors()]
                );
            }

            // Clear cart after successful checkout
            $this->cartService->clear();

            return $this->success(
                [
                'status' => 'success',
                'result' => [
                    'reservations' => array_map(
                        fn (Reservation $reservation) => base64_encode($reservation->getSid()),
                        $result->getReservations()
                    ),
                    'message' => __('Booking confirmed successfully.', 'bookiflex'),
                ],
                ]
            );
        } catch (Throwable $e) {
            return $this->handleError($e, 'cart_operation_failed', __('Failed to confirm booking.', 'bookiflex'));
        }
    }

    /**
     * POST /cart/calculate - Calculate cart totals
     *
     * Security: This endpoint is intentionally public and does not use WordPress nonces to support
     * guest checkout flow - users can browse and book without WordPress accounts. User identification
     * and cart binding is achieved through a secure session cookie (bflex_cart_session_id) managed by
     * SessionService. The session cookie contains a cryptographically secure ID (bflex_cart_ + 64 hex
     * characters) with httponly, samesite=Lax, and secure (SSL) flags, valid for 1 year.
     *
     * @return WP_REST_Response|WP_Error
     */
    public function calculateTotal(): WP_REST_Response|WP_Error
    {
        try {
            $this->cartService->setSessionId($this->sessionService->getSessionId());
            $cart = $this->cartService->getCart(true);
            $totals = $this->cartService->calculateTotal();

            return $this->success(
                [
                'status' => 'success',
                'result' => $this->cartTotalsTransformer->transform($cart, $totals),
                ]
            );
        } catch (Throwable $e) {
            return $this->handleError($e, 'cart_operation_failed', __('Failed to calculate totals.', 'bookiflex'));
        }
    }

    /**
     * POST /cart/extras - Add an extra service to the cart, attached to a parent accommodation.
     *
     * The endpoint resolves the parent accommodation cart item by hash and snapshots its
     * guests/nights/currency into the extra item, then delegates to CartService.
     *
     * Returns 404 when no extras provider is registered (Extras extension is not active).
     *
     * @return WP_REST_Response|WP_Error
     */
    public function addExtra(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        try {
            $addRequest = AddCartExtraRequest::fromWpRestRequest($request);

            $this->cartService->setSessionId($this->sessionService->getSessionId());

            if (!$this->cartService->supportsType(ExtraCartItem::TYPE)) {
                return $this->error(
                    'extras_not_supported',
                    __('Extras are not available.', 'bookiflex'),
                    404
                );
            }

            $cart = $this->cartService->getCart(true);
            $parent = $this->findAccommodationByHash($cart, $addRequest->getParentHash());
            if ($parent === null) {
                return $this->error(
                    'parent_not_found',
                    __('Parent accommodation not found in cart.', 'bookiflex'),
                    404,
                    ['params' => ['parentHash' => $addRequest->getParentHash()]]
                );
            }

            $stay = $parent->getStayPeriod();
            $nights = max(
                1,
                (int) $stay->getCheckInDate()->diff($stay->getCheckOutDate())->days
            );
            $guests = $parent->getGuestConfiguration()->getAdults()
                + count($parent->getGuestConfiguration()->getChildren());

            $this->cartService->addItem([
                'type'       => ExtraCartItem::TYPE,
                'extraId'    => $addRequest->getExtraId(),
                'parentHash' => $addRequest->getParentHash(),
                'quantity'   => $addRequest->getQuantity(),
                'guests'     => $guests,
                'nights'     => $nights,
                'currency'   => $cart->getCurrency(),
            ]);

            $cart = $this->cartService->getCart();
            $totals = $this->cartService->calculateTotal();
            $transformed = $this->cartTransformer->transform($cart, $totals);

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

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

            $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, 'cart_operation_failed', __('Failed to add extra to cart.', 'bookiflex'));
        }
    }

    private function findAccommodationByHash(\BookiFlex\Extensions\Cart\Model\Cart $cart, string $hash): ?AccommodationCartItem
    {
        foreach ($cart->getItems() as $item) {
            if ($item->getHash() !== $hash) {
                continue;
            }
            $domain = $item->toDomainObject();
            if ($domain instanceof AccommodationCartItem) {
                return $domain;
            }
        }
        return null;
    }
}
