import random
import string
from decimal import Decimal
from django.utils import timezone
from django.db import transaction as db_transaction
from django.db.models import Sum, Q

from .models import (
    LoyaltyProgram, LoyaltyRule, CustomerWallet,
    CoinTransaction, Booking, Restaurant,
    LoyaltySubscription, WalletTopup,
    KhataAccount, KhataTransaction,
    LoyaltyGroup, LoyaltyGroupMember,
)


# ──────────────────────────────────────────────
# Helpers
# ──────────────────────────────────────────────

def _generate_referral_code(user_id: int, restaurant_id: int) -> str:
    """8-char alphanumeric referral code, guaranteed unique."""
    while True:
        code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
        if not CustomerWallet.objects.filter(referral_code=code).exists():
            return code


def get_or_create_wallet(user, restaurant: Restaurant) -> CustomerWallet:
    """Return existing wallet or create a fresh one with a referral code."""
    wallet, created = CustomerWallet.objects.get_or_create(
        customer=user,
        restaurant=restaurant,
        defaults={'coin_balance': Decimal('0'), 'lifetime_cash_spent': Decimal('0')},
    )
    if created or not wallet.referral_code:
        wallet.referral_code = _generate_referral_code(user.id, restaurant.id)
        wallet.save(update_fields=['referral_code'])
    return wallet


# ──────────────────────────────────────────────
# Wallet Service
# ──────────────────────────────────────────────

class WalletService:

    @staticmethod
    def expire_stale_coins(wallet: CustomerWallet) -> Decimal:
        """
        Mark all expired CoinTransactions and deduct from balance.
        Called before any redemption check (Iron Law 3).
        Returns total coins expired in this call.
        """
        now = timezone.now()
        stale = CoinTransaction.objects.filter(
            wallet=wallet,
            is_expired=False,
            expires_at__isnull=False,
            expires_at__lte=now,
            coins__gt=0,
        )
        total_expired = Decimal('0')
        for tx in stale:
            tx.is_expired = True
            tx.save(update_fields=['is_expired'])
            total_expired += tx.coins

        if total_expired > 0:
            with db_transaction.atomic():
                w = CustomerWallet.objects.select_for_update().get(pk=wallet.pk)
                w.coin_balance = max(Decimal('0'), w.coin_balance - total_expired)
                w.save(update_fields=['coin_balance'])
                CoinTransaction.objects.create(
                    wallet=wallet,
                    transaction_type='EXPIRED',
                    coins=-total_expired,
                    notes=f"Auto-expired {total_expired} coins",
                )
        return total_expired

    @staticmethod
    def get_live_balance(wallet: CustomerWallet) -> Decimal:
        """Balance after expiring stale coins."""
        WalletService.expire_stale_coins(wallet)
        wallet.refresh_from_db()
        return wallet.coin_balance

    @staticmethod
    def credit_coins(
        wallet: CustomerWallet,
        coins: Decimal,
        transaction_type: str,
        rule: LoyaltyRule = None,
        booking: Booking = None,
        expiry_hours: int = None,
        notes: str = '',
    ) -> CoinTransaction:
        """Credit coins to wallet. Thread-safe via select_for_update."""
        expires_at = None
        if expiry_hours:
            expires_at = timezone.now() + timezone.timedelta(hours=expiry_hours)

        with db_transaction.atomic():
            w = CustomerWallet.objects.select_for_update().get(pk=wallet.pk)
            w.coin_balance += coins
            w.save(update_fields=['coin_balance'])

            tx = CoinTransaction.objects.create(
                wallet=wallet,
                transaction_type=transaction_type,
                coins=coins,
                source_rule=rule,
                source_booking=booking,
                expires_at=expires_at,
                notes=notes,
            )
        return tx


# ──────────────────────────────────────────────
# Iron Laws Enforcer
# ──────────────────────────────────────────────

class IronLawsError(Exception):
    pass


