from sqlalchemy.orm import Session
from sqlalchemy import func, and_, or_
from typing import Optional, Dict, Any, List
from datetime import datetime, timedelta
from ..models.invoice import Invoice, InvoiceItem, InvoiceStatus
from ...bookings.models.booking import Booking
from ..models.payment import Payment, PaymentStatus
from ...auth.models.user import User
from ...system.models.system_settings import SystemSettings
from ...shared.config.logging_config import get_logger

logger = get_logger(__name__)

def generate_invoice_number(db: Session, is_proforma: bool=False) -> str:
    prefix = 'PRO' if is_proforma else 'INV'
    today = datetime.utcnow().strftime('%Y%m%d')
    last_invoice = db.query(Invoice).filter(Invoice.invoice_number.like(f'{prefix}-{today}-%')).order_by(Invoice.invoice_number.desc()).first()
    if last_invoice:
        try:
            sequence = int(last_invoice.invoice_number.split('-')[-1])
            sequence += 1
        except (ValueError, IndexError):
            sequence = 1
    else:
        sequence = 1
    return f'{prefix}-{today}-{sequence:04d}'

class InvoiceService:

    @staticmethod
    def create_invoice_from_booking(booking_id: int, db: Session, created_by_id: Optional[int]=None, tax_rate: float=0.0, discount_amount: float=0.0, due_days: int=30, is_proforma: bool=False, invoice_amount: Optional[float]=None, request_id: Optional[str]=None, **kwargs) -> Dict[str, Any]:
        from sqlalchemy.orm import selectinload
        from ...hotel_services.models.service_usage import ServiceUsage
        from ...rooms.models.room import Room
        from ...rooms.models.room_type import RoomType
        from ...hotel_services.models.service import Service
        from sqlalchemy.exc import IntegrityError

        logger.info(
            f'Creating invoice from booking {booking_id}',
            extra={'booking_id': booking_id, 'request_id': request_id},
        )

        # NOTE:
        # FastAPI's `get_db` dependency already provides a Session with an
        # active transaction. Calling `Session.begin()` again on that same
        # Session results in `InvalidRequestError: A transaction is already begun`.
        # Instead of managing a separate Transaction object, we operate on the
        # Session directly and use `db.commit()` / `db.rollback()`.
        try:
            # Lock booking row to prevent race conditions
            booking = (
                db.query(Booking)
                .options(
                    selectinload(Booking.service_usages).selectinload(
                        ServiceUsage.service
                    ),
                    selectinload(Booking.room).selectinload(Room.room_type),
                    selectinload(Booking.payments),
                )
                .filter(Booking.id == booking_id)
                .with_for_update()
                .first()
            )
            if not booking:
                db.rollback()
                logger.error(
                    f'Booking {booking_id} not found',
                    extra={'booking_id': booking_id, 'request_id': request_id},
                )
                raise ValueError('Booking not found')
            user = db.query(User).filter(User.id == booking.user_id).first()
            if not user:
                db.rollback()
                raise ValueError('User not found')
        
            # Get tax_rate from system settings if not provided or is 0
            if tax_rate == 0.0:
                tax_rate_setting = db.query(SystemSettings).filter(SystemSettings.key == 'tax_rate').first()
                if tax_rate_setting and tax_rate_setting.value:
                    try:
                        tax_rate = float(tax_rate_setting.value)
                    except (ValueError, TypeError):
                        tax_rate = 0.0
            
            invoice_number = generate_invoice_number(db, is_proforma=is_proforma)
            booking_total = float(booking.total_price)
            if invoice_amount is not None:
                subtotal = float(invoice_amount)
                if invoice_amount < booking_total and discount_amount > 0:
                    if discount_amount > subtotal * 0.5:
                        proportion = float(invoice_amount) / booking_total
                        original_discount = float(booking.discount_amount) if booking.discount_amount else discount_amount
                        discount_amount = original_discount * proportion
            else:
                subtotal = booking_total
            tax_amount = (subtotal - discount_amount) * (tax_rate / 100)
            total_amount = subtotal + tax_amount - discount_amount
            amount_paid = sum((float(p.amount) for p in booking.payments if p.payment_status == PaymentStatus.completed))
            balance_due = total_amount - amount_paid
            if balance_due <= 0:
                status = InvoiceStatus.paid
                paid_date = datetime.utcnow()
            elif amount_paid > 0:
                status = InvoiceStatus.sent
                paid_date = None
            else:
                status = InvoiceStatus.draft
                paid_date = None
            # Get company information from system settings if not provided
            company_name = kwargs.get('company_name')
            company_address = kwargs.get('company_address')
            company_phone = kwargs.get('company_phone')
            company_email = kwargs.get('company_email')
            company_tax_id = kwargs.get('company_tax_id')
            company_logo_url = kwargs.get('company_logo_url')
            
            # If company info not provided, fetch from system settings
            if not company_name or not company_address or not company_phone or not company_email:
                company_settings = db.query(SystemSettings).filter(
                    SystemSettings.key.in_(['company_name', 'company_address', 'company_phone', 'company_email', 'company_logo_url'])
                ).all()
                
                settings_dict = {setting.key: setting.value for setting in company_settings if setting.value}
                
                if not company_name and settings_dict.get('company_name'):
                    company_name = settings_dict['company_name']
                if not company_address and settings_dict.get('company_address'):
                    company_address = settings_dict['company_address']
                if not company_phone and settings_dict.get('company_phone'):
                    company_phone = settings_dict['company_phone']
                if not company_email and settings_dict.get('company_email'):
                    company_email = settings_dict['company_email']
                if not company_logo_url and settings_dict.get('company_logo_url'):
                    company_logo_url = settings_dict['company_logo_url']
            
            invoice = Invoice(invoice_number=invoice_number, booking_id=booking_id, user_id=booking.user_id, issue_date=datetime.utcnow(), due_date=datetime.utcnow() + timedelta(days=due_days), paid_date=paid_date, subtotal=subtotal, tax_rate=tax_rate, tax_amount=tax_amount, discount_amount=discount_amount, total_amount=total_amount, amount_paid=amount_paid, balance_due=balance_due, status=status, is_proforma=is_proforma, company_name=company_name, company_address=company_address, company_phone=company_phone, company_email=company_email, company_tax_id=company_tax_id, company_logo_url=company_logo_url, customer_name=user.full_name or f'{user.email}', customer_email=user.email, customer_address=user.address, customer_phone=user.phone, customer_tax_id=kwargs.get('customer_tax_id'), notes=kwargs.get('notes'), terms_and_conditions=kwargs.get('terms_and_conditions'), payment_instructions=kwargs.get('payment_instructions'), created_by_id=created_by_id)
            db.add(invoice)
            db.flush()
            services_total = sum((float(su.total_price) for su in booking.service_usages))
            booking_total = float(booking.total_price)
            room_price = booking_total - services_total
            nights = (booking.check_out_date - booking.check_in_date).days
            if nights <= 0:
                nights = 1
            if invoice_amount is not None and invoice_amount < booking_total:
                proportion = float(invoice_amount) / booking_total
                room_price = room_price * proportion
                services_total = services_total * proportion
                item_description_suffix = f' (Partial: {proportion * 100:.0f}%)'
            else:
                item_description_suffix = ''
            room_item = InvoiceItem(invoice_id=invoice.id, description=f'Room: {booking.room.room_number} - {(booking.room.room_type.name if booking.room.room_type else 'N/A')} ({nights} night{('s' if nights > 1 else '')}){item_description_suffix}', quantity=nights, unit_price=room_price / nights if nights > 0 else room_price, tax_rate=tax_rate, discount_amount=0.0, line_total=room_price, room_id=booking.room_id)
            db.add(room_item)
            for service_usage in booking.service_usages:
                service_item_price = float(service_usage.total_price)
                if invoice_amount is not None and invoice_amount < booking_total:
                    proportion = float(invoice_amount) / booking_total
                    service_item_price = service_item_price * proportion
                service_item = InvoiceItem(invoice_id=invoice.id, description=f'Service: {service_usage.service.name}{item_description_suffix}', quantity=float(service_usage.quantity), unit_price=service_item_price / float(service_usage.quantity) if service_usage.quantity > 0 else service_item_price, tax_rate=tax_rate, discount_amount=0.0, line_total=service_item_price, service_id=service_usage.service_id)
                db.add(service_item)
            subtotal = room_price + services_total
            tax_amount = (subtotal - discount_amount) * (tax_rate / 100)
            total_amount = subtotal + tax_amount - discount_amount
            balance_due = total_amount - amount_paid
            invoice.subtotal = subtotal
            invoice.tax_amount = tax_amount
            invoice.total_amount = total_amount
            invoice.balance_due = balance_due

            # Commit all invoice changes in the current Session transaction
            db.commit()
            db.refresh(invoice)
            return InvoiceService.invoice_to_dict(invoice)
        except IntegrityError as e:
            db.rollback()
            logger.error(
                f'Database integrity error during invoice creation: {str(e)}',
                extra={'booking_id': booking_id, 'request_id': request_id},
            )
            raise ValueError(f'Invoice creation failed due to database conflict: {str(e)}')
        except Exception as e:
            db.rollback()
            logger.error(
                f'Error creating invoice: {str(e)}',
                extra={'booking_id': booking_id, 'request_id': request_id},
                exc_info=True,
            )
            raise

    @staticmethod
    def update_invoice(invoice_id: int, db: Session, updated_by_id: Optional[int]=None, request_id: Optional[str]=None, **kwargs) -> Dict[str, Any]:
        invoice = db.query(Invoice).filter(Invoice.id == invoice_id).first()
        if not invoice:
            raise ValueError('Invoice not found')
        allowed_fields = ['company_name', 'company_address', 'company_phone', 'company_email', 'company_tax_id', 'company_logo_url', 'notes', 'terms_and_conditions', 'payment_instructions', 'status', 'due_date', 'tax_rate', 'discount_amount']
        for field in allowed_fields:
            if field in kwargs:
                setattr(invoice, field, kwargs[field])
        if 'tax_rate' in kwargs or 'discount_amount' in kwargs:
            tax_rate = kwargs.get('tax_rate', invoice.tax_rate)
            discount_amount = kwargs.get('discount_amount', invoice.discount_amount)
            # Convert decimal types to float for arithmetic operations
            subtotal = float(invoice.subtotal) if invoice.subtotal else 0.0
            discount_amount = float(discount_amount) if discount_amount else 0.0
            tax_rate = float(tax_rate) if tax_rate else 0.0
            amount_paid = float(invoice.amount_paid) if invoice.amount_paid else 0.0
            
            invoice.tax_amount = (subtotal - discount_amount) * (tax_rate / 100)
            invoice.total_amount = subtotal + invoice.tax_amount - discount_amount
            invoice.balance_due = invoice.total_amount - amount_paid
        if invoice.balance_due <= 0 and invoice.status != InvoiceStatus.paid:
            invoice.status = InvoiceStatus.paid
            invoice.paid_date = datetime.utcnow()
        elif invoice.balance_due > 0 and invoice.status == InvoiceStatus.paid:
            invoice.status = InvoiceStatus.sent
            invoice.paid_date = None
        invoice.updated_by_id = updated_by_id
        invoice.updated_at = datetime.utcnow()
        db.commit()
        db.refresh(invoice)
        return InvoiceService.invoice_to_dict(invoice)

    @staticmethod
    def mark_invoice_as_paid(invoice_id: int, db: Session, amount: Optional[float]=None, updated_by_id: Optional[int]=None, request_id: Optional[str]=None) -> Dict[str, Any]:
        invoice = db.query(Invoice).filter(Invoice.id == invoice_id).first()
        if not invoice:
            raise ValueError('Invoice not found')
        payment_amount = amount if amount is not None else float(invoice.balance_due)
        invoice.amount_paid += payment_amount
        invoice.balance_due = invoice.total_amount - invoice.amount_paid
        if invoice.balance_due <= 0:
            invoice.status = InvoiceStatus.paid
            invoice.paid_date = datetime.utcnow()
        else:
            invoice.status = InvoiceStatus.sent
        invoice.updated_by_id = updated_by_id
        invoice.updated_at = datetime.utcnow()
        db.commit()
        db.refresh(invoice)
        return InvoiceService.invoice_to_dict(invoice)

    @staticmethod
    def get_invoice(invoice_id: int, db: Session) -> Optional[Dict[str, Any]]:
        invoice = db.query(Invoice).filter(Invoice.id == invoice_id).first()
        if not invoice:
            return None
        return InvoiceService.invoice_to_dict(invoice)

    @staticmethod
    def get_invoices(db: Session, user_id: Optional[int]=None, booking_id: Optional[int]=None, status: Optional[str]=None, page: int=1, limit: int=10) -> Dict[str, Any]:
        query = db.query(Invoice)
        if user_id:
            query = query.filter(Invoice.user_id == user_id)
        if booking_id:
            query = query.filter(Invoice.booking_id == booking_id)
        if status:
            try:
                status_enum = InvoiceStatus(status)
                query = query.filter(Invoice.status == status_enum)
            except ValueError:
                pass
        total = query.count()
        offset = (page - 1) * limit
        invoices = query.order_by(Invoice.created_at.desc()).offset(offset).limit(limit).all()
        return {'invoices': [InvoiceService.invoice_to_dict(inv) for inv in invoices], 'total': total, 'page': page, 'limit': limit, 'total_pages': (total + limit - 1) // limit}

    @staticmethod
    def invoice_to_dict(invoice: Invoice) -> Dict[str, Any]:
        promotion_code = None
        if invoice.notes and 'Promotion Code:' in invoice.notes:
            try:
                promotion_code = invoice.notes.split('Promotion Code:')[1].split('\n')[0].strip()
            except:
                pass
        return {'id': invoice.id, 'invoice_number': invoice.invoice_number, 'booking_id': invoice.booking_id, 'user_id': invoice.user_id, 'issue_date': invoice.issue_date.isoformat() if invoice.issue_date else None, 'due_date': invoice.due_date.isoformat() if invoice.due_date else None, 'paid_date': invoice.paid_date.isoformat() if invoice.paid_date else None, 'subtotal': float(invoice.subtotal) if invoice.subtotal else 0.0, 'tax_rate': float(invoice.tax_rate) if invoice.tax_rate else 0.0, 'tax_amount': float(invoice.tax_amount) if invoice.tax_amount else 0.0, 'discount_amount': float(invoice.discount_amount) if invoice.discount_amount else 0.0, 'total_amount': float(invoice.total_amount) if invoice.total_amount else 0.0, 'amount_paid': float(invoice.amount_paid) if invoice.amount_paid else 0.0, 'balance_due': float(invoice.balance_due) if invoice.balance_due else 0.0, 'status': invoice.status.value if invoice.status else None, 'company_name': invoice.company_name, 'company_address': invoice.company_address, 'company_phone': invoice.company_phone, 'company_email': invoice.company_email, 'company_tax_id': invoice.company_tax_id, 'company_logo_url': invoice.company_logo_url, 'customer_name': invoice.customer_name, 'customer_email': invoice.customer_email, 'customer_address': invoice.customer_address, 'customer_phone': invoice.customer_phone, 'customer_tax_id': invoice.customer_tax_id, 'notes': invoice.notes, 'terms_and_conditions': invoice.terms_and_conditions, 'payment_instructions': invoice.payment_instructions, 'is_proforma': invoice.is_proforma if hasattr(invoice, 'is_proforma') else False, 'promotion_code': promotion_code, 'items': [{'id': item.id, 'description': item.description, 'quantity': float(item.quantity) if item.quantity else 0.0, 'unit_price': float(item.unit_price) if item.unit_price else 0.0, 'tax_rate': float(item.tax_rate) if item.tax_rate else 0.0, 'discount_amount': float(item.discount_amount) if item.discount_amount else 0.0, 'line_total': float(item.line_total) if item.line_total else 0.0, 'room_id': item.room_id, 'service_id': item.service_id} for item in invoice.items], 'created_at': invoice.created_at.isoformat() if invoice.created_at else None, 'updated_at': invoice.updated_at.isoformat() if invoice.updated_at else None}