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

use BookiFlex\Core\Contract\CancellationPolicy\CancellationPolicyTypeInterface;
use BookiFlex\Core\Exception\InvalidArgumentException as DomainInvalidArgumentException;
use BookiFlex\Core\Util\DateTimeUtils;
use BookiFlex\Core\Exception\MoneyCalculationException;
use BookiFlex\Core\Model\Reservation\Reservation;
use Brick\Math\Exception\MathException;
use Brick\Math\Exception\NumberFormatException;
use Brick\Math\Exception\RoundingNecessaryException;
use Brick\Money\Exception\MoneyMismatchException;
use Brick\Money\Exception\UnknownCurrencyException;
use Brick\Money\Money;

defined( 'ABSPATH' ) || exit;

/**
 * Represents a cancellation rule with time interval and penalty configuration.
 *
 * Rule applies when cancellation happens within specified time interval before check-in.
 * Interval is defined as [minHoursUntilStart, maxHoursUntilStart).
 */
class CancellationRule
{
    /**
     * @param string $type Rule type @see CancellationPolicyTypeInterface
     * @param ?float $minHoursUntilStart Inclusive lower bound (null = -∞)
     * @param ?float $maxHoursUntilStart Exclusive upper bound (null = +∞)
     * @param ?int $penaltyPercentage Penalty percentage (0-100)
     * @param ?float $fixedPenalty Fixed penalty amount
     * @param ?int $penaltyInFirstNights Penalty in number of nights
     *
     * @throws DomainInvalidArgumentException
     */
    public function __construct(
        private readonly string $type,
        private readonly ?float $minHoursUntilStart,
        private readonly ?float $maxHoursUntilStart,
        private readonly ?int $penaltyPercentage,
        private readonly ?float $fixedPenalty,
        private readonly ?int $penaltyInFirstNights
    ) {
        // Validate interval bounds
        if ($minHoursUntilStart !== null && $maxHoursUntilStart !== null) {
            if ($minHoursUntilStart >= $maxHoursUntilStart) {
                throw new DomainInvalidArgumentException(
                    'minHoursUntilStart must be less than maxHoursUntilStart'
                );
            }
        }

        if ($penaltyPercentage !== null && ($penaltyPercentage < 0 || $penaltyPercentage > 100)) {
            throw new DomainInvalidArgumentException('penaltyPercentage must be between 0 and 100');
        }

        if ($fixedPenalty !== null && $fixedPenalty < 0) {
            throw new DomainInvalidArgumentException('fixedPenalty must be non-negative');
        }

        if ($penaltyInFirstNights !== null && $penaltyInFirstNights < 0) {
            throw new DomainInvalidArgumentException('penaltyInFirstNights must be non-negative');
        }
    }

    // ========================================================================
    // Factory Methods for Common Rules
    // ========================================================================

    /**
     * Create a rule with 0% penalty at any time (fully flexible).
     */
    public static function createFullyFlexible(): self
    {
        return new self(
            type: CancellationPolicyTypeInterface::PENALTY_AMOUNT_TYPE_PERCENTAGE,
            minHoursUntilStart: null,
            maxHoursUntilStart: null,
            penaltyPercentage: 0,
            fixedPenalty: null,
            penaltyInFirstNights: null
        );
    }

    /**
     * Create a rule with 100% penalty if cancelled within given hours (flexible).
     * Example: 24h before check-in.
     */
    public static function createFlexible(int $hoursBefore = 24): self
    {
        return new self(
            type: CancellationPolicyTypeInterface::PENALTY_AMOUNT_TYPE_PERCENTAGE,
            minHoursUntilStart: null,
            maxHoursUntilStart: $hoursBefore,
            penaltyPercentage: 100,
            fixedPenalty: null,
            penaltyInFirstNights: null
        );
    }

    /**
     * Create a rule with 50% penalty if cancelled within given hours (moderate).
     * Example: 48h before check-in.
     */
    public static function createModerate(int $hoursBefore = 48, int $penaltyPercent = 50): self
    {
        return new self(
            type: CancellationPolicyTypeInterface::PENALTY_AMOUNT_TYPE_PERCENTAGE,
            minHoursUntilStart: null,
            maxHoursUntilStart: $hoursBefore,
            penaltyPercentage: $penaltyPercent,
            fixedPenalty: null,
            penaltyInFirstNights: null
        );
    }

