<?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\CancellationPolicyInterface;
use BookiFlex\Core\Exception\InvalidArgumentException as DomainInvalidArgumentException;
use BookiFlex\Core\Exception\MoneyCalculationException;
use BookiFlex\Core\Model\Identifiable;
use BookiFlex\Core\Model\Penalty\Penalty;
use BookiFlex\Core\Model\Penalty\PenaltyStatus;
use BookiFlex\Core\Model\Reservation\Reservation;
use BookiFlex\Core\Util\DateTimeUtils;

defined( 'ABSPATH' ) || exit;

/**
 * Represents a cancellation policy with rules that define penalties.
 *
 * Policy contains multiple rules that are evaluated based on timing
 * relative to check-in date. The first applicable rule is used
 * to calculate penalties.
 */
class CancellationPolicy implements CancellationPolicyInterface
{
    use Identifiable;

    /**
     * @param string $name Policy name
     * @param CancellationRule[] $rules Array of cancellation rules
     * @param bool $active Whether policy is active
     *
     * @throws DomainInvalidArgumentException
     */
    public function __construct(
        private readonly string $name,
        private readonly string $description,
        private array $rules,
        private readonly bool $active
    ) {
        if (empty($name)) {
            throw new DomainInvalidArgumentException('Name must not be empty.');
        }
        if (empty($rules)) {
            throw new DomainInvalidArgumentException('Rules must be a non-empty array.');
        }
    }

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

    public function getDescription(): string
    {
        return $this->description;
    }

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

    /**
     * Get cancellation rules.
     *
     * @return CancellationRule[]
     */
    public function getRules(): array
    {
        return $this->rules;
    }

    /**
     * Get rules sorted by interval priority.
     * Rules with more specific intervals come first.
     *
     * @return CancellationRule[]
     */
    public function getRulesSortedByPriority(): array
    {
        $sorted = $this->rules;
        usort($sorted, fn(CancellationRule $a, CancellationRule $b) =>
            $a->getSortingPriority() <=> $b->getSortingPriority()
        );
        return $sorted;
    }

    /**
     * Get rules sorted by minimum hours (ascending).
     * Rules closer to check-in come first.
     *
     * @return CancellationRule[]
     */
    public function getRulesSortedByProximity(): array
    {
        $sorted = $this->rules;
        usort($sorted, function (CancellationRule $a, CancellationRule $b) {
            $aMin = $a->getMinHoursUntilStart() ?? -PHP_FLOAT_MAX;
            $bMin = $b->getMinHoursUntilStart() ?? -PHP_FLOAT_MAX;
            return $aMin <=> $bMin;
        });
        return $sorted;
    }

    /**
     * Get rules sorted by maximum hours (descending).
     * Rules further from check-in come first.
     *
     * @return CancellationRule[]
     */
    public function getRulesSortedByDistance(): array
    {
        $sorted = $this->rules;
        usort($sorted, function (CancellationRule $a, CancellationRule $b) {
            $aMax = $a->getMaxHoursUntilStart() ?? PHP_FLOAT_MAX;
            $bMax = $b->getMaxHoursUntilStart() ?? PHP_FLOAT_MAX;
            return $bMax <=> $aMax;
        });
        return $sorted;
    }

    /**
     * Find the most lenient rule (furthest from check-in with no/minimal penalty).
     *
     * @return CancellationRule|null
     */
    public function getMostLenientRule(): ?CancellationRule
    {
        $lenientRule = null;
        $maxDistance = -1;

        foreach ($this->rules as $rule) {
            if ($rule->isFreeRule()) {
                // For free rules, consider the max hours (how far out the free period extends)
                $distance = $rule->getMaxHoursUntilStart() ?? PHP_FLOAT_MAX;
                if ($distance > $maxDistance) {
                    $maxDistance = $distance;
                    $lenientRule = $rule;
                }
            }
        }

        return $lenientRule;
    }

    /**
     * Find the strictest rule (closest to check-in or highest penalty).
     *
     * @return CancellationRule|null
     */
    public function getStrictestRule(): ?CancellationRule
    {
        $strictestRule = null;
        $maxPenalty = -1;

        foreach ($this->rules as $rule) {
            $penaltyScore = $this->calculateRulePenaltyScore($rule);
            if ($penaltyScore > $maxPenalty) {
                $maxPenalty = $penaltyScore;
                $strictestRule = $rule;
            }
        }

        return $strictestRule;
    }

