<?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\Model;

use BookiFlex\Extensions\Cart\Contract\CartItemInterface;
use BookiFlex\Core\Model\Customer\Customer;
use BookiFlex\Core\Model\Identifiable;
use BookiFlex\Core\Model\Timestamps;
use BookiFlex\Core\Util\DateTimeUtils;
use DateTimeImmutable;
use InvalidArgumentException;

defined( 'ABSPATH' ) || exit;

class Cart
{
    use Timestamps;
    use Identifiable;

    private string $sessionId;
    private string $currency;
    private string $status;
    private ?string $promoCode = null;
    private ?string $specialRequest = null;
    private ?string $arrivalTime = null;
    private string $checkInTime;
    private string $checkOutTime;
    /** @var CartItem[] */
    private array $items = [];
    private ?Customer $customer = null;

    private function __construct(
        string $sessionId,
        string $currency,
        string $checkInTime,
        string $checkOutTime
    ) {
        $this->sessionId = $sessionId;
        $this->currency = $currency;
        $this->checkInTime = $checkInTime;
        $this->checkOutTime = $checkOutTime;
        $this->createdAt = DateTimeUtils::nowUtc();
        $this->status = 'pending';
    }

    public static function create(
        string $sessionId,
        string $currency,
        string $checkInTime,
        string $checkOutTime
    ): self {
        return new self($sessionId, $currency, $checkInTime, $checkOutTime);
    }

    /**
     * Add or update item in cart
     */
    public function addItem(CartItemInterface $item, int $quantity): void
    {
        $hash = $item->getHash();

        // Check if similar item exists
        foreach ($this->items as $existingItem) {
            if ($existingItem->getHash() === $hash) {
                // Update quantity instead of adding duplicate
                $existingItem->setQuantity($quantity);
                // Change domain model's data
                $existingItem->setData($item->toArray());
                $this->updatedAt = DateTimeUtils::nowUtc();
                return;
            }
        }

        // Add new item
        $cartItem = CartItem::create($this, $item, $quantity);
        $this->items[] = $cartItem;
        $this->updatedAt = DateTimeUtils::nowUtc();
    }

    /**
     * Remove item from cart
     */
    public function removeItem(string $hash): bool
    {
        foreach ($this->items as $key => $item) {
            if ($item->getHash() === $hash) {
                unset($this->items[$key]);
                $this->items = array_values($this->items);
                $this->updatedAt = DateTimeUtils::nowUtc();
                return true;
            }
        }

        return false;
    }

    /**
     * Update item quantity
     */
    public function updateItemQuantity(string $hash, int $quantity): bool
    {
        if ($quantity <= 0) {
            return $this->removeItem($hash);
        }

        foreach ($this->items as $item) {
            if ($item->getHash() === $hash) {
                $item->setQuantity($quantity);
                $this->updatedAt = DateTimeUtils::nowUtc();
                return true;
            }
        }

        return false;
    }

    /**
     * Get all items of specific type
     * @return CartItem[]
     */
    public function getItemsByType(string $type): array
    {
        return array_filter(
            $this->items,
            fn(CartItem $item) => $item->getType() === $type
        );
    }

    /**
     * Get all cart items
     */
    public function getItems(): array
    {
        return $this->items;
    }

    /**
     * Get total price for cart
     */
    public function getTotalPrice(): int
    {
        $total = 0;
        foreach ($this->items as $item) {
            $total += $item->getTotalPrice();
        }
        return $total;
    }

    /**
     * Get reconstructed items as domain objects
     * @return CartItemInterface[]
     */
    public function getDomainItems(): array
    {
        $domainItems = [];
        foreach ($this->items as $cartItem) {
            $domainItems[] = $cartItem->toDomainObject();
        }
        return $domainItems;
    }

    /**
     * Reset the cart to a new currency.
     *
     * Adopts the given currency and drops the current contents, which were priced
     * in the previous currency. Used when the site currency changes while a cart is
     * still open, so the cart never mixes currencies.
     */
    public function resetCurrency(string $currency): void
    {
        $this->currency = $currency;
        $this->clear();
    }

