<?php

namespace App\Domain\Pricing\Services;

use App\Domain\Participant\Models\Participant;
use App\Domain\Pricing\Enums\AdjustmentType;
use App\Domain\Pricing\Models\BasePrice;
use App\Domain\Pricing\Models\PricingRule;
use App\Domain\Pricing\Models\Promotion;
use App\Domain\Shared\Exceptions\DomainException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;

class PricingService
{
    /**
     * Maximum discount percentage applied globally (academy setting, default 50%).
     */
    private const DEFAULT_MAX_DISCOUNT_PERCENT = 50;

    /**
     * The 10-step price resolution engine.
     *
     * @param Model            $priceable   The item being priced (TrainingProgram, Product, etc.)
     * @param Participant|null $participant The buyer (null for anonymous walk-in product sales)
     * @param int|null         $branchId    Override branch (falls back to participant's branch)
     * @param string|null      $couponCode  Coupon code to validate and apply
     * @param string|null      $date        Pricing date (defaults to today)
     *
     * @throws DomainException When no base price is found
     */
    public function calculate(
        Model $priceable,
        ?Participant $participant = null,
        ?int $branchId = null,
        ?string $couponCode = null,
        ?string $date = null
    ): PriceResult {
        $date ??= now()->toDateString();
        $branchId ??= $this->resolveParticipantBranch($participant);

        // ─── Step 1: Find base price (fail if none exists) ────────────
        $basePrice = $this->findBasePrice($priceable, $branchId, $date, $participant);
        if (!$basePrice) {
            throw new DomainException('لا يوجد سعر محدد');
        }

        $originalAmount = $basePrice->amount;
        $currentAmount = $originalAmount;
        $appliedRules = [];
        $totalDiscount = 0;

        // ─── Step 2: Gather applicable rules ──────────────────────────
        $rules = $this->gatherApplicableRules($priceable, $branchId, $date);

        // ─── Step 3: Evaluate conditions against participant context ──
        if ($participant) {
            $context = $this->buildParticipantContext($participant);
            $rules = $rules->filter(fn (PricingRule $rule) => $this->evaluateConditions($rule, $context));
        } else {
            // No participant: only rules without conditions apply
            $rules = $rules->filter(fn (PricingRule $rule) => empty($rule->conditions));
        }

        // ─── Step 4: Filter non-stackable conflicts ──────────────────
        $rules = $this->filterNonStackableConflicts($rules);

        // ─── Step 5: Apply rules in priority order (ASC) ─────────────
        $rules = $rules->sortBy('priority')->values();

        foreach ($rules as $rule) {
            $adjustedAmount = $this->applyAdjustment($currentAmount, $originalAmount, $rule);

            // ─── Step 6: Apply rule-level caps ────────────────────────
            if ($rule->max_discount_percent !== null) {
                $maxAllowedDiscount = (int) floor($originalAmount * $rule->max_discount_percent / 100);
                $ruleDiscount = $currentAmount - $adjustedAmount;
                if ($ruleDiscount > $maxAllowedDiscount) {
                    $adjustedAmount = $currentAmount - $maxAllowedDiscount;
                }
            }

            // Only record if the rule actually changed the amount
            if ($adjustedAmount !== $currentAmount) {
                $ruleDiscount = $currentAmount - $adjustedAmount;
                $appliedRules[] = [
                    'rule_id' => $rule->id,
                    'rule_name' => $rule->name_ar,
                    'rule_type' => $rule->rule_type->value,
                    'adjustment_type' => $rule->adjustment_type->value,
                    'before' => $currentAmount,
                    'after' => $adjustedAmount,
                    'discount' => $ruleDiscount,
                ];
                $totalDiscount += $ruleDiscount;
                $currentAmount = $adjustedAmount;
            }
        }

        // ─── Step 7: Apply promotion/coupon (if any) ─────────────────
        $appliedPromotion = null;
        if ($couponCode) {
            $promotion = $this->validateCoupon($couponCode, $participant, $currentAmount, $priceable);
            if ($promotion) {
                $promoAdjusted = $this->applyPromotionAdjustment($currentAmount, $originalAmount, $promotion);

                // Apply max_discount_amount cap on promotion
                if ($promotion->max_discount_amount !== null) {
                    $promoDiscount = $currentAmount - $promoAdjusted;
                    if ($promoDiscount > $promotion->max_discount_amount) {
                        $promoAdjusted = $currentAmount - $promotion->max_discount_amount;
                    }
                }

                $promoDiscount = $currentAmount - $promoAdjusted;
                $appliedPromotion = [
                    'promotion_id' => $promotion->id,
                    'code' => $promotion->code,
                    'name' => $promotion->name_ar,
                    'before' => $currentAmount,
                    'after' => $promoAdjusted,
                    'discount' => $promoDiscount,
                ];
                $totalDiscount += $promoDiscount;
                $currentAmount = $promoAdjusted;
            }
        }

        // ─── Step 8: Apply global maximum discount ───────────────────
        $maxDiscountPercent = $this->getMaxDiscountPercent();
        $maxAllowedTotalDiscount = (int) floor($originalAmount * $maxDiscountPercent / 100);
        if ($totalDiscount > $maxAllowedTotalDiscount) {
            $currentAmount = $originalAmount - $maxAllowedTotalDiscount;
            $totalDiscount = $maxAllowedTotalDiscount;
        }

        // ─── Step 9: Floor check — never negative ────────────────────
        if ($currentAmount < 0) {
            $currentAmount = 0;
            $totalDiscount = $originalAmount;
        }

        // ─── Step 10: Return PriceResult ─────────────────────────────
        return new PriceResult(
            baseAmount: $originalAmount,
            finalAmount: $currentAmount,
            totalDiscount: $totalDiscount,
            appliedRules: $appliedRules,
            appliedPromotion: $appliedPromotion,
            basePriceId: $basePrice->id,
        );
    }