    /**
     * Create a strict rule with configurable time window and penalty.
     *
     * @param int $hoursBefore Hours before check-in (24, 48, 72, 168)
     * @param int $penaltyPercent Penalty percentage (20, 50, 100)
     */
    public static function createStrict(int $hoursBefore = 168, int $penaltyPercent = 100): self
    {
        return new self(
            type: CancellationPolicyTypeInterface::PENALTY_AMOUNT_TYPE_PERCENTAGE,
            minHoursUntilStart: null,
            maxHoursUntilStart: $hoursBefore,
            penaltyPercentage: $penaltyPercent,
            fixedPenalty: null,
            penaltyInFirstNights: null
        );
    }

    /**
     * Create a rule with penalty equal to the first night (common for hotels/hostels).
     */
    public static function createFirstNightPenalty(): self
    {
        return new self(
            type: CancellationPolicyTypeInterface::PENALTY_AMOUNT_FIRST_NIGHT,
            minHoursUntilStart: null,
            maxHoursUntilStart: null,
            penaltyPercentage: null,
            fixedPenalty: null,
            penaltyInFirstNights: 1
        );
    }

    /**
     * Create a rule that always applies with full penalty (non-refundable).
     */
    public static function createNonRefundable(): self
    {
        return new self(
            type: CancellationPolicyTypeInterface::PENALTY_AMOUNT_TYPE_PERCENTAGE,
            minHoursUntilStart: null,
            maxHoursUntilStart: null,
            penaltyPercentage: 100,
            fixedPenalty: null,
            penaltyInFirstNights: null
        );
    }

    // ========================================================================
    // Public Getters
    // ========================================================================

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

    /**
     * Get minimum hours until start (inclusive lower bound).
     */
    public function getMinHoursUntilStart(): ?float
    {
        return $this->minHoursUntilStart;
    }

    /**
     * Get maximum hours until start (exclusive upper bound).
     */
    public function getMaxHoursUntilStart(): ?float
    {
        return $this->maxHoursUntilStart;
    }

    /**
     * Get penalty percentage if applicable.
     */
    public function getPenaltyPercentage(): ?int
    {
        return $this->penaltyPercentage;
    }

    /**
     * Get fixed penalty amount if applicable.
     */
    public function getFixedPenalty(): ?float
    {
        return $this->fixedPenalty;
    }

    /**
     * Get penalty in number of first nights if applicable.
     */
    public function getPenaltyInFirstNights(): ?int
    {
        return $this->penaltyInFirstNights;
    }

    // ========================================================================
    // Rule Application Methods
    // ========================================================================

    /**
     * Check if rule applies for given cancellation timing.
     * Rule applies if: minHours <= hoursUntilStart < maxHours
     *
     * @param \DateTimeImmutable $checkInDateTime Check-in date and time
     * @param \DateTimeImmutable $cancellationDateTime Cancellation date and time
     * @return bool
     */
    public function appliesTo(
        \DateTimeImmutable $checkInDateTime,
        \DateTimeImmutable $cancellationDateTime
    ): bool {
        $hoursUntilStart = ($checkInDateTime->getTimestamp() - $cancellationDateTime->getTimestamp()) / 3600;

        // Check lower bound (inclusive)
        if ($this->minHoursUntilStart !== null && $hoursUntilStart < $this->minHoursUntilStart) {
            return false;
        }

        // Check upper bound (exclusive)
        if ($this->maxHoursUntilStart !== null && $hoursUntilStart >= $this->maxHoursUntilStart) {
            return false;
        }

        return true;
    }