class IronLawsEnforcer:

    @staticmethod
    def validate_redemption(
        wallet: CustomerWallet,
        bill_amount: Decimal,
        coins_requested: Decimal,
    ) -> dict:
        """
        Enforce all 4 Iron Laws before any redemption.
        Returns {'approved': True, 'coins_approved': X, 'discount_amount': Y}
        or raises IronLawsError with a user-facing message.
        """
        program = wallet.restaurant.loyalty_program
        coin_value = Decimal(str(program.coin_value_in_rupees))

        # Law 3: expire stale coins first
        WalletService.expire_stale_coins(wallet)
        wallet.refresh_from_db()

        # Check balance
        if wallet.coin_balance < coins_requested:
            raise IronLawsError(
                f"Insufficient coins. You have {wallet.coin_balance} coins."
            )

        # Law 1: coins can cover at most redemption_cap_percent of bill
        max_discount = bill_amount * (Decimal(str(program.redemption_cap_percent)) / Decimal('100'))
        max_coins_by_cap = max_discount / coin_value

        if coins_requested > max_coins_by_cap:
            raise IronLawsError(
                f"Coins can cover at most {program.redemption_cap_percent}% of your bill. "
                f"Maximum coins you can redeem: {int(max_coins_by_cap)}."
            )

        coins_approved = coins_requested
        discount_amount = coins_approved * coin_value

        return {
            'approved': True,
            'coins_approved': coins_approved,
            'discount_amount': discount_amount,
        }

    @staticmethod
    def apply_redemption(
        wallet: CustomerWallet,
        booking: Booking,
        coins_to_redeem: Decimal,
    ) -> CoinTransaction:
        """
        Debit coins from wallet and update booking total.
        Law 4: lifetime_cash_spent only increments by actual cash paid.
        """
        program = wallet.restaurant.loyalty_program
        discount = coins_to_redeem * program.coin_value_in_rupees

        with db_transaction.atomic():
            w = CustomerWallet.objects.select_for_update().get(pk=wallet.pk)
            w.coin_balance -= coins_to_redeem
            w.save(update_fields=['coin_balance'])

            tx = CoinTransaction.objects.create(
                wallet=wallet,
                transaction_type='REDEEMED',
                coins=-coins_to_redeem,
                source_booking=booking,
                notes=f"Redeemed for booking #{booking.id}, discount ₹{discount:.2f}",
            )

        # Law 4: update lifetime_cash_spent with actual cash paid
        actual_cash = booking.total_amount - discount
        CustomerWallet.objects.filter(pk=wallet.pk).update(
            lifetime_cash_spent=wallet.lifetime_cash_spent + max(Decimal('0'), actual_cash)
        )
        return tx