    // ─── Step 1: Base Price Resolution ────────────────────────────────────

    private function findBasePrice(Model $priceable, ?int $branchId, string $date, ?Participant $participant = null): ?BasePrice
    {
        $query = BasePrice::where('priceable_type', $priceable->getMorphClass())
            ->where('priceable_id', $priceable->getKey())
            ->where('is_active', true)
            ->where('effective_from', '<=', $date)
            ->where(function ($q) use ($date) {
                $q->whereNull('effective_to')
                    ->orWhere('effective_to', '>=', $date);
            });

        // Resolve membership type from participant
        $membershipType = $participant?->membership_type?->value ?? 'non_member';

        // Try membership-type-specific price first, then fallback to any
        $withMembership = fn ($q) => (clone $q)->where('metadata->membership_type', $membershipType);

        // Branch-specific + membership type first
        if ($branchId) {
            $match = $withMembership((clone $query)->where('branch_id', $branchId))
                ->orderByDesc('priority')
                ->first();
            if ($match) {
                return $match;
            }

            // Branch-specific without membership filter
            $branchAny = (clone $query)
                ->where('branch_id', $branchId)
                ->orderByDesc('priority')
                ->first();
            if ($branchAny) {
                return $branchAny;
            }
        }

        // No branch: membership type first
        $match = $withMembership((clone $query)->whereNull('branch_id'))
            ->orderByDesc('priority')
            ->first();
        if ($match) {
            return $match;
        }

        // Final fallback: any active base price
        return $query->whereNull('branch_id')
            ->orderByDesc('priority')
            ->first();
    }

    // ─── Step 2: Gather Applicable Rules ──────────────────────────────────