    /**
     * Calculate penalty amount for this rule.
     *
     * @throws MoneyCalculationException
     */
    public function calculatePenalty(Reservation $reservation): Money
    {
        try {
            $amount = $reservation->getCancellationBaseSummary($this->penaltyInFirstNights);

            switch ($this->type) {
                case CancellationPolicyTypeInterface::PENALTY_AMOUNT_FIRST_NIGHT:
                    return $amount;

                case CancellationPolicyTypeInterface::PENALTY_AMOUNT_TYPE_PERCENTAGE:
                    return $amount->multipliedBy($this->penaltyPercentage / 100);

                case CancellationPolicyTypeInterface::PENALTY_AMOUNT_TYPE_FIXED:
                    return Money::of($this->fixedPenalty, $amount->getCurrency());

                case CancellationPolicyTypeInterface::PENALTY_AMOUNT_TYPE_FIXED_PERCENTAGE:
                    $penalty = $amount->multipliedBy($this->penaltyPercentage / 100);
                    if ($this->fixedPenalty) {
                        $penalty = $penalty->plus(Money::of($this->fixedPenalty, $penalty->getCurrency()));
                    }
                    return $penalty;

                case CancellationPolicyTypeInterface::PENALTY_AMOUNT_TYPE_FIXED_FIRST_NIGHTS:
                    return $amount->plus(Money::of($this->fixedPenalty, $amount->getCurrency()));

                default:
                    return Money::zero($amount->getCurrency());
            }
        } catch (MoneyMismatchException|
        UnknownCurrencyException|
        RoundingNecessaryException|
        MathException|
        NumberFormatException $e) {
            throw new MoneyCalculationException(
                sprintf(
                    'Failed to calculate money: %s',
                    $e->getMessage()
                ),
                0,
                $e,
                ['error' => $e->getMessage()]
            );
        }
    }

    // ========================================================================
    // Rule Analysis Methods
    // ========================================================================

    /**
     * Check if this is a free cancellation rule (no penalties).
     */
    public function isFreeRule(): bool
    {
        return ($this->penaltyPercentage === null || $this->penaltyPercentage === 0)
            && ($this->fixedPenalty === null || $this->fixedPenalty === 0.0)
            && ($this->penaltyInFirstNights === null || $this->penaltyInFirstNights === 0);
    }

    /**
     * Check if this rule always applies (covers entire timeline).
     */
    public function isAlwaysApplicable(): bool
    {
        return $this->minHoursUntilStart === null && $this->maxHoursUntilStart === null;
    }

    /**
     * Check if this is a non-refundable rule (always applies with 100% penalty).
     */
    public function isNonRefundable(): bool
    {
        return $this->isAlwaysApplicable() &&
            $this->penaltyPercentage !== null &&
            $this->penaltyPercentage >= 100;
    }

    /**
     * Check if rule has any penalty.
     */
    public function hasPenalty(): bool
    {
        return ($this->penaltyPercentage !== null && $this->penaltyPercentage > 0)
            || ($this->fixedPenalty !== null && $this->fixedPenalty > 0)
            || ($this->penaltyInFirstNights !== null && $this->penaltyInFirstNights > 0);
    }

    /**
     * Get penalty summary for display.
     *
     * @return array Summary with type and values
     */
    public function getPenaltySummary(): array
    {
        $summary = [
            'type' => $this->type,
            'has_penalty' => $this->hasPenalty(),
            'interval' => [
                'min' => $this->minHoursUntilStart,
                'max' => $this->maxHoursUntilStart,
            ],
        ];

        if ($this->penaltyPercentage !== null && $this->penaltyPercentage > 0) {
            $summary['percentage'] = $this->penaltyPercentage;
        }

        if ($this->fixedPenalty !== null && $this->fixedPenalty > 0) {
            $summary['fixed_amount'] = $this->fixedPenalty;
        }

        if ($this->penaltyInFirstNights !== null && $this->penaltyInFirstNights > 0) {
            $summary['nights'] = $this->penaltyInFirstNights;
        }

        return $summary;
    }

    /**
     * Get sorting priority for rules.
     * Lower values = higher priority (checked first).
     *
     * @return float Priority value for sorting
     */
    public function getSortingPriority(): float
    {
        // Rules with smaller intervals (closer to check-in) have higher priority
        // Non-refundable (always applicable) has lowest priority

        if ($this->isAlwaysApplicable()) {
            return PHP_FLOAT_MAX; // Lowest priority
        }

        // Use minHours as primary sorting key (null = -∞ = highest priority)
        if ($this->minHoursUntilStart === null) {
            return -PHP_FLOAT_MAX;
        }

        return $this->minHoursUntilStart;
    }