    /**
     * Clear all items
     */
    public function clear(): void
    {
        $this->items = [];
        $this->customer = null;
        $this->promoCode = null;
        $this->specialRequest = null;
        $this->arrivalTime = null;
        $this->status = 'pending';
        $this->updatedAt = DateTimeUtils::nowUtc();
    }

    /**
     * Check if cart is empty
     */
    public function isEmpty(): bool
    {
        return count($this->items) === 0;
    }

    /**
     * Get total items count
     */
    public function getTotalQuantity(): int
    {
        $total = 0;
        foreach ($this->items as $item) {
            $total += $item->getQuantity();
        }
        return $total;
    }

    /**
     * Check if cart is expired
     */
    public function isExpired(int $ttl = 3600): bool
    {
        $expirationTime = (new DateTimeImmutable())->modify("-{$ttl} seconds");
        $lastUpdate = $this->updatedAt ?? $this->createdAt;

        return $lastUpdate < $expirationTime;
    }

    /**
     * Validate all items in cart
     */
    public function validate(): array
    {
        $errors = [];

        if ($this->isEmpty()) {
            $errors['cart_empty'] = 'Cart is empty';
        }

        foreach ($this->items as $index => $item) {
            $domainItem = $item->toDomainObject();
            if (!$domainItem->validate()) {
                // Flatten item validation errors with item index prefix
                foreach ($domainItem->getValidationErrors() as $code => $message) {
                    $errors["item_{$index}_{$code}"] = $message;
                }
            }
        }

        if (!$this->customer && $this->status === 'checkout') {
            $errors['customer_required'] = 'Customer information is required';
        }

        return $errors;
    }

    /**
     * Mark cart as being checked out
     */
    public function startCheckout(): void
    {
        $this->status = 'checkout';
        $this->updatedAt = DateTimeUtils::nowUtc();
    }

    /**
     * Mark cart as completed
     */
    public function complete(): void
    {
        $this->status = 'completed';
        $this->updatedAt = DateTimeUtils::nowUtc();
    }

    /**
     * Mark cart as abandoned
     */
    public function abandon(): void
    {
        $this->status = 'abandoned';
        $this->updatedAt = DateTimeUtils::nowUtc();
    }

    // Getters and setters

    public function getSessionId(): string
    {
        return $this->sessionId;
    }

    public function getCurrency(): string
    {
        return $this->currency;
    }

    public function getStatus(): string
    {
        return $this->status;
    }

    public function getPromoCode(): ?string
    {
        return $this->promoCode;
    }

    public function setPromoCode(?string $promoCode): void
    {
        $this->promoCode = $promoCode;
        $this->updatedAt = DateTimeUtils::nowUtc();
    }

    public function getSpecialRequest(): ?string
    {
        return $this->specialRequest;
    }

    public function setSpecialRequest(?string $specialRequest): void
    {
        $value = $specialRequest ? trim($specialRequest) : null;
        $this->specialRequest = $value ? mb_substr($value, 0, 500) : null;
        $this->updatedAt = DateTimeUtils::nowUtc();
    }

    public function getArrivalTime(): ?string
    {
        return $this->arrivalTime;
    }

    public function setArrivalTime(?string $arrivalTime): void
    {
        if ($arrivalTime && !preg_match('/^([01]\d|2[0-3]):([0-5]\d)$/', $arrivalTime)) {
            throw new InvalidArgumentException('Invalid time format. Expected HH:MM.');
        }

        $this->arrivalTime = $arrivalTime;
        $this->updatedAt = DateTimeUtils::nowUtc();
    }

    public function getCheckInTime(): string
    {
        return $this->checkInTime;
    }

    public function getCheckOutTime(): string
    {
        return $this->checkOutTime;
    }

    public function getCustomer(): ?Customer
    {
        return $this->customer;
    }

    public function setCustomer(?Customer $customer): void
    {
        $this->customer = $customer;
        $this->updatedAt = DateTimeUtils::nowUtc();
    }
}