    private function gatherApplicableRules(Model $priceable, ?int $branchId, string $date): Collection
    {
        return PricingRule::where('is_active', true)
            ->where(function ($q) use ($date) {
                $q->whereNull('effective_from')
                    ->orWhere('effective_from', '<=', $date);
            })
            ->where(function ($q) use ($date) {
                $q->whereNull('effective_to')
                    ->orWhere('effective_to', '>=', $date);
            })
            ->where(function ($q) use ($branchId) {
                $q->whereNull('branch_id');
                if ($branchId) {
                    $q->orWhere('branch_id', $branchId);
                }
            })
            ->where(function ($q) use ($priceable) {
                // Global rules (no target), type-wide rules, or specific target
                $q->where(function ($sq) {
                    $sq->whereNull('target_type')->whereNull('target_id');
                })
                ->orWhere(function ($sq) use ($priceable) {
                    $sq->where('target_type', $priceable->getMorphClass())
                        ->where('target_id', $priceable->getKey());
                })
                ->orWhere(function ($sq) use ($priceable) {
                    $sq->where('target_type', $priceable->getMorphClass())
                        ->whereNull('target_id');
                });
            })
            ->where(function ($q) {
                // Respect usage limits
                $q->whereNull('usage_limit')
                    ->orWhereColumn('usage_count', '<', 'usage_limit');
            })
            ->get();
    }

    // ─── Step 3: Build Participant Context ────────────────────────────────

    private function buildParticipantContext(Participant $participant): array
    {
        $person = $participant->person;
        $age = $person?->date_of_birth
            ? (int) now()->diffInYears($person->date_of_birth)
            : null;

        // Family size: count participants sharing a guardian
        $familySize = 1;
        $siblingOrder = 1;
        if ($participant->relationLoaded('guardians') ? $participant->guardians->isNotEmpty() : $participant->guardians()->exists()) {
            $guardianIds = $participant->guardians->pluck('id');
            if ($guardianIds->isNotEmpty()) {
                $siblings = Participant::whereHas('guardians', function ($q) use ($guardianIds) {
                    $q->whereIn('guardians.id', $guardianIds);
                })->orderBy('created_at')->pluck('id');

                $familySize = max(1, $siblings->count());
                $siblingOrder = $siblings->search($participant->id) + 1;
                if ($siblingOrder < 1) {
                    $siblingOrder = 1;
                }
            }
        }

        // Membership duration in months
        $membershipMonths = $participant->created_at
            ? (int) now()->diffInMonths($participant->created_at)
            : 0;

        // Active enrollment count
        $enrollmentCount = $participant->activeEnrollments()->count();

        return [
            'age' => $age,
            'gender' => $person?->gender,
            'classification' => $participant->classification ?? 'regular',
            'membership_duration_months' => $membershipMonths,
            'family_size' => $familySize,
            'sibling_order' => $siblingOrder,
            'enrollment_count' => $enrollmentCount,
            'branch_id' => $this->resolveParticipantBranch($participant),
        ];
    }

    // ─── Step 3: Condition Evaluation ─────────────────────────────────────

    private function evaluateConditions(PricingRule $rule, array $context): bool
    {
        $conditions = $rule->conditions;
        if (empty($conditions)) {
            return true;
        }

        return match ($rule->rule_type) {
            \App\Domain\Pricing\Enums\PricingRuleType::Age => $this->evaluateRange($context['age'], $conditions),
            \App\Domain\Pricing\Enums\PricingRuleType::MembershipDuration => $this->evaluateRange($context['membership_duration_months'], $conditions),
            \App\Domain\Pricing\Enums\PricingRuleType::FamilySize => $this->evaluateRange($context['family_size'], $conditions),
            \App\Domain\Pricing\Enums\PricingRuleType::SiblingOrder => $this->evaluateRange($context['sibling_order'], $conditions),
            \App\Domain\Pricing\Enums\PricingRuleType::Classification => $this->evaluateInList($context['classification'], $conditions),
            \App\Domain\Pricing\Enums\PricingRuleType::EnrollmentVolume => $this->evaluateRange($context['enrollment_count'], $conditions),
            \App\Domain\Pricing\Enums\PricingRuleType::Gender => $this->evaluateInList($context['gender'], $conditions),
            \App\Domain\Pricing\Enums\PricingRuleType::Branch => $this->evaluateInList($context['branch_id'], $conditions),
            \App\Domain\Pricing\Enums\PricingRuleType::EnrollmentTiming => $this->evaluateTimingCondition($conditions),
            \App\Domain\Pricing\Enums\PricingRuleType::Seasonal => $this->evaluateSeasonalCondition($conditions),
            \App\Domain\Pricing\Enums\PricingRuleType::DayTime => $this->evaluateDayTimeCondition($conditions),
            \App\Domain\Pricing\Enums\PricingRuleType::Loyalty => $this->evaluateRange($context['membership_duration_months'], $conditions),
            \App\Domain\Pricing\Enums\PricingRuleType::Custom => true, // Custom rules always pass (admin assigns manually)
        };
    }

