from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError
from fastapi import HTTPException, status
from typing import Optional
from decimal import Decimal
from . import repository
from .schema import (
    ServiceCreate, ServiceUpdate,
    InvoiceCreate, InvoiceUpdate,
    InvoicelineitemCreate, InvoicelineitemUpdate,
    PaymentCreate, PaymentUpdate,
    ProcessPaymentRequest, IssueInvoiceRequest,
)
from patient import repository as patient_repo
from appointment import repository as appointment_repo
from department import repository as department_repo
from utils.utils import utc_now, InvoiceStatus


# Service handlers
def create_service(db: Session, data: ServiceCreate):
    if data.department_id:
        dept = department_repo.get_by_id(db, data.department_id)
        if not dept:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Referenced department does not exist")
    
    try:
        obj = repository.create(db, data.model_dump())
        db.commit()
        db.refresh(obj)
        return obj
    except IntegrityError as exc:
        db.rollback()
        msg = str(exc.orig).lower() if exc.orig else ""
        if "unique" in msg or "duplicate" in msg:
            raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Service already exists")
        if "foreign key" in msg:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Referenced resource does not exist")
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Data integrity error")
    except HTTPException:
        db.rollback()
        raise
    except Exception:
        db.rollback()
        raise


def get_service(db: Session, entity_id: str):
    obj = repository.get_by_id(db, entity_id)
    if not obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Service not found")
    return obj


def list_services(db: Session, limit: int, offset: int, is_active: Optional[bool] = None, service_type: Optional[str] = None, department_id: Optional[str] = None):
    filters = {}
    if is_active is not None:
        filters["is_active"] = is_active
    if service_type is not None:
        filters["service_type"] = service_type
    if department_id is not None:
        filters["department_id"] = department_id
    items = repository.list_all(db, limit=limit, offset=offset, **filters)
    total = repository.count_all(db, **filters)
    return {"items": items, "total": total, "limit": limit, "offset": offset}


def update_service(db: Session, entity_id: str, data: ServiceUpdate):
    obj = repository.get_by_id(db, entity_id)
    if not obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Service not found")
    
    update_data = data.model_dump(exclude_unset=True)
    if "department_id" in update_data and update_data["department_id"] is not None:
        dept = department_repo.get_by_id(db, update_data["department_id"])
        if not dept:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Referenced department does not exist")
    
    try:
        updated_obj = repository.update(db, entity_id, update_data)
        db.commit()
        db.refresh(updated_obj)
        return updated_obj
    except IntegrityError as exc:
        db.rollback()
        msg = str(exc.orig).lower() if exc.orig else ""
        if "unique" in msg or "duplicate" in msg:
            raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Service already exists")
        if "foreign key" in msg:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Referenced resource does not exist")
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Data integrity error")
    except HTTPException:
        db.rollback()
        raise
    except Exception:
        db.rollback()
        raise


def delete_service(db: Session, entity_id: str):
    obj = repository.get_by_id(db, entity_id)
    if not obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Service not found")
    
    try:
        repository.delete(db, entity_id)
        db.commit()
        return {"message": "Service deleted successfully"}
    except IntegrityError as exc:
        db.rollback()
        msg = str(exc.orig).lower() if exc.orig else ""
        if "foreign key" in msg:
            raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Cannot delete service as it is referenced by invoice line items")
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Data integrity error")
    except HTTPException:
        db.rollback()
        raise
    except Exception:
        db.rollback()
        raise


# Invoice handlers
def create_invoice(db: Session, data: InvoiceCreate):
    patient = patient_repo.get_by_id(db, data.patient_id)
    if not patient:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Referenced patient does not exist")
    
    if data.appointment_id:
        appt = appointment_repo.get_by_id(db, data.appointment_id)
        if not appt:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Referenced appointment does not exist")
    
    try:
        obj = repository.invoice_create(db, data.model_dump())
        db.commit()
        db.refresh(obj)
        return obj
    except IntegrityError as exc:
        db.rollback()
        msg = str(exc.orig).lower() if exc.orig else ""
        if "unique" in msg or "duplicate" in msg:
            raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Invoice with this invoice_number already exists")
        if "foreign key" in msg:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Referenced resource does not exist")
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Data integrity error")
    except HTTPException:
        db.rollback()
        raise
    except Exception:
        db.rollback()
        raise


def get_invoice(db: Session, entity_id: str):
    obj = repository.invoice_get_by_id(db, entity_id)
    if not obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Invoice not found")
    return obj


def get_invoice_details(db: Session, entity_id: str):
    obj = repository.invoice_get_with_details(db, entity_id)
    if not obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Invoice not found")
    return obj


def list_invoices(db: Session, limit: int, offset: int, status_filter: Optional[str] = None, patient_id: Optional[str] = None, appointment_id: Optional[str] = None):
    filters = {}
    if status_filter is not None:
        filters["status"] = status_filter
    if patient_id is not None:
        filters["patient_id"] = patient_id
    if appointment_id is not None:
        filters["appointment_id"] = appointment_id
    items = repository.invoice_list_all(db, limit=limit, offset=offset, **filters)
    total = repository.invoice_count_all(db, **filters)
    return {"items": items, "total": total, "limit": limit, "offset": offset}


