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

use BookiFlex\Core\Contract\Adjustment\AdjustableInterface;
use BookiFlex\Core\Contract\Adjustment\AdjustmentInterface;
use BookiFlex\Core\Contract\Adjustment\AdjustmentMode;
use BookiFlex\Core\Contract\Adjustment\AdjustmentStrategy;
use BookiFlex\Core\Contract\Adjustment\AdjustmentType;
use BookiFlex\Core\Model\Identifiable;
use BookiFlex\Core\Model\Timestamps;
use BookiFlex\Core\Util\DateTimeUtils;

defined( 'ABSPATH' ) || exit;

/**
 * Represents a pricing adjustment (tax, discount, or charge) applied to reservations or items.
 *
 * Adjustments modify the base price through:
 * - **Type**: Tax, Discount, or Charge
 * - **Mode**: Percentage or Fixed amount
 * - **Strategy**: Exclusive (added to price) or Inclusive (already in price)
 *
 * ## Examples:
 * - VAT 20% exclusive: Adds 20% to the base price
 * - Early bird 10% discount: Reduces price by 10%
 * - Cleaning fee $50: Adds fixed $50 charge
 * - GST inclusive: Tax already included in base price
 *
 * @package BookiFlex\Core\Model\Reservation
 */
class Adjustment implements AdjustmentInterface
{
    use Timestamps;
    use Identifiable;

    /** @var AdjustableInterface|null The entity this adjustment is applied to */
    private ?AdjustableInterface $adjustable = null;