    private function evaluateRange(?int $value, array $conditions): bool
    {
        if ($value === null) {
            return false;
        }

        $min = $conditions['min'] ?? null;
        $max = $conditions['max'] ?? null;

        if ($min !== null && $value < (int) $min) {
            return false;
        }
        if ($max !== null && $value > (int) $max) {
            return false;
        }

        return true;
    }

    private function evaluateInList(mixed $value, array $conditions): bool
    {
        $allowed = $conditions['values'] ?? $conditions['in'] ?? [];
        if (empty($allowed)) {
            return true;
        }

        return in_array($value, $allowed);
    }

    private function evaluateTimingCondition(array $conditions): bool
    {
        $now = now();

        if (isset($conditions['before_date']) && $now->gt($conditions['before_date'])) {
            return false;
        }
        if (isset($conditions['after_date']) && $now->lt($conditions['after_date'])) {
            return false;
        }

        // days_before_start requires enrollment context — pass if condition is set
        // (caller enriches context when doing enrollment-specific pricing)
        return true;
    }

    private function evaluateSeasonalCondition(array $conditions): bool
    {
        $months = $conditions['months'] ?? [];
        if (!empty($months) && !in_array(now()->month, $months)) {
            return false;
        }

        return true;
    }

    private function evaluateDayTimeCondition(array $conditions): bool
    {
        $now = now();

        $days = $conditions['days'] ?? [];
        if (!empty($days) && !in_array(strtolower($now->format('l')), $days)) {
            return false;
        }

        $afterHour = $conditions['after_hour'] ?? null;
        $beforeHour = $conditions['before_hour'] ?? null;
        if ($afterHour !== null && $now->hour < (int) $afterHour) {
            return false;
        }
        if ($beforeHour !== null && $now->hour >= (int) $beforeHour) {
            return false;
        }

        return true;
    }

    // ─── Step 4: Non-Stackable Conflict Resolution ────────────────────────

    private function filterNonStackableConflicts(Collection $rules): Collection
    {
        $stackable = $rules->filter(fn (PricingRule $r) => $r->is_stackable);
        $nonStackable = $rules->filter(fn (PricingRule $r) => !$r->is_stackable);

        if ($nonStackable->count() <= 1) {
            return $stackable->merge($nonStackable)->values();
        }

        // Among conflicting non-stackable rules, highest priority wins (lowest number)
        $winner = $nonStackable->sortBy('priority')->first();

        return $stackable->push($winner)->values();
    }

    // ─── Step 5/6: Apply Adjustment ───────────────────────────────────────

    /**
     * Apply a pricing rule adjustment.
     *
     * Percentage values stored in hundredths (basis points):
     * 10% = 1000, 25% = 2500, 50% = 5000, 12.5% = 1250
     */
    private function applyAdjustment(int $currentAmount, int $originalAmount, PricingRule $rule): int
    {
        return match ($rule->adjustment_type) {
            AdjustmentType::PercentageDiscount => $currentAmount - (int) floor($currentAmount * $rule->adjustment_value / 10000),
            AdjustmentType::FixedDiscount => max(0, $currentAmount - $rule->adjustment_value),
            AdjustmentType::FixedPrice => $rule->adjustment_value,
            AdjustmentType::PercentageIncrease => $currentAmount + (int) floor($currentAmount * $rule->adjustment_value / 10000),
        };
    }