class RulesEngine:

    @staticmethod
    def _is_welcome_hook_applicable(rule: LoyaltyRule, wallet: CustomerWallet, booking: Booking) -> bool:
        config = rule.trigger_config
        min_spend = Decimal(str(config.get('min_spend', 0)))
        first_order_only = config.get('first_order_only', True)

        if first_order_only and wallet.total_orders > 0:
            return False
        if booking.total_amount < min_spend:
            return False
        already_awarded = CoinTransaction.objects.filter(
            wallet=wallet,
            source_rule=rule,
            transaction_type='EARNED',
        ).exists()
        if already_awarded:
            return False
        return True

    @staticmethod
    def _is_milestone_applicable(rule: LoyaltyRule, wallet: CustomerWallet, booking: Booking) -> bool:
        config = rule.trigger_config
        threshold = Decimal(str(config.get('order_amount', 1000)))
        if booking.total_amount < threshold:
            return False
        return True

    @staticmethod
    def evaluate_booking(booking: Booking):
        """
        Called when a booking status changes to COMPLETED.
        Fires all active rules for the restaurant.
        """
        restaurant = booking.restaurant

        # Skip if no loyalty program or not active
        try:
            program = restaurant.loyalty_program
        except LoyaltyProgram.DoesNotExist:
            return
        if not program.is_active:
            return

        wallet = get_or_create_wallet(booking.customer, restaurant)
        active_rules = program.rules.filter(is_active=True)

        applicable_coin_rules = []
        for rule in active_rules:
            if rule.rule_type == 'WELCOME_HOOK':
                if RulesEngine._is_welcome_hook_applicable(rule, wallet, booking):
                    applicable_coin_rules.append(rule)
            elif rule.rule_type == 'MILESTONE_SPEND':
                if RulesEngine._is_milestone_applicable(rule, wallet, booking):
                    applicable_coin_rules.append(rule)
            elif rule.rule_type == 'REFERRAL':
                RulesEngine._apply_referral(rule, wallet, booking)
            elif rule.rule_type == 'SUMMIT':
                RulesEngine._check_summit(rule, wallet)

        if applicable_coin_rules:
            # Only fire the higher-value rule, not both (anti-stacking - Iron Law 2)
            applicable_coin_rules.sort(key=lambda r: r.reward_coins, reverse=True)
            best_rule = applicable_coin_rules[0]
            if best_rule.rule_type == 'WELCOME_HOOK':
                RulesEngine._apply_welcome_hook(best_rule, wallet, booking)
            elif best_rule.rule_type == 'MILESTONE_SPEND':
                RulesEngine._apply_milestone(best_rule, wallet, booking)

        # Society Vault contribution (Phase 3)
        VaultService.contribute_to_vault(booking)

        # Update order count and lifetime_cash_spent (cash only — Law 4)
        coin_discount = sum(
            abs(tx.coins) * program.coin_value_in_rupees
            for tx in booking.coin_transactions.filter(transaction_type='REDEEMED')
        )
        actual_cash = max(Decimal('0'), booking.total_amount - coin_discount)
        CustomerWallet.objects.filter(pk=wallet.pk).update(
            total_orders=wallet.total_orders + 1,
            lifetime_cash_spent=wallet.lifetime_cash_spent + actual_cash,
        )

        # Mark booking as evaluated to avoid double-awarding
        from .models import Booking
        Booking.objects.filter(pk=booking.pk).update(is_loyalty_evaluated=True)

    @staticmethod
    def _apply_welcome_hook(rule: LoyaltyRule, wallet: CustomerWallet, booking: Booking):
        """Award welcome coins on first order if spend threshold is met."""
        config = rule.trigger_config
        min_spend = Decimal(str(config.get('min_spend', 0)))
        first_order_only = config.get('first_order_only', True)

        if first_order_only and wallet.total_orders > 0:
            return
        if booking.total_amount < min_spend:
            return
        # Don't double-award: check if welcome coins already given
        already_awarded = CoinTransaction.objects.filter(
            wallet=wallet,
            source_rule=rule,
            transaction_type='EARNED',
        ).exists()
        if already_awarded:
            return

        WalletService.credit_coins(
            wallet=wallet,
            coins=rule.reward_coins,
            transaction_type='EARNED',
            rule=rule,
            booking=booking,
            expiry_hours=rule.reward_expiry_hours,
            notes=f"Welcome Hook — first order ≥ ₹{min_spend}",
        )

    @staticmethod
    def _apply_referral(rule: LoyaltyRule, wallet: CustomerWallet, booking: Booking):
        """Award referral coins to the person who referred this customer."""
        if not wallet.referred_by:
            return
        # Only on the new customer's first order
        if wallet.total_orders > 0:
            return
        # Find referrer's wallet for this restaurant
        try:
            referrer_wallet = CustomerWallet.objects.get(
                customer=wallet.referred_by,
                restaurant=wallet.restaurant,
            )
        except CustomerWallet.DoesNotExist:
            return

        # Don't double-award
        already_awarded = CoinTransaction.objects.filter(
            wallet=referrer_wallet,
            source_rule=rule,
            transaction_type='REFERRAL',
            notes__contains=str(wallet.customer.id),
        ).exists()
        if already_awarded:
            return

        WalletService.credit_coins(
            wallet=referrer_wallet,
            coins=rule.reward_coins,
            transaction_type='REFERRAL',
            rule=rule,
            booking=booking,
            expiry_hours=rule.reward_expiry_hours,
            notes=f"Referral reward — referred user id:{wallet.customer.id}",
        )
        WalletService.credit_coins(
            wallet=wallet,
            coins=rule.reward_coins,
            transaction_type='REFERRAL',
            rule=rule,
            booking=booking,
            expiry_hours=rule.reward_expiry_hours,
            notes=f"Referral reward — referred by user id:{referrer_wallet.customer.id}",
        )

    @staticmethod
    def _apply_milestone(rule: LoyaltyRule, wallet: CustomerWallet, booking: 'Booking'):
        """Award coins when order amount hits a milestone threshold."""
        config = rule.trigger_config
        threshold = Decimal(str(config.get('order_amount', 1000)))
        if booking.total_amount < threshold:
            return
        WalletService.credit_coins(
            wallet=wallet,
            coins=rule.reward_coins,
            transaction_type='EARNED',
            rule=rule,
            booking=booking,
            expiry_hours=rule.reward_expiry_hours,
            notes=f"Milestone: order ≥ ₹{threshold}",
        )

    @staticmethod
    def _check_summit(rule: LoyaltyRule, wallet: CustomerWallet):
        """Flag customer as Summit VIP when lifetime cash spend hits target."""
        if wallet.is_summit_vip:
            return
        target = Decimal(str(rule.trigger_config.get('target_spend', 7000)))
        max_vips = rule.trigger_config.get('max_vips', 100)

        if wallet.lifetime_cash_spent < target:
            return

        current_vips = CustomerWallet.objects.filter(
            restaurant=wallet.restaurant,
            is_summit_vip=True,
        ).count()
        if current_vips >= max_vips:
            return

        with db_transaction.atomic():
            wallet_locked = CustomerWallet.objects.select_for_update().get(pk=wallet.pk)
            wallet_locked.is_summit_vip = True
            wallet_locked.summit_vip_at = timezone.now()
            wallet_locked.save(update_fields=['is_summit_vip', 'summit_vip_at'])

            if rule.reward_coins > 0:
                WalletService.credit_coins(
                    wallet=wallet_locked,
                    coins=rule.reward_coins,
                    transaction_type='EARNED',
                    rule=rule,
                    notes="Ultimate Summit VIP bonus coins",
                )

            try:
                from .whatsapp_service import send_summit_vip_notification
                send_summit_vip_notification(wallet_locked)
            except Exception:
                pass