    // ========================================================================
    // Description Generation
    // ========================================================================

    /**
     * Generate translated description for the rule.
     *
     * @param \DateTimeImmutable $checkInDateTime Check-in date and time
     * @param bool $detailed If true, includes exact dates; if false, uses relative time periods
     * @return string Translated and formatted description
     */
    public function getDescription(\DateTimeImmutable $checkInDateTime, bool $detailed = false): string
    {
        // Special cases first
        if ($this->isNonRefundable()) {
            return __('Non-refundable rate: 100% penalty applies at any time', 'bookiflex');
        }

        if ($this->isFreeRule()) {
            return $this->getFreeCancellationDescription($checkInDateTime, $detailed);
        }

        // Regular penalty rules - combine interval and penalty in one message
        return $this->getCombinedPenaltyDescription($checkInDateTime, $detailed);
    }

    /**
     * Get translated free cancellation description.
     */
    private function getFreeCancellationDescription(\DateTimeImmutable $checkInDateTime, bool $detailed): string
    {
        if ($this->isAlwaysApplicable()) {
            return __('Free cancellation at any time', 'bookiflex');
        }

        if ($this->maxHoursUntilStart === null) {
            if ($detailed) {
                $date = $checkInDateTime->modify("-{$this->minHoursUntilStart} hours")->format('Y-m-d H:i');
                return sprintf(
                    /* translators: %s: date and time */
                    __('Free cancellation from %s onwards', 'bookiflex'),
                    $date
                );
            }
            return sprintf(
                /* translators: %s: formatted duration (e.g. "2 days", "12 hours") */
                __('Free cancellation if cancelled more than %s before check-in', 'bookiflex'),
                DateTimeUtils::formatHoursDuration((int) $this->minHoursUntilStart)
            );
        }

        if ($this->minHoursUntilStart === null || $this->minHoursUntilStart == 0) {
            if ($detailed) {
                $date = $checkInDateTime->modify("-{$this->maxHoursUntilStart} hours")->format('Y-m-d H:i');
                return sprintf(
                    /* translators: %s: date and time */
                    __('Free cancellation until %s', 'bookiflex'),
                    $date
                );
            }
            return sprintf(
                /* translators: %s: formatted duration (e.g. "2 days", "12 hours") */
                __('Free cancellation if cancelled more than %s before check-in', 'bookiflex'),
                DateTimeUtils::formatHoursDuration((int) $this->maxHoursUntilStart)
            );
        }

        if ($detailed) {
            $minDate = $checkInDateTime->modify("-{$this->maxHoursUntilStart} hours")->format('Y-m-d H:i');
            $maxDate = $checkInDateTime->modify("-{$this->minHoursUntilStart} hours")->format('Y-m-d H:i');
            return sprintf(
                /* translators: %1$s: start date and time, %2$s: end date and time */
                __('Free cancellation between %1$s and %2$s', 'bookiflex'),
                $minDate,
                $maxDate
            );
        }
        return sprintf(
            /* translators: %1$s: formatted duration, %2$s: formatted duration */
            __('Free cancellation between %1$s and %2$s before check-in', 'bookiflex'),
            DateTimeUtils::formatHoursDuration((int) $this->minHoursUntilStart),
            DateTimeUtils::formatHoursDuration((int) $this->maxHoursUntilStart)
        );
    }