    // ─── Step 7: Promotion/Coupon Validation ──────────────────────────────

    /**
     * Validate a coupon code against 7 checks.
     */
    public function validateCoupon(
        string $code,
        ?Participant $participant,
        int $cartTotal,
        Model $priceable
    ): ?Promotion {
        // Check 1: Exists and is active
        $promotion = Promotion::where('code', $code)
            ->where('is_active', true)
            ->first();

        if (!$promotion) {
            return null;
        }

        $now = now()->toDateString();

        // Check 2: Start date
        if ($promotion->start_date && $now < $promotion->start_date->format('Y-m-d')) {
            return null;
        }

        // Check 3: End date
        if ($promotion->end_date && $now > $promotion->end_date->format('Y-m-d')) {
            return null;
        }

        // Check 4: Global usage limit
        if ($promotion->usage_limit !== null && $promotion->usage_count >= $promotion->usage_limit) {
            return null;
        }

        // Check 5: Per-participant usage limit
        if ($participant && $promotion->per_participant_limit !== null) {
            $participantUses = $promotion->usages()
                ->where('participant_id', $participant->id)
                ->count();
            if ($participantUses >= $promotion->per_participant_limit) {
                return null;
            }
        }

        // Check 6: Minimum purchase amount
        if ($promotion->min_purchase_amount !== null && $cartTotal < $promotion->min_purchase_amount) {
            return null;
        }

        // Check 7: Target match (if promotion is scoped to specific priceable)
        if ($promotion->target_type && $promotion->target_id) {
            if (
                $priceable->getMorphClass() !== $promotion->target_type
                || $priceable->getKey() != $promotion->target_id
            ) {
                return null;
            }
        }

        return $promotion;
    }

    private function applyPromotionAdjustment(int $currentAmount, int $originalAmount, Promotion $promotion): int
    {
        $adjustmentType = $promotion->adjustment_type;
        $value = $promotion->adjustment_value;

        return match ($adjustmentType) {
            AdjustmentType::PercentageDiscount => $currentAmount - (int) floor($currentAmount * $value / 10000),
            AdjustmentType::FixedDiscount => max(0, $currentAmount - $value),
            AdjustmentType::FixedPrice => $value,
            AdjustmentType::PercentageIncrease => $currentAmount + (int) floor($currentAmount * $value / 10000),
        };
    }

    // ─── Post-Calculation: Record Usage ───────────────────────────────────

    /**
     * Record promotion/coupon usage after a successful transaction.
     * Call this from POSService/InvoiceService AFTER committing the sale.
     */
    public function incrementUsage(
        Promotion $promotion,
        ?Participant $participant,
        ?int $invoiceId,
        int $discountAmount
    ): void {
        $promotion->increment('usage_count');

        $promotion->usages()->create([
            'academy_id' => $promotion->academy_id,
            'participant_id' => $participant?->id,
            'invoice_id' => $invoiceId,
            'discount_amount' => $discountAmount,
            'used_at' => now(),
        ]);
    }

    // ─── Step 8: Global Max Discount ──────────────────────────────────────

    private function getMaxDiscountPercent(): int
    {
        // TODO: Read from academy system_settings when that module is built
        // For now, use the default constant
        return self::DEFAULT_MAX_DISCOUNT_PERCENT;
    }

    // ─── Helpers ──────────────────────────────────────────────────────────

    private function resolveParticipantBranch(?Participant $participant): ?int
    {
        if (!$participant) {
            return null;
        }

        // Participant may have branch via their primary enrollment group
        // or via person record — check what's available
        return $participant->getAttribute('branch_id')
            ?? $participant->person?->getAttribute('branch_id')
            ?? null;
    }
}