# ──────────────────────────────────────────────
# Phase 2 — Subscription Service
# ──────────────────────────────────────────────

class SubscriptionService:

    @staticmethod
    def get_active_subscription(wallet: CustomerWallet):
        """Return the wallet's currently active subscription or None."""
        today = timezone.now().date()
        return LoyaltySubscription.objects.filter(
            wallet=wallet,
            status='ACTIVE',
            valid_from__lte=today,
            valid_until__gte=today,
        ).first()

    @staticmethod
    def create_subscription(wallet: CustomerWallet, rule: LoyaltyRule, payment_ref: str = '') -> LoyaltySubscription:
        """Create a 30-day subscription for the wallet."""
        today = timezone.now().date()
        amount = Decimal(str(rule.trigger_config.get('monthly_price', 399)))
        sub = LoyaltySubscription.objects.create(
            wallet=wallet,
            rule=rule,
            amount_paid=amount,
            payment_reference=payment_ref,
            valid_from=today,
            valid_until=today + timezone.timedelta(days=30),
            status='ACTIVE',
        )
        return sub

    @staticmethod
    def check_entitlement(wallet: CustomerWallet, booking: 'Booking') -> bool:
        """
        Returns True if a free beverage is unlocked today for this booking.
        Rule config: {"monthly_price": 399, "min_food_pair": 99, "daily_item_category": "BEVERAGE"}
        """
        sub = SubscriptionService.get_active_subscription(wallet)
        if not sub:
            return False

        today = timezone.now().date()
        if sub.last_used_date == today:
            return False  # Already used today

        rule_config = sub.rule.trigger_config if sub.rule else {}
        min_food = Decimal(str(rule_config.get('min_food_pair', 99)))
        bev_category = rule_config.get('daily_item_category', 'BEVERAGE')

        # Check booking has a food item ≥ min_food and a beverage item
        food_total = Decimal('0')
        has_beverage = False
        for item in booking.items.all():
            if item.menu_item.category == bev_category:
                has_beverage = True
            else:
                food_total += item.total_price

        if not has_beverage or food_total < min_food:
            return False

        # Mark as used
        sub.last_used_date = today
        sub.total_uses += 1
        sub.save(update_fields=['last_used_date', 'total_uses'])
        return True


# ──────────────────────────────────────────────
# Phase 2 — Wallet Top-up Service
# ──────────────────────────────────────────────

class WalletTopupService:

    @staticmethod
    def process_topup(
        wallet: CustomerWallet,
        rule: LoyaltyRule,
        cash_paid: Decimal,
        payment_ref: str = '',
    ) -> WalletTopup:
        """
        Credit coins = cash_paid + bonus_percent of cash_paid.
        Rule config: {"min_topup": 1000, "bonus_percent": 50}
        """
        config = rule.trigger_config
        min_topup = Decimal(str(config.get('min_topup', 1000)))
        bonus_pct = Decimal(str(config.get('bonus_percent', 50)))

        if cash_paid < min_topup:
            raise ValueError(f"Minimum top-up is ₹{min_topup}.")

        # coins: 1 coin = 1 rupee (face value) + bonus
        base_coins = cash_paid
        bonus_coins = (cash_paid * bonus_pct / Decimal('100')).quantize(Decimal('0.01'))
        total_coins = base_coins + bonus_coins

        topup = WalletTopup.objects.create(
            wallet=wallet,
            rule=rule,
            cash_paid=cash_paid,
            coins_credited=total_coins,
            payment_reference=payment_ref,
        )

        WalletService.credit_coins(
            wallet=wallet,
            coins=total_coins,
            transaction_type='TOPUP_BONUS',
            rule=rule,
            notes=f"Top-up ₹{cash_paid} → {total_coins} coins ({bonus_pct}% bonus)",
        )
        return topup