    /**
     * Get translated combined penalty description (interval + penalty amount).
     */
    private function getCombinedPenaltyDescription(\DateTimeImmutable $checkInDateTime, bool $detailed): string
    {
        $penalty = $this->getPenaltyAmountText();

        // Special case: no-show (after check-in)
        if ($this->maxHoursUntilStart !== null && $this->maxHoursUntilStart <= 0) {
            if ($detailed) {
                $date = $checkInDateTime->format('Y-m-d H:i');
                return sprintf(
                    /* translators: %1$s: penalty description, %2$s: date and time */
                    __('%1$s for no-show or cancellation after %2$s', 'bookiflex'),
                    $penalty,
                    $date
                );
            }
            return sprintf(
                /* translators: %s: penalty description */
                __('%s for no-show (after check-in time)', 'bookiflex'),
                $penalty
            );
        }

        // Within X hours of check-in
        if ($this->minHoursUntilStart === null || $this->minHoursUntilStart == 0) {
            if ($this->maxHoursUntilStart !== null) {
                if ($detailed) {
                    $date = $checkInDateTime->modify("-{$this->maxHoursUntilStart} hours")->format('Y-m-d H:i');
                    return sprintf(
                        /* translators: %1$s: penalty description, %2$s: date and time */
                        __('%1$s if cancelled after %2$s', 'bookiflex'),
                        $penalty,
                        $date
                    );
                }
                return sprintf(
                    /* translators: %1$s: penalty description, %2$s: formatted duration */
                    __('%1$s if cancelled within %2$s of check-in', 'bookiflex'),
                    $penalty,
                    DateTimeUtils::formatHoursDuration((int) $this->maxHoursUntilStart)
                );
            }
        }

        // From X hours onwards
        if ($this->maxHoursUntilStart === null && $this->minHoursUntilStart !== null) {
            if ($detailed) {
                $date = $checkInDateTime->modify("-{$this->minHoursUntilStart} hours")->format('Y-m-d H:i');
                return sprintf(
                    /* translators: %1$s: penalty description, %2$s: date and time */
                    __('%1$s if cancelled after %2$s', 'bookiflex'),
                    $penalty,
                    $date
                );
            }
            return sprintf(
                /* translators: %1$s: penalty description, %2$s: formatted duration */
                __('%1$s if cancelled within %2$s of check-in', 'bookiflex'),
                $penalty,
                DateTimeUtils::formatHoursDuration((int) $this->minHoursUntilStart)
            );
        }

        // Between X and Y hours
        if ($this->minHoursUntilStart !== null && $this->maxHoursUntilStart !== null) {
            if ($detailed) {
                $minDate = $checkInDateTime->modify("-{$this->maxHoursUntilStart} hours")->format('Y-m-d H:i');
                $maxDate = $checkInDateTime->modify("-{$this->minHoursUntilStart} hours")->format('Y-m-d H:i');
                return sprintf(
                    /* translators: %1$s: penalty description, %2$s: start date and time, %3$s: end date and time */
                    __('%1$s if cancelled between %2$s and %3$s', 'bookiflex'),
                    $penalty,
                    $minDate,
                    $maxDate
                );
            }
            return sprintf(
                /* translators: %1$s: penalty description, %2$s: formatted duration, %3$s: formatted duration */
                __('%1$s if cancelled between %2$s and %3$s before check-in', 'bookiflex'),
                $penalty,
                DateTimeUtils::formatHoursDuration((int) $this->minHoursUntilStart),
                DateTimeUtils::formatHoursDuration((int) $this->maxHoursUntilStart)
            );
        }

        // Fallback
        return $penalty;
    }

    /**
     * Get penalty amount as formatted translated text.
     *
     * @return string Formatted penalty text (e.g., "50% charge + $100 fee")
     */
    private function getPenaltyAmountText(): string
    {
        $parts = [];

        if ($this->penaltyPercentage !== null && $this->penaltyPercentage > 0) {
            $parts[] = sprintf(
                /* translators: %d: percentage */
                __('%d%% charge', 'bookiflex'),
                $this->penaltyPercentage
            );
        }

        if ($this->fixedPenalty !== null && $this->fixedPenalty > 0) {
            $parts[] = sprintf(
                /* translators: %1$s: currency symbol, %2$s: penalty amount */
                __('%1$s%2$s fee', 'bookiflex'),
                '', // currency symbol will be added later if needed
                $this->fixedPenalty
            );
        }

        if ($this->penaltyInFirstNights !== null && $this->penaltyInFirstNights > 0) {
            if ($this->penaltyInFirstNights === 1) {
                $parts[] = sprintf(
                    /* translators: %d: number of nights */
                    __('%d night charge', 'bookiflex'),
                    $this->penaltyInFirstNights
                );
            } else {
                $parts[] = sprintf(
                    /* translators: %d: number of nights */
                    __('%d nights charge', 'bookiflex'),
                    $this->penaltyInFirstNights
                );
            }
        }

        if (empty($parts)) {
            return __('Penalty', 'bookiflex');
        }

        return implode(' + ', $parts);
    }
}