    /**
     * Create a new pricing adjustment.
     *
     * @param string $name Display name (e.g., "Value Added Tax", "Early Bird Discount")
     * @param string $code Short code identifier (e.g., "VAT", "GST", "EARLY10")
     * @param float $amount Amount value (percentage or fixed based on mode)
     * @param AdjustmentType $type Type of adjustment (tax, discount, charge)
     * @param AdjustmentStrategy $strategy How to apply (exclusive/inclusive)
     * @param AdjustmentMode $mode Calculation mode (percentage/fixed)
     * @param string $source Module/system that created this adjustment
     * @param bool $refundable Whether included in refund calculations
     * @param bool $active Whether currently applied
     */
    public function __construct(
        private string $name,
        private string $code,
        private float $amount,
        private AdjustmentType $type,
        private AdjustmentStrategy $strategy,
        private AdjustmentMode $mode,
        private string $source,
        private bool $refundable = false,
        private bool $active = true
    ) {
        // Force exclusive strategy for non-tax adjustments
        // (Only taxes can be inclusive)
        if ($type !== AdjustmentType::Tax) {
            $this->strategy = AdjustmentStrategy::Exclusive;
        }

        // Ensure amount is always positive
        $this->amount = abs($amount);

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

    // ========================================================================
    // Factory Methods
    // ========================================================================

    /**
     * Create adjustment from array data.
     *
     * @param array $data Adjustment data with required keys:
     *   - name: string
     *   - code: string
     *   - amount: float
     *   - type: string ('tax', 'discount', 'charge')
     *   - strategy: string ('exclusive', 'inclusive')
     *   - mode: string ('percentage', 'fixed')
     *   - source: string
     *   - refundable: bool (optional)
     *
     * @return self
     * @throws \InvalidArgumentException If required data missing
     */
    public static function fromArray(array $data): self
    {
        $required = ['name', 'code', 'amount', 'type', 'strategy', 'mode', 'source'];
        $missing = array_diff($required, array_keys($data));

        if (!empty($missing)) {
            throw new \InvalidArgumentException(
                'Missing required adjustment data: ' . implode(', ', $missing)
            );
        }

        return new self(
            $data['name'],
            $data['code'],
            (float) $data['amount'],
            AdjustmentType::tryFrom($data['type']),
            AdjustmentStrategy::tryFrom($data['strategy']),
            AdjustmentMode::tryFrom($data['mode']),
            $data['source'],
            $data['refundable'] ?? false,
            $data['active'] ?? true
        );
    }

    /**
     * Create a percentage tax adjustment.
     *
     * @param string $name Tax name
     * @param string $code Tax code
     * @param float $percentage Tax percentage (e.g., 20.0 for 20%)
     * @param string $source Source module
     * @param bool $inclusive Whether tax is inclusive
     * @return self
     */
    public static function percentageTax(
        string $name,
        string $code,
        float $percentage,
        string $source,
        bool $inclusive = false
    ): self {
        return new self(
            $name,
            $code,
            $percentage,
            AdjustmentType::Tax,
            $inclusive ? AdjustmentStrategy::Inclusive : AdjustmentStrategy::Exclusive,
            AdjustmentMode::Percentage,
            $source,
            false,
            true
        );
    }

    /**
     * Create a percentage discount adjustment.
     *
     * @param string $name Discount name
     * @param string $code Discount code
     * @param float $percentage Discount percentage
     * @param string $source Source module
     * @param bool $refundable Whether refundable
     * @return self
     */
    public static function percentageDiscount(
        string $name,
        string $code,
        float $percentage,
        string $source,
        bool $refundable = true
    ): self {
        return new self(
            $name,
            $code,
            $percentage,
            AdjustmentType::Discount,
            AdjustmentStrategy::Exclusive,
            AdjustmentMode::Percentage,
            $source,
            $refundable,
            true
        );
    }

    /**
     * Create a fixed charge adjustment.
     *
     * @param string $name Charge name
     * @param string $code Charge code
     * @param float $amount Fixed amount
     * @param string $source Source module
     * @param bool $refundable Whether refundable
     * @return self
     */
    public static function fixedCharge(
        string $name,
        string $code,
        float $amount,
        string $source,
        bool $refundable = false
    ): self {
        return new self(
            $name,
            $code,
            $amount,
            AdjustmentType::Charge,
            AdjustmentStrategy::Exclusive,
            AdjustmentMode::Fixed,
            $source,
            $refundable,
            true
        );
    }

    // ========================================================================
    // Properties Access
    // ========================================================================

    /**
     * Get adjustment display name.
     */
    public function getName(): string
    {
        return $this->name;
    }

    /**
     * Get adjustment code identifier.
     */
    public function getCode(): string
    {
        return $this->code;
    }

    /**
     * Get adjustment amount.
     *
     * @return float Percentage (0-100) or fixed amount in minor units
     */
    public function getAmount(): float
    {
        return $this->amount;
    }

    /**
     * Get adjustment type.
     */
    public function getType(): AdjustmentType
    {
        return $this->type;
    }

    /**
     * Get application strategy.
     */
    public function getStrategy(): AdjustmentStrategy
    {
        return $this->strategy;
    }

    /**
     * Get calculation mode.
     */
    public function getMode(): AdjustmentMode
    {
        return $this->mode;
    }

    /**
     * Get source module/system.
     */
    public function getSource(): string
    {
        return $this->source;
    }

    // ========================================================================
    // State Management
    // ========================================================================

    /**
     * Check if adjustment is currently active.
     */
    public function isActive(): bool
    {
        return $this->active;
    }

    /**
     * Activate or deactivate adjustment.
     *
     * @param bool $active New state
     * @return $this
     */
    public function setActive(bool $active): self
    {
        $this->active = $active;
        return $this;
    }

    /**
     * Check if adjustment is refundable.
     */
    public function isRefundable(): bool
    {
        return $this->refundable;
    }

    /**
     * Set refundable status.
     *
     * @param bool $refundable New status
     * @return $this
     */
    public function setRefundable(bool $refundable): self
    {
        $this->refundable = $refundable;
        return $this;
    }

    // ========================================================================
    // Relationship Management
    // ========================================================================

    /**
     * Get the entity this adjustment is applied to.
     */
    public function getAdjustable(): ?AdjustableInterface
    {
        return $this->adjustable;
    }

    /**
     * Set the entity this adjustment is applied to.
     *
     * @param AdjustableInterface|null $adjustable Target entity
     * @return $this
     */
    public function setAdjustable(?AdjustableInterface $adjustable): self
    {
        $this->adjustable = $adjustable;
        return $this;
    }

    // ========================================================================
    // Type Checking Helpers
    // ========================================================================

    /**
     * Check if this is a tax adjustment.
     */
    public function isTax(): bool
    {
        return $this->type === AdjustmentType::Tax;
    }

    /**
     * Check if this is a discount adjustment.
     */
    public function isDiscount(): bool
    {
        return $this->type === AdjustmentType::Discount;
    }

    /**
     * Check if this is a charge adjustment.
     */
    public function isCharge(): bool
    {
        return $this->type === AdjustmentType::Charge;
    }

    /**
     * Check if adjustment uses percentage mode.
     */
    public function isPercentage(): bool
    {
        return $this->mode === AdjustmentMode::Percentage;
    }

    /**
     * Check if adjustment uses fixed amount mode.
     */
    public function isFixed(): bool
    {
        return $this->mode === AdjustmentMode::Fixed;
    }

    /**
     * Check if adjustment is inclusive (already in base price).
     */
    public function isInclusive(): bool
    {
        return $this->strategy === AdjustmentStrategy::Inclusive;
    }

    /**
     * Check if adjustment is exclusive (added to base price).
     */
    public function isExclusive(): bool
    {
        return $this->strategy === AdjustmentStrategy::Exclusive;
    }

    // ========================================================================
    // Serialization
    // ========================================================================

    /**
     * Convert adjustment to array representation.
     *
     * @return array Adjustment data
     */
    public function toArray(): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'code' => $this->code,
            'amount' => $this->amount,
            'type' => $this->type->value,
            'strategy' => $this->strategy->value,
            'mode' => $this->mode->value,
            'source' => $this->source,
            'refundable' => $this->refundable,
            'active' => $this->active,
            'created_at' => $this->createdAt->format('Y-m-d H:i:s'),
            'updated_at' => $this->updatedAt?->format('Y-m-d H:i:s'),
        ];
    }

    /**
     * Get human-readable description of the adjustment.
     *
     * @return string Description like "VAT 20% (exclusive)" or "Cleaning fee $50"
     */
    public function getDescription(): string
    {
        $description = $this->name . ' ';

        if ($this->isPercentage()) {
            $description .= $this->amount . '%';
        } else {
            $description .= number_format($this->amount / 100, 2);
        }

        if ($this->isTax() && $this->isInclusive()) {
            $description .= ' (inclusive)';
        }

        return $description;
    }
}