# ──────────────────────────────────────────────
# Phase 3 — Khata Service
# ──────────────────────────────────────────────

class KhataService:

    @staticmethod
    def get_or_create_khata(wallet: CustomerWallet) -> KhataAccount:
        khata, _ = KhataAccount.objects.get_or_create(wallet=wallet)
        return khata

    @staticmethod
    def use_lifeline(wallet: CustomerWallet, booking: 'Booking') -> KhataTransaction:
        khata = KhataService.get_or_create_khata(wallet)

        if khata.status == 'BLOCKED':
            raise ValueError("Your Khata account is blocked. Please clear your tab first.")
        if khata.lifelines_remaining <= 0:
            raise ValueError("No lifelines remaining this month.")

        today = timezone.now().date()
        import calendar
        _, last_day = calendar.monthrange(today.year, today.month)
        if today.month == 12:
            due_date = today.replace(year=today.year + 1, month=1, day=1)
        else:
            due_date = today.replace(month=today.month + 1, day=1)

        with db_transaction.atomic():
            khata_locked = KhataAccount.objects.select_for_update().get(pk=khata.pk)
            khata_locked.lifelines_used += 1
            khata_locked.current_tab_amount += booking.total_amount
            khata_locked.tab_due_date = due_date
            khata_locked.save(update_fields=['lifelines_used', 'current_tab_amount', 'tab_due_date'])

            tx = KhataTransaction.objects.create(
                khata=khata,
                booking=booking,
                amount=booking.total_amount,
            )
            booking.payment_status = 'UNPAID'
            booking.save(update_fields=['payment_status'])

        return tx

    @staticmethod
    def clear_tab(wallet: CustomerWallet, payment_ref: str = '') -> bool:
        khata = KhataService.get_or_create_khata(wallet)
        now = timezone.now()
        with db_transaction.atomic():
            KhataTransaction.objects.filter(khata=khata, is_cleared=False).update(
                is_cleared=True, cleared_at=now
            )
            khata.current_tab_amount = Decimal('0')
            khata.status = 'ACTIVE'
            khata.save(update_fields=['current_tab_amount', 'status'])
        return True

    @staticmethod
    def check_overdue(wallet: CustomerWallet):
        khata = KhataService.get_or_create_khata(wallet)
        if khata.current_tab_amount <= 0 or khata.status == 'ACTIVE' and not khata.tab_due_date:
            return
        today = timezone.now().date()
        if khata.tab_due_date and today > khata.tab_due_date:
            delta_days = (today - khata.tab_due_date).days
            new_status = 'BLOCKED' if delta_days > 15 else 'OVERDUE'
            if khata.status != new_status:
                khata.status = new_status
                khata.save(update_fields=['status'])


# ──────────────────────────────────────────────
# Phase 3 — Society Vault Service
# ──────────────────────────────────────────────

class VaultService:

    @staticmethod
    def contribute_to_vault(booking: 'Booking'):
        """Called after each completed booking to add member's share to group vault."""
        try:
            wallet = CustomerWallet.objects.get(
                customer=booking.customer,
                restaurant=booking.restaurant,
            )
        except CustomerWallet.DoesNotExist:
            return

        memberships = LoyaltyGroupMember.objects.filter(
            wallet=wallet,
            group__is_active=True,
            group__restaurant=booking.restaurant,
        ).select_related('group')

        for membership in memberships:
            group = membership.group
            contribution = (booking.total_amount * group.vault_percentage / Decimal('100')).quantize(
                Decimal('0.01')
            )
            LoyaltyGroup.objects.filter(pk=group.pk).update(
                vault_balance=group.vault_balance + contribution
            )

    @staticmethod
    def redeem_vault(group: LoyaltyGroup, amount: Decimal, notes: str = '') -> bool:
        """Owner redeems vault funds for group catering."""
        if group.vault_balance < amount:
            raise ValueError(f"Insufficient vault balance. Available: ₹{group.vault_balance}")
        LoyaltyGroup.objects.filter(pk=group.pk).update(
            vault_balance=group.vault_balance - amount
        )
        return True