def update_invoice(db: Session, entity_id: str, data: InvoiceUpdate):
    obj = repository.invoice_get_by_id(db, entity_id)
    if not obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Invoice not found")
    
    update_data = data.model_dump(exclude_unset=True)
    if "patient_id" in update_data:
        patient = patient_repo.get_by_id(db, update_data["patient_id"])
        if not patient:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Referenced patient does not exist")
    
    if "appointment_id" in update_data and update_data["appointment_id"] is not None:
        appt = appointment_repo.get_by_id(db, update_data["appointment_id"])
        if not appt:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Referenced appointment does not exist")
    
    try:
        updated_obj = repository.invoice_update(db, entity_id, update_data)
        db.commit()
        db.refresh(updated_obj)
        return updated_obj
    except IntegrityError as exc:
        db.rollback()
        msg = str(exc.orig).lower() if exc.orig else ""
        if "unique" in msg or "duplicate" in msg:
            raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Invoice with this invoice_number already exists")
        if "foreign key" in msg:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Referenced resource does not exist")
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Data integrity error")
    except HTTPException:
        db.rollback()
        raise
    except Exception:
        db.rollback()
        raise


def delete_invoice(db: Session, entity_id: str):
    obj = repository.invoice_get_by_id(db, entity_id)
    if not obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Invoice not found")
    
    try:
        repository.invoice_delete(db, entity_id)
        db.commit()
        return {"message": "Invoice deleted successfully"}
    except IntegrityError as exc:
        db.rollback()
        msg = str(exc.orig).lower() if exc.orig else ""
        if "foreign key" in msg:
            raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Cannot delete invoice as it is referenced by payments")
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Data integrity error")
    except HTTPException:
        db.rollback()
        raise
    except Exception:
        db.rollback()
        raise


def issue_invoice(db: Session, entity_id: str, data: IssueInvoiceRequest):
    obj = repository.invoice_get_by_id(db, entity_id)
    if not obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Invoice not found")
    
    if obj.status != InvoiceStatus.DRAFT:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Only draft invoices can be issued")
    
    try:
        updated_obj = repository.invoice_update(db, entity_id, {"status": InvoiceStatus.ISSUED})
        db.commit()
        db.refresh(updated_obj)
        return updated_obj
    except HTTPException:
        db.rollback()
        raise
    except Exception:
        db.rollback()
        raise


# InvoiceLineItem handlers
def create_invoicelineitem(db: Session, data: InvoicelineitemCreate):
    invoice = repository.invoice_get_by_id(db, data.invoice_id)
    if not invoice:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Referenced invoice does not exist")
    
    if data.service_id:
        service = repository.get_by_id(db, data.service_id)
        if not service:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Referenced service does not exist")
    
    try:
        obj = repository.lineitem_create(db, data.model_dump())
        db.commit()
        db.refresh(obj)
        return obj
    except IntegrityError as exc:
        db.rollback()
        msg = str(exc.orig).lower() if exc.orig else ""
        if "foreign key" in msg:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Referenced resource does not exist")
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Data integrity error")
    except HTTPException:
        db.rollback()
        raise
    except Exception:
        db.rollback()
        raise


def get_invoicelineitem(db: Session, entity_id: str):
    obj = repository.lineitem_get_by_id(db, entity_id)
    if not obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Invoice line item not found")
    return obj


def list_invoicelineitems(db: Session, limit: int, offset: int, invoice_id: Optional[str] = None, service_id: Optional[str] = None):
    filters = {}
    if invoice_id is not None:
        filters["invoice_id"] = invoice_id
    if service_id is not None:
        filters["service_id"] = service_id
    items = repository.lineitem_list_all(db, limit=limit, offset=offset, **filters)
    total = repository.lineitem_count_all(db, **filters)
    return {"items": items, "total": total, "limit": limit, "offset": offset}


def update_invoicelineitem(db: Session, entity_id: str, data: InvoicelineitemUpdate):
    obj = repository.lineitem_get_by_id(db, entity_id)
    if not obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Invoice line item not found")
    
    update_data = data.model_dump(exclude_unset=True)
    if "invoice_id" in update_data:
        invoice = repository.invoice_get_by_id(db, update_data["invoice_id"])
        if not invoice:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Referenced invoice does not exist")
    
    if "service_id" in update_data and update_data["service_id"] is not None:
        service = repository.get_by_id(db, update_data["service_id"])
        if not service:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Referenced service does not exist")
    
    try:
        updated_obj = repository.lineitem_update(db, entity_id, update_data)
        db.commit()
        db.refresh(updated_obj)
        return updated_obj
    except IntegrityError as exc:
        db.rollback()
        msg = str(exc.orig).lower() if exc.orig else ""
        if "foreign key" in msg:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Referenced resource does not exist")
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Data integrity error")
    except HTTPException:
        db.rollback()
        raise
    except Exception:
        db.rollback()
        raise


