from sqlalchemy.orm import Session
from datetime import datetime, timedelta
from typing import Optional, List, Dict, Any
import secrets
import string
from decimal import Decimal
from ..models.group_booking import (
    GroupBooking, GroupBookingMember, GroupRoomBlock, GroupPayment,
    GroupBookingStatus, PaymentOption
)
from ..models.booking import Booking, BookingStatus
from ...rooms.models.room import Room, RoomStatus
from ...rooms.models.room_type import RoomType
from ...auth.models.user import User
from ...payments.models.payment import Payment, PaymentStatus, PaymentMethod
import logging

logger = logging.getLogger(__name__)

class GroupBookingService:
    
    @staticmethod
    def generate_group_booking_number(db: Session) -> str:
        """Generate unique group booking number using cryptographically secure random"""
        max_attempts = 10
        alphabet = string.ascii_uppercase + string.digits
        for _ in range(max_attempts):
            timestamp = datetime.utcnow().strftime('%Y%m%d')
            # Use secrets.choice() instead of random.choices() for security
            random_suffix = ''.join(secrets.choice(alphabet) for _ in range(6))
            booking_number = f"GRP-{timestamp}-{random_suffix}"
            
            existing = db.query(GroupBooking).filter(
                GroupBooking.group_booking_number == booking_number
            ).first()
            
            if not existing:
                return booking_number
        
        # Fallback with secure random suffix
        random_suffix = ''.join(secrets.choice(alphabet) for _ in range(4))
        return f"GRP-{int(datetime.utcnow().timestamp())}{random_suffix}"
    
    @staticmethod
    def calculate_group_discount(
        total_rooms: int,
        base_rate: Decimal,
        discount_percentage: Optional[float] = None
    ) -> Dict[str, Any]:
        """Calculate group discount based on number of rooms"""
        original_total = base_rate * total_rooms
        
        # Default discount tiers
        if discount_percentage is None:
            if total_rooms >= 20:
                discount_percentage = 15.0
            elif total_rooms >= 10:
                discount_percentage = 10.0
            elif total_rooms >= 5:
                discount_percentage = 5.0
            else:
                discount_percentage = 0.0
        
        discount_amount = original_total * Decimal(str(discount_percentage)) / Decimal('100')
        total_price = original_total - discount_amount
        
        return {
            'original_total': float(original_total),
            'discount_percentage': discount_percentage,
            'discount_amount': float(discount_amount),
            'total_price': float(total_price)
        }
    
    @staticmethod
    def check_room_availability(
        db: Session,
        room_type_id: int,
        check_in: datetime,
        check_out: datetime,
        num_rooms: int
    ) -> Dict[str, Any]:
        """Check if enough rooms are available for blocking"""
        # Get all rooms of this type
        rooms = db.query(Room).filter(Room.room_type_id == room_type_id).all()
        
        if len(rooms) < num_rooms:
            return {
                'available': False,
                'available_count': len(rooms),
                'required_count': num_rooms,
                'message': f'Only {len(rooms)} rooms available, {num_rooms} required'
            }
        
        # Check for conflicting bookings
        available_rooms = []
        for room in rooms:
            # Check if room has any bookings during the period
            conflicting_bookings = db.query(Booking).filter(
                Booking.room_id == room.id,
                Booking.status.in_([BookingStatus.confirmed, BookingStatus.checked_in]),
                Booking.check_in_date < check_out,
                Booking.check_out_date > check_in
            ).count()
            
            # Check for other group blocks
            conflicting_blocks = db.query(GroupRoomBlock).join(GroupBooking).filter(
                GroupRoomBlock.room_type_id == room_type_id,
                GroupBooking.status.in_([
                    GroupBookingStatus.confirmed,
                    GroupBookingStatus.partially_confirmed,
                    GroupBookingStatus.checked_in
                ]),
                GroupBooking.check_in_date < check_out,
                GroupBooking.check_out_date > check_in,
                GroupRoomBlock.is_active == True
            ).count()
            
            if conflicting_bookings == 0 and conflicting_blocks == 0:
                available_rooms.append(room)
        
        if len(available_rooms) < num_rooms:
            return {
                'available': False,
                'available_count': len(available_rooms),
                'required_count': num_rooms,
                'message': f'Only {len(available_rooms)} rooms available for the selected dates'
            }
        
        return {
            'available': True,
            'available_count': len(available_rooms),
            'required_count': num_rooms
        }
    
    @staticmethod
    def create_group_booking(
        db: Session,
        coordinator_id: int,
        coordinator_name: str,
        coordinator_email: str,
        coordinator_phone: Optional[str],
        check_in_date: datetime,
        check_out_date: datetime,
        room_blocks: List[Dict[str, Any]],
        group_name: Optional[str] = None,
        group_type: Optional[str] = None,
        payment_option: PaymentOption = PaymentOption.coordinator_pays_all,
        deposit_required: bool = False,
        deposit_percentage: Optional[int] = None,
        special_requests: Optional[str] = None,
        notes: Optional[str] = None,
        cancellation_policy: Optional[str] = None,
        cancellation_deadline: Optional[datetime] = None,
        cancellation_penalty_percentage: Optional[float] = None,
        group_discount_percentage: Optional[float] = None
    ) -> GroupBooking:
        """Create a new group booking with room blocks"""
        
        # Validate dates
        if check_out_date <= check_in_date:
            raise ValueError("Check-out date must be after check-in date")
        
        # Calculate total rooms and base pricing
        total_rooms = sum(block.get('num_rooms', 0) for block in room_blocks)
        if total_rooms == 0:
            raise ValueError("At least one room must be blocked")
        
        # Calculate pricing for each room block
        total_original_price = Decimal('0')
        room_block_objects = []
        
        for block_data in room_blocks:
            room_type_id = block_data.get('room_type_id')
            num_rooms = block_data.get('num_rooms', 0)
            rate_per_room = Decimal(str(block_data.get('rate_per_room', 0)))
            
            if not room_type_id or num_rooms <= 0:
                continue
            
            # Check availability
            availability = GroupBookingService.check_room_availability(
                db, room_type_id, check_in_date, check_out_date, num_rooms
            )
            
            if not availability['available']:
                raise ValueError(availability.get('message', 'Rooms not available'))
            
            # Get room type
            room_type = db.query(RoomType).filter(RoomType.id == room_type_id).first()
            if not room_type:
                raise ValueError(f"Room type {room_type_id} not found")
            
            block_total = rate_per_room * num_rooms
            total_original_price += block_total
            
            # Create room block object (will be saved later)
            room_block = GroupRoomBlock(
                room_type_id=room_type_id,
                rooms_blocked=num_rooms,
                rooms_confirmed=0,
                rooms_available=num_rooms,
                rate_per_room=rate_per_room,
                total_block_price=block_total,
                is_active=True
            )
            room_block_objects.append(room_block)
        
        # Calculate group discount
        base_rate = total_original_price / total_rooms if total_rooms > 0 else Decimal('0')
        pricing = GroupBookingService.calculate_group_discount(
            total_rooms, base_rate, group_discount_percentage
        )
        
        # Calculate deposit
        deposit_amount = None
        if deposit_required:
            if deposit_percentage:
                deposit_amount = Decimal(str(pricing['total_price'])) * Decimal(str(deposit_percentage)) / Decimal('100')
            else:
                deposit_amount = Decimal(str(pricing['total_price'])) * Decimal('0.2')  # Default 20%
        
        # Create group booking
        group_booking_number = GroupBookingService.generate_group_booking_number(db)
        
        group_booking = GroupBooking(
            group_booking_number=group_booking_number,
            coordinator_id=coordinator_id,
            coordinator_name=coordinator_name,
            coordinator_email=coordinator_email,
            coordinator_phone=coordinator_phone,
            group_name=group_name,
            group_type=group_type,
            total_rooms=total_rooms,
            total_guests=0,  # Will be updated when members are added
            check_in_date=check_in_date,
            check_out_date=check_out_date,
            base_rate_per_room=base_rate,
            group_discount_percentage=Decimal(str(pricing['discount_percentage'])),
            group_discount_amount=Decimal(str(pricing['discount_amount'])),
            original_total_price=Decimal(str(pricing['original_total'])),
            discount_amount=Decimal(str(pricing['discount_amount'])),
            total_price=Decimal(str(pricing['total_price'])),
            payment_option=payment_option,
            deposit_required=deposit_required,
            deposit_percentage=deposit_percentage,
            deposit_amount=deposit_amount,
            amount_paid=Decimal('0'),
            balance_due=Decimal(str(pricing['total_price'])),
            status=GroupBookingStatus.draft,
            cancellation_policy=cancellation_policy,
            cancellation_deadline=cancellation_deadline,
            cancellation_penalty_percentage=Decimal(str(cancellation_penalty_percentage)) if cancellation_penalty_percentage else None,
            special_requests=special_requests,
            notes=notes
        )
        
        db.add(group_booking)
        db.flush()
        
        # Add room blocks
        for room_block in room_block_objects:
            room_block.group_booking_id = group_booking.id
            db.add(room_block)
        
        db.commit()
        db.refresh(group_booking)
        
        return group_booking
    
    @staticmethod
    def add_member_to_group(
        db: Session,
        group_booking_id: int,
        full_name: str,
        email: Optional[str] = None,
        phone: Optional[str] = None,
        user_id: Optional[int] = None,
        room_block_id: Optional[int] = None,
        special_requests: Optional[str] = None,
        preferences: Optional[Dict[str, Any]] = None
    ) -> GroupBookingMember:
        """Add a member to a group booking"""
        
        group_booking = db.query(GroupBooking).filter(
            GroupBooking.id == group_booking_id
        ).first()
        
        if not group_booking:
            raise ValueError("Group booking not found")
        
        # Calculate individual amount if individual payment
        individual_amount = None
        if group_booking.payment_option == PaymentOption.individual_payments:
            # Distribute cost evenly among members
            current_member_count = db.query(GroupBookingMember).filter(
                GroupBookingMember.group_booking_id == group_booking_id
            ).count()
            individual_amount = group_booking.total_price / (current_member_count + 1)
        
        member = GroupBookingMember(
            group_booking_id=group_booking_id,
            full_name=full_name,
            email=email,
            phone=phone,
            user_id=user_id,
            room_block_id=room_block_id,
            special_requests=special_requests,
            preferences=preferences,
            individual_amount=individual_amount,
            individual_paid=Decimal('0'),
            individual_balance=individual_amount if individual_amount else Decimal('0')
        )
        
        db.add(member)
        
        # Update total guests
        group_booking.total_guests += 1
        
        db.commit()
        db.refresh(member)
        
        return member
    
    @staticmethod
    def confirm_group_booking(
        db: Session,
        group_booking_id: int
    ) -> GroupBooking:
        """Confirm a group booking and activate room blocks"""
        
        group_booking = db.query(GroupBooking).filter(
            GroupBooking.id == group_booking_id
        ).first()
        
        if not group_booking:
            raise ValueError("Group booking not found")
        
        if group_booking.status not in [GroupBookingStatus.draft, GroupBookingStatus.pending]:
            raise ValueError(f"Cannot confirm booking with status {group_booking.status}")
        
        # Re-check availability
        room_blocks = db.query(GroupRoomBlock).filter(
            GroupRoomBlock.group_booking_id == group_booking_id
        ).all()
        
        for room_block in room_blocks:
            availability = GroupBookingService.check_room_availability(
                db,
                room_block.room_type_id,
                group_booking.check_in_date,
                group_booking.check_out_date,
                room_block.rooms_blocked
            )
            
            if not availability['available']:
                raise ValueError(f"Rooms no longer available: {availability.get('message')}")
        
        # Update status
        group_booking.status = GroupBookingStatus.confirmed
        group_booking.confirmed_at = datetime.utcnow()
        
        db.commit()
        db.refresh(group_booking)
        
        return group_booking
    
    @staticmethod
    def create_individual_booking_from_member(
        db: Session,
        member_id: int,
        room_id: int
    ) -> Booking:
        """Create an individual booking for a group member"""
        
        member = db.query(GroupBookingMember).filter(
            GroupBookingMember.id == member_id
        ).first()
        
        if not member:
            raise ValueError("Group member not found")
        
        group_booking = member.group_booking
        
        if not group_booking:
            raise ValueError("Group booking not found")
        
        # Check if room is available
        room = db.query(Room).filter(Room.id == room_id).first()
        if not room:
            raise ValueError("Room not found")
        
        # Verify room type matches
        if member.room_block_id:
            room_block = db.query(GroupRoomBlock).filter(
                GroupRoomBlock.id == member.room_block_id
            ).first()
            if room_block and room.room_type_id != room_block.room_type_id:
                raise ValueError("Room type does not match the assigned room block")
        
        # Calculate price for this booking
        nights = (group_booking.check_out_date - group_booking.check_in_date).days
        if nights <= 0:
            nights = 1
        
        if member.individual_amount:
            booking_price = member.individual_amount
        else:
            # Use proportional share
            booking_price = group_booking.total_price / group_booking.total_rooms
        
        # Generate booking number using cryptographically secure random
        prefix = 'BK'
        ts = int(datetime.utcnow().timestamp() * 1000)
        # Use secrets.randbelow() instead of random.randint() for security
        rand = secrets.randbelow(9000) + 1000  # Random number between 1000-9999
        booking_number = f'{prefix}-{ts}-{rand}'
        
        # Ensure uniqueness
        existing = db.query(Booking).filter(Booking.booking_number == booking_number).first()
        if existing:
            # If collision, generate new secure random number
            rand = secrets.randbelow(9000) + 1000
            booking_number = f'{prefix}-{ts}-{rand}'
        
        # Create booking
        booking = Booking(
            booking_number=booking_number,
            user_id=member.user_id if member.user_id else group_booking.coordinator_id,
            room_id=room_id,
            check_in_date=group_booking.check_in_date,
            check_out_date=group_booking.check_out_date,
            num_guests=1,
            total_price=booking_price,
            original_price=booking_price,
            discount_amount=Decimal('0'),
            status=BookingStatus.confirmed,
            deposit_paid=False,
            requires_deposit=False,
            special_requests=member.special_requests,
            group_booking_id=group_booking_id
        )
        
        db.add(booking)
        
        # Update member
        member.assigned_room_id = room_id
        member.individual_booking_id = booking.id
        
        # Update room block
        if member.room_block_id:
            room_block = db.query(GroupRoomBlock).filter(
                GroupRoomBlock.id == member.room_block_id
            ).first()
            if room_block:
                room_block.rooms_confirmed += 1
                room_block.rooms_available -= 1
        
        # Update group booking status
        confirmed_count = db.query(GroupBookingMember).filter(
            GroupBookingMember.group_booking_id == group_booking_id,
            GroupBookingMember.individual_booking_id.isnot(None)
        ).count()
        
        if confirmed_count == group_booking.total_rooms:
            group_booking.status = GroupBookingStatus.confirmed
        elif confirmed_count > 0:
            group_booking.status = GroupBookingStatus.partially_confirmed
        
        db.commit()
        db.refresh(booking)
        
        return booking
    
    @staticmethod
    def add_group_payment(
        db: Session,
        group_booking_id: int,
        amount: Decimal,
        payment_method: str,
        payment_type: str = 'deposit',
        transaction_id: Optional[str] = None,
        paid_by_member_id: Optional[int] = None,
        paid_by_user_id: Optional[int] = None,
        notes: Optional[str] = None
    ) -> GroupPayment:
        """Add a payment to a group booking"""
        
        group_booking = db.query(GroupBooking).filter(
            GroupBooking.id == group_booking_id
        ).first()
        
        if not group_booking:
            raise ValueError("Group booking not found")
        
        payment = GroupPayment(
            group_booking_id=group_booking_id,
            amount=amount,
            payment_method=payment_method,
            payment_type=payment_type,
            payment_status='completed',
            transaction_id=transaction_id,
            payment_date=datetime.utcnow(),
            paid_by_member_id=paid_by_member_id,
            paid_by_user_id=paid_by_user_id,
            notes=notes
        )
        
        db.add(payment)
        
        # Update group booking payment totals
        group_booking.amount_paid += amount
        group_booking.balance_due = group_booking.total_price - group_booking.amount_paid
        
        # Update member payment if individual payment
        if paid_by_member_id:
            member = db.query(GroupBookingMember).filter(
                GroupBookingMember.id == paid_by_member_id
            ).first()
            if member:
                member.individual_paid += amount
                member.individual_balance = (member.individual_amount or Decimal('0')) - member.individual_paid
        
        db.commit()
        db.refresh(payment)
        
        return payment
    
    @staticmethod
    def cancel_group_booking(
        db: Session,
        group_booking_id: int,
        cancellation_reason: Optional[str] = None
    ) -> GroupBooking:
        """Cancel a group booking"""
        
        group_booking = db.query(GroupBooking).filter(
            GroupBooking.id == group_booking_id
        ).first()
        
        if not group_booking:
            raise ValueError("Group booking not found")
        
        if group_booking.status in [GroupBookingStatus.checked_out, GroupBookingStatus.cancelled]:
            raise ValueError(f"Cannot cancel booking with status {group_booking.status}")
        
        # Calculate cancellation penalty
        penalty_amount = Decimal('0')
        if group_booking.cancellation_penalty_percentage:
            penalty_amount = group_booking.total_price * (
                Decimal(str(group_booking.cancellation_penalty_percentage)) / Decimal('100')
            )
        
        # Update status
        group_booking.status = GroupBookingStatus.cancelled
        group_booking.cancelled_at = datetime.utcnow()
        
        # Release room blocks
        room_blocks = db.query(GroupRoomBlock).filter(
            GroupRoomBlock.group_booking_id == group_booking_id
        ).all()
        
        for room_block in room_blocks:
            room_block.is_active = False
            room_block.block_released_at = datetime.utcnow()
        
        # Cancel individual bookings if any
        individual_bookings = db.query(Booking).filter(
            Booking.group_booking_id == group_booking_id
        ).all()
        
        for booking in individual_bookings:
            if booking.status not in [BookingStatus.checked_out, BookingStatus.cancelled]:
                booking.status = BookingStatus.cancelled
        
        db.commit()
        db.refresh(group_booking)
        
        return group_booking