    /**
     * Check if policy has any free cancellation period.
     *
     * @return bool
     */
    public function hasFreeCancellationPeriod(): bool
    {
        foreach ($this->rules as $rule) {
            if ($rule->isFreeRule() || !$rule->hasPenalty()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Check if policy is non-refundable (no free cancellation).
     *
     * @return bool
     */
    public function isNonRefundable(): bool
    {
        // If all rules have penalties, it's non-refundable
        foreach ($this->rules as $rule) {
            if ($rule->isFreeRule() || !$rule->hasPenalty()) {
                return false;
            }
        }
        return true;
    }

    /**
     * Get policy type based on rules analysis.
     *
     * @return string 'free', 'flexible', 'moderate', 'strict', 'non_refundable'
     */
    public function getPolicyType(): string
    {
        if (!$this->hasFreeCancellationPeriod()) {
            return 'non_refundable';
        }

        $lenientRule = $this->getMostLenientRule();
        if (!$lenientRule) {
            return 'strict';
        }

        // Check how far out free cancellation extends
        $freeUntilHours = $lenientRule->getMaxHoursUntilStart() ?? PHP_FLOAT_MAX;

        if ($freeUntilHours >= 168) { // 7+ days
            return 'free';
        } elseif ($freeUntilHours >= 72) { // 3+ days
            return 'flexible';
        } elseif ($freeUntilHours >= 24) { // 1+ day
            return 'moderate';
        } else {
            return 'strict';
        }
    }

    /**
     * Calculate penalties for reservation cancellation.
     *
     * @param Reservation $reservation
     * @param \DateTimeImmutable|null $cancellationDate Date when cancellation is made (null = now)
     * @return Penalty[]
     * @throws MoneyCalculationException
     */
    public function calculatePenalties(Reservation $reservation, ?\DateTimeImmutable $cancellationDate = null): array
    {
        if ($cancellationDate === null) {
            $cancellationDate = DateTimeUtils::nowAtLocalTimeZone();
        }

        $penalties = [];

        // Sort rules by priority (rules with smaller intervals checked first)
        $sortedRules = $this->rules;
        usort($sortedRules, fn(CancellationRule $a, CancellationRule $b) =>
            $a->getSortingPriority() <=> $b->getSortingPriority()
        );

        $minPossibleCheckInDateTime = $reservation->getMinPossibleCheckInDateTime();

        foreach ($sortedRules as $rule) {
            if ($rule->appliesTo($minPossibleCheckInDateTime, $cancellationDate)) {
                $amount = $rule->calculatePenalty($reservation);
                $penalties[] = new Penalty(
                    $amount,
                    new \DateTimeImmutable(),
                    $this->formatPenaltyDescription($rule, $minPossibleCheckInDateTime),
                    new PenaltyStatus(PenaltyStatus::CALCULATED)
                );
                // Apply only the first matching rule
                break;
            }
        }

        return $penalties;
    }

    /**
     * Get cancellation consequences for display.
     *
     * @param \DateTimeImmutable $checkInDateTime Min possible check-in date time
     * @return string[] Array of translated consequence descriptions
     */
    public function getCancellationConsequences(\DateTimeImmutable $checkInDateTime): array
    {
        $consequences = [];

        foreach ($this->rules as $rule) {
            $consequences[] = $rule->getDescription($checkInDateTime);
        }

        return $consequences;
    }

    /**
     * Get applicable rule for specific cancellation date.
     *
     * @param \DateTimeImmutable $checkInDateTime
     * @param \DateTimeImmutable $cancellationDate
     * @return CancellationRule|null
     */
    public function getApplicableRule(
        \DateTimeImmutable $checkInDateTime,
        \DateTimeImmutable $cancellationDate
    ): ?CancellationRule {
        $sortedRules = $this->getRulesSortedByProximity();

        foreach ($sortedRules as $rule) {
            if ($rule->appliesTo($checkInDateTime, $cancellationDate)) {
                return $rule;
            }
        }

        return null;
    }

    // ========================================================================
    // Private Helper Methods
    // ========================================================================

    /**
     * Calculate penalty score for rule comparison.
     * Higher score means stricter penalty.
     */
    private function calculateRulePenaltyScore(CancellationRule $rule): float
    {
        $score = 0;

        // Add percentage to score
        if ($rule->getPenaltyPercentage() !== null) {
            $score += $rule->getPenaltyPercentage();
        }

        // Add fixed penalty (normalized to percentage assuming $100 base)
        if ($rule->getFixedPenalty() !== null) {
            $score += min(100, $rule->getFixedPenalty() / 100 * 100);
        }

        // Add nights penalty (assuming each night is ~33% of total)
        if ($rule->getPenaltyInFirstNights() !== null) {
            $score += min(100, $rule->getPenaltyInFirstNights() * 33);
        }

        // Factor in timing (earlier penalties are stricter)
        if ($rule->getMinHoursUntilStart() === 0.0) {
            $score *= 1.5; // Immediate penalties are strictest
        }

        return $score;
    }

    /**
     * Format penalty description from rule.
     */
    private function formatPenaltyDescription(
        CancellationRule $rule,
        \DateTimeImmutable $checkInDateTime
    ): string {
        $descriptions = $rule->getDescription($checkInDateTime);

        if (empty($descriptions)) {
            return 'Cancellation penalty';
        }

        return $descriptions;
    }
}