def delete_invoicelineitem(db: Session, entity_id: str):
    obj = repository.lineitem_get_by_id(db, entity_id)
    if not obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Invoice line item not found")
    
    try:
        repository.lineitem_delete(db, entity_id)
        db.commit()
        return {"message": "Invoice line item deleted successfully"}
    except HTTPException:
        db.rollback()
        raise
    except Exception:
        db.rollback()
        raise


# Payment handlers
def create_payment(db: Session, data: PaymentCreate):
    invoice = repository.invoice_get_by_id(db, data.invoice_id)
    if not invoice:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Referenced invoice does not exist")
    
    patient = patient_repo.get_by_id(db, data.patient_id)
    if not patient:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Referenced patient does not exist")
    
    if data.amount > invoice.balance_due:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Payment amount cannot exceed invoice balance due")
    
    try:
        obj = repository.payment_create(db, data.model_dump())
        db.commit()
        db.refresh(obj)
        return obj
    except IntegrityError as exc:
        db.rollback()
        msg = str(exc.orig).lower() if exc.orig else ""
        if "foreign key" in msg:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Referenced resource does not exist")
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Data integrity error")
    except HTTPException:
        db.rollback()
        raise
    except Exception:
        db.rollback()
        raise


def get_payment(db: Session, entity_id: str):
    obj = repository.payment_get_by_id(db, entity_id)
    if not obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Payment not found")
    return obj


def list_payments(db: Session, limit: int, offset: int, invoice_id: Optional[str] = None, patient_id: Optional[str] = None, payment_method: Optional[str] = None):
    filters = {}
    if invoice_id is not None:
        filters["invoice_id"] = invoice_id
    if patient_id is not None:
        filters["patient_id"] = patient_id
    if payment_method is not None:
        filters["payment_method"] = payment_method
    items = repository.payment_list_all(db, limit=limit, offset=offset, **filters)
    total = repository.payment_count_all(db, **filters)
    return {"items": items, "total": total, "limit": limit, "offset": offset}


def update_payment(db: Session, entity_id: str, data: PaymentUpdate):
    obj = repository.payment_get_by_id(db, entity_id)
    if not obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Payment not found")
    
    update_data = data.model_dump(exclude_unset=True)
    if "invoice_id" in update_data:
        invoice = repository.invoice_get_by_id(db, update_data["invoice_id"])
        if not invoice:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Referenced invoice does not exist")
    
    if "patient_id" in update_data:
        patient = patient_repo.get_by_id(db, update_data["patient_id"])
        if not patient:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Referenced patient does not exist")
    
    try:
        updated_obj = repository.payment_update(db, entity_id, update_data)
        db.commit()
        db.refresh(updated_obj)
        return updated_obj
    except IntegrityError as exc:
        db.rollback()
        msg = str(exc.orig).lower() if exc.orig else ""
        if "foreign key" in msg:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Referenced resource does not exist")
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Data integrity error")
    except HTTPException:
        db.rollback()
        raise
    except Exception:
        db.rollback()
        raise


def delete_payment(db: Session, entity_id: str):
    obj = repository.payment_get_by_id(db, entity_id)
    if not obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Payment not found")
    
    try:
        repository.payment_delete(db, entity_id)
        db.commit()
        return {"message": "Payment deleted successfully"}
    except HTTPException:
        db.rollback()
        raise
    except Exception:
        db.rollback()
        raise


def process_payment(db: Session, data: ProcessPaymentRequest):
    invoice = repository.invoice_get_by_id(db, data.invoice_id)
    if not invoice:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Referenced invoice does not exist")
    
    patient = patient_repo.get_by_id(db, data.patient_id)
    if not patient:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Referenced patient does not exist")
    
    if data.amount > invoice.balance_due:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Payment amount cannot exceed invoice balance due")
    
    try:
        payment_data = {
            "invoice_id": data.invoice_id,
            "patient_id": data.patient_id,
            "payment_date": utc_now(),
            "amount": data.amount,
            "payment_method": data.payment_method,
            "transaction_reference": data.transaction_reference,
            "notes": data.notes,
            "processed_by_user_id": data.processed_by_user_id,
        }
        payment = repository.payment_create(db, payment_data)
        db.flush()
        
        new_amount_paid = invoice.amount_paid + data.amount
        new_balance_due = invoice.total_amount - new_amount_paid
        
        new_status = invoice.status
        if new_balance_due == Decimal("0.00"):
            new_status = InvoiceStatus.PAID
        elif new_amount_paid > Decimal("0.00") and new_balance_due > Decimal("0.00"):
            new_status = InvoiceStatus.PARTIALLY_PAID
        
        invoice_update_data = {
            "amount_paid": new_amount_paid,
            "balance_due": new_balance_due,
            "status": new_status,
        }
        repository.invoice_update(db, invoice.id, invoice_update_data)
        
        db.commit()
        db.refresh(payment)
        db.refresh(invoice)
        return payment
    except IntegrityError as exc:
        db.rollback()
        msg = str(exc.orig).lower() if exc.orig else ""
        if "foreign key" in msg:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Referenced resource does not exist")
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Data integrity error")
    except HTTPException:
        db.rollback()
        raise
    except Exception:
        db.rollback()
        raise