from sqlalchemy.orm import Session
from datetime import datetime, timedelta, date
from typing import Optional
import secrets
import string
from ..models.user_loyalty import UserLoyalty
from ..models.loyalty_tier import LoyaltyTier, TierLevel
from ..models.loyalty_point_transaction import LoyaltyPointTransaction, TransactionType, TransactionSource
from ..models.loyalty_reward import LoyaltyReward
from ..models.reward_redemption import RewardRedemption, RedemptionStatus
from ..models.referral import Referral, ReferralStatus
from ...bookings.models.booking import Booking, BookingStatus
from ...auth.models.user import User
import logging

logger = logging.getLogger(__name__)

class LoyaltyService:
    # Points earning rates
    POINTS_PER_DOLLAR = 10  # Base: 10 points per dollar spent
    BIRTHDAY_BONUS_POINTS = 500
    ANNIVERSARY_BONUS_POINTS = 1000
    REFERRER_POINTS = 500  # Points for referrer when referral completes booking
    REFERRED_BONUS_POINTS = 250  # Bonus points for new referred user
    
    # Points expiration (in days)
    POINTS_EXPIRATION_DAYS = 365  # Points expire after 1 year
    
    @staticmethod
    def is_loyalty_enabled(db: Session) -> bool:
        """Check if loyalty program is enabled"""
        try:
            from ...system.models.system_settings import SystemSettings
            setting = db.query(SystemSettings).filter(
                SystemSettings.key == 'loyalty_program_enabled'
            ).first()
            
            if not setting:
                return True  # Default to enabled
            
            return setting.value.lower() == 'true'
        except Exception:
            return True  # Default to enabled on error
    
    @staticmethod
    def get_or_create_user_loyalty(db: Session, user_id: int) -> UserLoyalty:
        """Get or create loyalty record for user"""
        user_loyalty = db.query(UserLoyalty).filter(UserLoyalty.user_id == user_id).first()
        
        if not user_loyalty:
            # Get bronze tier (lowest tier)
            bronze_tier = db.query(LoyaltyTier).filter(LoyaltyTier.level == TierLevel.bronze).first()
            
            if not bronze_tier:
                # Create default tiers if they don't exist
                LoyaltyService.create_default_tiers(db)
                bronze_tier = db.query(LoyaltyTier).filter(LoyaltyTier.level == TierLevel.bronze).first()
            
            # Generate referral code
            referral_code = LoyaltyService.generate_referral_code(db, user_id)
            
            user_loyalty = UserLoyalty(
                user_id=user_id,
                tier_id=bronze_tier.id,
                total_points=0,
                lifetime_points=0,
                available_points=0,
                expired_points=0,
                referral_code=referral_code,
                referral_count=0,
                tier_started_date=datetime.utcnow()
            )
            db.add(user_loyalty)
            db.commit()
            db.refresh(user_loyalty)
        
        return user_loyalty
    
    @staticmethod
    def generate_referral_code(db: Session, user_id: int, length: int = 8) -> str:
        """Generate unique referral code for user using cryptographically secure random"""
        max_attempts = 10
        alphabet = string.ascii_uppercase + string.digits
        for _ in range(max_attempts):
            # Generate code: USER1234 format using cryptographically secure random
            # Use secrets.choice() instead of random.choices() for security
            random_part = ''.join(secrets.choice(alphabet) for _ in range(length-8))
            code = f"USER{user_id:04d}{random_part}"
            
            # Check if code exists
            existing = db.query(UserLoyalty).filter(UserLoyalty.referral_code == code).first()
            if not existing:
                return code
        
        # Fallback: timestamp-based with secure random suffix
        random_suffix = ''.join(secrets.choice(alphabet) for _ in range(4))
        return f"REF{int(datetime.utcnow().timestamp())}{user_id}{random_suffix}"
    
    @staticmethod
    def create_default_tiers(db: Session):
        """Create default loyalty tiers if they don't exist"""
        tiers_data = [
            {
                'level': TierLevel.bronze,
                'name': 'Bronze',
                'description': 'Starting tier - Earn points on every booking',
                'min_points': 0,
                'points_earn_rate': 1.0,
                'discount_percentage': 0,
                'benefits': 'Welcome bonus points, Access to basic rewards',
                'color': '#CD7F32'
            },
            {
                'level': TierLevel.silver,
                'name': 'Silver',
                'description': 'Earn points faster with 1.25x multiplier',
                'min_points': 5000,
                'points_earn_rate': 1.25,
                'discount_percentage': 2,
                'benefits': '25% faster point earning, 2% member discount, Priority customer support',
                'color': '#C0C0C0'
            },
            {
                'level': TierLevel.gold,
                'name': 'Gold',
                'description': 'Premium tier with exclusive benefits',
                'min_points': 15000,
                'points_earn_rate': 1.5,
                'discount_percentage': 5,
                'benefits': '50% faster point earning, 5% member discount, Room upgrade priority, Exclusive rewards',
                'color': '#FFD700'
            },
            {
                'level': TierLevel.platinum,
                'name': 'Platinum',
                'description': 'Elite tier with maximum benefits',
                'min_points': 50000,
                'points_earn_rate': 2.0,
                'discount_percentage': 10,
                'benefits': '100% faster point earning, 10% member discount, Guaranteed room upgrades, Concierge service, Birthday & anniversary bonuses',
                'color': '#E5E4E2'
            }
        ]
        
        for tier_data in tiers_data:
            existing = db.query(LoyaltyTier).filter(LoyaltyTier.level == tier_data['level']).first()
            if not existing:
                tier = LoyaltyTier(**tier_data)
                db.add(tier)
        
        db.commit()
    
    @staticmethod
    def earn_points_from_booking(
        db: Session,
        user_id: int,
        booking: Booking,
        amount: float
    ) -> int:
        """Earn points from completed booking"""
        try:
            # Check if loyalty program is enabled
            if not LoyaltyService.is_loyalty_enabled(db):
                logger.info("Loyalty program is disabled. Skipping points earning.")
                return 0
            user_loyalty = LoyaltyService.get_or_create_user_loyalty(db, user_id)
            tier = db.query(LoyaltyTier).filter(LoyaltyTier.id == user_loyalty.tier_id).first()
            
            if not tier:
                logger.error(f"Tier not found for user {user_id}")
                return 0
            
            # Calculate base points (points per dollar * amount)
            base_points = int(amount * LoyaltyService.POINTS_PER_DOLLAR)
            
            # Apply tier multiplier
            tier_multiplier = float(tier.points_earn_rate) if tier.points_earn_rate else 1.0
            points_earned = int(base_points * tier_multiplier)
            
            # Calculate expiration date
            expires_at = datetime.utcnow() + timedelta(days=LoyaltyService.POINTS_EXPIRATION_DAYS)
            
            # Create transaction
            transaction = LoyaltyPointTransaction(
                user_loyalty_id=user_loyalty.id,
                booking_id=booking.id,
                transaction_type=TransactionType.earned,
                source=TransactionSource.booking,
                points=points_earned,
                description=f"Points earned from booking {booking.booking_number}",
                expires_at=expires_at,
                reference_number=booking.booking_number
            )
            db.add(transaction)
            
            # Update user loyalty
            user_loyalty.total_points += points_earned
            user_loyalty.lifetime_points += points_earned
            user_loyalty.available_points += points_earned
            user_loyalty.last_points_earned_date = datetime.utcnow()
            
            # Check for tier upgrade
            LoyaltyService.check_and_upgrade_tier(db, user_loyalty)
            
            db.commit()
            db.refresh(user_loyalty)
            
            logger.info(f"User {user_id} earned {points_earned} points from booking {booking.booking_number}")
            return points_earned
            
        except Exception as e:
            logger.error(f"Error earning points from booking: {str(e)}")
            db.rollback()
            return 0
    
    @staticmethod
    def check_and_upgrade_tier(db: Session, user_loyalty: UserLoyalty):
        """Check if user should be upgraded to higher tier"""
        current_tier = db.query(LoyaltyTier).filter(LoyaltyTier.id == user_loyalty.tier_id).first()
        if not current_tier:
            return
        
        # Get all tiers ordered by min_points descending
        all_tiers = db.query(LoyaltyTier).filter(LoyaltyTier.is_active == True).order_by(LoyaltyTier.min_points.desc()).all()
        
        for tier in all_tiers:
            if user_loyalty.lifetime_points >= tier.min_points and tier.min_points > current_tier.min_points:
                # Upgrade to this tier
                user_loyalty.tier_id = tier.id
                user_loyalty.tier_started_date = datetime.utcnow()
                
                # Calculate next tier points
                next_tier = LoyaltyService.get_next_tier(db, tier)
                if next_tier:
                    user_loyalty.next_tier_points_needed = next_tier.min_points - user_loyalty.lifetime_points
                else:
                    user_loyalty.next_tier_points_needed = None
                
                logger.info(f"User {user_loyalty.user_id} upgraded to {tier.name} tier")
                break
    
    @staticmethod
    def get_next_tier(db: Session, current_tier: LoyaltyTier) -> Optional[LoyaltyTier]:
        """Get next tier above current tier"""
        all_tiers = db.query(LoyaltyTier).filter(
            LoyaltyTier.is_active == True,
            LoyaltyTier.min_points > current_tier.min_points
        ).order_by(LoyaltyTier.min_points.asc()).first()
        
        return all_tiers
    
    @staticmethod
    def redeem_points(
        db: Session,
        user_id: int,
        reward_id: int,
        booking_id: Optional[int] = None
    ) -> Optional[RewardRedemption]:
        """Redeem points for a reward"""
        try:
            user_loyalty = LoyaltyService.get_or_create_user_loyalty(db, user_id)
            reward = db.query(LoyaltyReward).filter(LoyaltyReward.id == reward_id).first()
            
            if not reward:
                raise ValueError("Reward not found")
            
            # Get user's tier info for better availability checking
            user_tier = db.query(LoyaltyTier).filter(LoyaltyTier.id == user_loyalty.tier_id).first()
            user_tier_min_points = user_tier.min_points if user_tier else None
            
            # If reward requires a specific tier, pre-load tier info for comparison
            if reward.applicable_tier_id:
                required_tier = db.query(LoyaltyTier).filter(LoyaltyTier.id == reward.applicable_tier_id).first()
                if required_tier:
                    reward._required_tier_min_points = required_tier.min_points
            
            if not reward.is_available(user_loyalty.tier_id, user_tier_min_points):
                # Log detailed reason for debugging
                logger.warning(
                    f"Reward {reward.id} not available for user {user_id}: "
                    f"reward_tier={reward.applicable_tier_id}, user_tier={user_loyalty.tier_id}, "
                    f"user_tier_points={user_tier_min_points}, reward_active={reward.is_active}, "
                    f"stock={reward.redeemed_count}/{reward.stock_quantity if reward.stock_quantity else 'unlimited'}"
                )
                if not reward.is_active:
                    raise ValueError("Reward is not active")
                if reward.applicable_tier_id and user_loyalty.tier_id != reward.applicable_tier_id:
                    raise ValueError(f"Reward is only available for specific tier")
                if reward.valid_until and datetime.utcnow() > reward.valid_until:
                    raise ValueError("Reward has expired")
                if reward.stock_quantity is not None and reward.redeemed_count >= reward.stock_quantity:
                    raise ValueError("Reward is out of stock")
                raise ValueError("Reward is not available for your tier or has expired")
            
            if user_loyalty.available_points < reward.points_cost:
                raise ValueError(
                    f"Insufficient points. Required: {reward.points_cost}, Available: {user_loyalty.available_points}"
                )
            
            # Generate redemption code
            redemption_code = LoyaltyService.generate_redemption_code(db)
            
            # Create redemption
            redemption = RewardRedemption(
                user_loyalty_id=user_loyalty.id,
                reward_id=reward.id,
                booking_id=booking_id,
                points_used=reward.points_cost,
                status=RedemptionStatus.active,
                code=redemption_code,
                expires_at=datetime.utcnow() + timedelta(days=365) if reward.reward_type == 'voucher' else None
            )
            db.add(redemption)
            
            # Deduct points
            user_loyalty.available_points -= reward.points_cost
            user_loyalty.total_points -= reward.points_cost
            
            # Create transaction
            transaction = LoyaltyPointTransaction(
                user_loyalty_id=user_loyalty.id,
                booking_id=booking_id,
                transaction_type=TransactionType.redeemed,
                source=TransactionSource.redemption,
                points=-reward.points_cost,
                description=f"Points redeemed for {reward.name}",
                reference_number=redemption_code
            )
            db.add(transaction)
            
            # Update reward redemption count
            reward.redeemed_count += 1
            
            db.commit()
            db.refresh(redemption)
            
            logger.info(f"User {user_id} redeemed {reward.points_cost} points for reward {reward.name}")
            return redemption
            
        except Exception as e:
            logger.error(f"Error redeeming points: {str(e)}")
            db.rollback()
            raise
    
    @staticmethod
    def generate_redemption_code(db: Session, length: int = 12) -> str:
        """Generate unique redemption code using cryptographically secure random"""
        max_attempts = 10
        alphabet = string.ascii_uppercase + string.digits
        for _ in range(max_attempts):
            # Use secrets.choice() instead of random.choices() for security
            code = ''.join(secrets.choice(alphabet) for _ in range(length))
            existing = db.query(RewardRedemption).filter(RewardRedemption.code == code).first()
            if not existing:
                return code
        # Fallback with secure random suffix
        random_suffix = ''.join(secrets.choice(alphabet) for _ in range(4))
        return f"RED{int(datetime.utcnow().timestamp())}{random_suffix}"
    
    @staticmethod
    def process_referral(
        db: Session,
        referred_user_id: int,
        referral_code: str,
        booking_id: Optional[int] = None
    ):
        """Process referral when new user books with referral code"""
        try:
            # Find referrer
            referrer_loyalty = db.query(UserLoyalty).filter(
                UserLoyalty.referral_code == referral_code
            ).first()
            
            if not referrer_loyalty:
                logger.warning(f"Invalid referral code: {referral_code}")
                return
            
            if referrer_loyalty.user_id == referred_user_id:
                logger.warning(f"User cannot refer themselves")
                return
            
            # Check if referral already exists
            existing_referral = db.query(Referral).filter(
                Referral.referrer_id == referrer_loyalty.id,
                Referral.referred_user_id == referred_user_id
            ).first()
            
            if existing_referral and existing_referral.status == ReferralStatus.rewarded:
                logger.info(f"Referral already processed for user {referred_user_id}")
                return
            
            # Create or update referral
            if not existing_referral:
                referral = Referral(
                    referrer_id=referrer_loyalty.id,
                    referred_user_id=referred_user_id,
                    referral_code=referral_code,
                    booking_id=booking_id,
                    status=ReferralStatus.pending
                )
                db.add(referral)
            else:
                referral = existing_referral
                referral.booking_id = booking_id
            
            # Award points when booking is completed
            if booking_id:
                booking = db.query(Booking).filter(Booking.id == booking_id).first()
                if booking and booking.status == BookingStatus.confirmed:
                    # Award referrer points
                    expires_at = datetime.utcnow() + timedelta(days=LoyaltyService.POINTS_EXPIRATION_DAYS)
                    
                    referrer_transaction = LoyaltyPointTransaction(
                        user_loyalty_id=referrer_loyalty.id,
                        transaction_type=TransactionType.earned,
                        source=TransactionSource.referral,
                        points=LoyaltyService.REFERRER_POINTS,
                        description=f"Referral bonus for user {referred_user_id}",
                        expires_at=expires_at,
                        reference_number=referral_code
                    )
                    db.add(referrer_transaction)
                    
                    referrer_loyalty.total_points += LoyaltyService.REFERRER_POINTS
                    referrer_loyalty.lifetime_points += LoyaltyService.REFERRER_POINTS
                    referrer_loyalty.available_points += LoyaltyService.REFERRER_POINTS
                    referrer_loyalty.referral_count += 1
                    referral.referrer_points_earned = LoyaltyService.REFERRER_POINTS
                    
                    # Award referred user bonus points
                    referred_loyalty = LoyaltyService.get_or_create_user_loyalty(db, referred_user_id)
                    
                    referred_transaction = LoyaltyPointTransaction(
                        user_loyalty_id=referred_loyalty.id,
                        transaction_type=TransactionType.bonus,
                        source=TransactionSource.referral,
                        points=LoyaltyService.REFERRED_BONUS_POINTS,
                        description=f"Welcome bonus for using referral code",
                        expires_at=expires_at,
                        reference_number=referral_code
                    )
                    db.add(referred_transaction)
                    
                    referred_loyalty.total_points += LoyaltyService.REFERRED_BONUS_POINTS
                    referred_loyalty.lifetime_points += LoyaltyService.REFERRED_BONUS_POINTS
                    referred_loyalty.available_points += LoyaltyService.REFERRED_BONUS_POINTS
                    referral.referred_points_earned = LoyaltyService.REFERRED_BONUS_POINTS
                    
                    referral.status = ReferralStatus.rewarded
                    referral.completed_at = datetime.utcnow()
                    referral.rewarded_at = datetime.utcnow()
                else:
                    referral.status = ReferralStatus.completed
                    referral.completed_at = datetime.utcnow()
            
            db.commit()
            logger.info(f"Referral processed: referrer {referrer_loyalty.user_id} referred user {referred_user_id}")
            
        except Exception as e:
            logger.error(f"Error processing referral: {str(e)}")
            db.rollback()
    
    @staticmethod
    def check_birthday_reward(db: Session, user_id: int, prevent_recent_update: bool = False):
        """Check and award birthday bonus points
        
        Args:
            db: Database session
            user_id: User ID to check
            prevent_recent_update: If True, prevents reward if birthday was updated recently (within 365 days)
        """
        try:
            user = db.query(User).filter(User.id == user_id).first()
            if not user:
                return
            
            user_loyalty = LoyaltyService.get_or_create_user_loyalty(db, user_id)
            
            if not user_loyalty.birthday:
                return
            
            today = date.today()
            birthday = user_loyalty.birthday
            
            # Prevent abuse: Don't award if birthday was updated within last 365 days (once per year only)
            if prevent_recent_update and user_loyalty.updated_at:
                days_since_update = (datetime.utcnow() - user_loyalty.updated_at).days
                if days_since_update < 365:
                    logger.warning(f"Birthday reward blocked for user {user_id}: birthday updated {days_since_update} days ago (minimum 365 days required - once per year only)")
                    return
            
            # Check if it's user's birthday
            if today.month == birthday.month and today.day == birthday.day:
                # Check if already awarded THIS YEAR (fix: check from Jan 1 of current year to now)
                year_start = datetime(today.year, 1, 1)
                now = datetime.utcnow()
                
                existing_birthday = db.query(LoyaltyPointTransaction).filter(
                    LoyaltyPointTransaction.user_loyalty_id == user_loyalty.id,
                    LoyaltyPointTransaction.source == TransactionSource.birthday,
                    LoyaltyPointTransaction.created_at >= year_start,
                    LoyaltyPointTransaction.created_at <= now
                ).first()
                
                if not existing_birthday:
                    # Award birthday bonus
                    expires_at = datetime.utcnow() + timedelta(days=LoyaltyService.POINTS_EXPIRATION_DAYS)
                    
                    transaction = LoyaltyPointTransaction(
                        user_loyalty_id=user_loyalty.id,
                        transaction_type=TransactionType.bonus,
                        source=TransactionSource.birthday,
                        points=LoyaltyService.BIRTHDAY_BONUS_POINTS,
                        description=f"Birthday bonus - Happy Birthday!",
                        expires_at=expires_at
                    )
                    db.add(transaction)
                    
                    user_loyalty.total_points += LoyaltyService.BIRTHDAY_BONUS_POINTS
                    user_loyalty.lifetime_points += LoyaltyService.BIRTHDAY_BONUS_POINTS
                    user_loyalty.available_points += LoyaltyService.BIRTHDAY_BONUS_POINTS
                    
                    db.commit()
                    logger.info(f"Birthday bonus awarded to user {user_id}")
                else:
                    logger.info(f"Birthday reward already awarded this year for user {user_id}")
                    
        except Exception as e:
            logger.error(f"Error checking birthday reward: {str(e)}")
            db.rollback()
    
    @staticmethod
    def check_anniversary_reward(db: Session, user_id: int, prevent_recent_update: bool = False):
        """Check and award anniversary bonus points
        
        Args:
            db: Database session
            user_id: User ID to check
            prevent_recent_update: If True, prevents reward if anniversary was updated recently (within 365 days)
        """
        try:
            user_loyalty = LoyaltyService.get_or_create_user_loyalty(db, user_id)
            
            if not user_loyalty.anniversary_date:
                return
            
            today = date.today()
            anniversary = user_loyalty.anniversary_date
            
            # Prevent abuse: Don't award if anniversary was updated within last 365 days (once per year only)
            if prevent_recent_update and user_loyalty.updated_at:
                days_since_update = (datetime.utcnow() - user_loyalty.updated_at).days
                if days_since_update < 365:
                    logger.warning(f"Anniversary reward blocked for user {user_id}: anniversary updated {days_since_update} days ago (minimum 365 days required - once per year only)")
                    return
            
            # Check if it's anniversary
            if today.month == anniversary.month and today.day == anniversary.day:
                # Check if already awarded THIS YEAR (fix: check from Jan 1 of current year to now)
                year_start = datetime(today.year, 1, 1)
                now = datetime.utcnow()
                
                existing_anniversary = db.query(LoyaltyPointTransaction).filter(
                    LoyaltyPointTransaction.user_loyalty_id == user_loyalty.id,
                    LoyaltyPointTransaction.source == TransactionSource.anniversary,
                    LoyaltyPointTransaction.created_at >= year_start,
                    LoyaltyPointTransaction.created_at <= now
                ).first()
                
                if not existing_anniversary:
                    # Award anniversary bonus
                    expires_at = datetime.utcnow() + timedelta(days=LoyaltyService.POINTS_EXPIRATION_DAYS)
                    
                    transaction = LoyaltyPointTransaction(
                        user_loyalty_id=user_loyalty.id,
                        transaction_type=TransactionType.bonus,
                        source=TransactionSource.anniversary,
                        points=LoyaltyService.ANNIVERSARY_BONUS_POINTS,
                        description=f"Anniversary bonus - Thank you for your loyalty!",
                        expires_at=expires_at
                    )
                    db.add(transaction)
                    
                    user_loyalty.total_points += LoyaltyService.ANNIVERSARY_BONUS_POINTS
                    user_loyalty.lifetime_points += LoyaltyService.ANNIVERSARY_BONUS_POINTS
                    user_loyalty.available_points += LoyaltyService.ANNIVERSARY_BONUS_POINTS
                    
                    db.commit()
                    logger.info(f"Anniversary bonus awarded to user {user_id}")
                else:
                    logger.info(f"Anniversary reward already awarded this year for user {user_id}")
                    
        except Exception as e:
            logger.error(f"Error checking anniversary reward: {str(e)}")
            db.rollback()
    
    @staticmethod
    def expire_points(db: Session):
        """Expire points that have passed expiration date"""
        try:
            now = datetime.utcnow()
            
            # Find all expired point transactions
            expired_transactions = db.query(LoyaltyPointTransaction).filter(
                LoyaltyPointTransaction.transaction_type == TransactionType.earned,
                LoyaltyPointTransaction.points > 0,
                LoyaltyPointTransaction.expires_at.isnot(None),
                LoyaltyPointTransaction.expires_at < now
            ).all()
            
            for transaction in expired_transactions:
                # Check if already expired
                if transaction.description and 'expired' in transaction.description.lower():
                    continue
                
                user_loyalty = transaction.user_loyalty
                
                # Create expiration transaction
                expiration_transaction = LoyaltyPointTransaction(
                    user_loyalty_id=user_loyalty.id,
                    transaction_type=TransactionType.expired,
                    source=transaction.source,
                    points=-transaction.points,
                    description=f"Points expired from {transaction.description or 'earned points'}",
                    reference_number=transaction.reference_number
                )
                db.add(expiration_transaction)
                
                # Update user loyalty
                user_loyalty.total_points -= transaction.points
                user_loyalty.available_points -= transaction.points
                user_loyalty.expired_points += transaction.points
                
                # Mark transaction as expired
                transaction.description = f"{transaction.description or ''} [EXPIRED]"
            
            db.commit()
            logger.info(f"Expired {len(expired_transactions)} point transactions")
            
        except Exception as e:
            logger.error(f"Error expiring points: {str(e)}")
            db.rollback()

