from sqlalchemy.orm import Session
from fastapi import HTTPException, status
from decimal import Decimal, ROUND_HALF_UP
from typing import Optional, List
from . import repository
from .schema import (
    LoancalculationCreate,
    LoancalculationUpdate,
    LoanCalculationInput,
    AmortizationscheduleentryCreate,
    AmortizationscheduleentryUpdate,
)
from .models import Loancalculation, Amortizationscheduleentry


def _get_or_raise(db: Session, entity_id: str, repo_module) -> Loancalculation:
    obj = repo_module.get_by_id(db, entity_id)
    if not obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Loan calculation with id {entity_id} not found")
    return obj


def create_loan_calculation(db: Session, data: LoancalculationCreate) -> Loancalculation:
    try:
        loan_data = data.model_dump()
        monthly_payment, total_interest, total_amount = _calculate_loan_metrics(
            loan_data["principal"],
            loan_data["annual_interest_rate"],
            loan_data["loan_term_months"]
        )
        loan_data["monthly_payment"] = monthly_payment
        loan_data["total_interest"] = total_interest
        loan_data["total_amount"] = total_amount
        
        obj = repository.create(db, loan_data)
        db.commit()
        db.refresh(obj)
        return obj
    except Exception:
        db.rollback()
        raise


def list_loan_calculations(db: Session, limit: int, offset: int, session_id: Optional[str] = None) -> List[Loancalculation]:
    filters = {}
    if session_id:
        filters["session_id"] = session_id
    return repository.list_all(db, limit, offset, **filters)


def get_loan_calculation(db: Session, entity_id: str) -> Loancalculation:
    return _get_or_raise(db, entity_id, repository)


def update_loan_calculation(db: Session, entity_id: str, data: LoancalculationUpdate) -> Loancalculation:
    obj = _get_or_raise(db, entity_id, repository)
    update_data = data.model_dump(exclude_unset=True)
    if not update_data:
        return obj
    
    try:
        updated = repository.update(db, entity_id, update_data)
        if not updated:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Loan calculation with id {entity_id} not found")
        db.commit()
        db.refresh(updated)
        return updated
    except Exception:
        db.rollback()
        raise


def delete_loan_calculation(db: Session, entity_id: str) -> bool:
    obj = _get_or_raise(db, entity_id, repository)
    try:
        result = repository.delete(db, entity_id)
        db.commit()
        return result
    except Exception:
        db.rollback()
        raise


def get_loan_calculation_with_schedule(db: Session, entity_id: str) -> Loancalculation:
    obj = repository.get_with_details(db, entity_id)
    if not obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Loan calculation with id {entity_id} not found")
    return obj


def calculate_loan(db: Session, data: LoanCalculationInput) -> Loancalculation:
    try:
        monthly_payment, total_interest, total_amount = _calculate_loan_metrics(
            data.principal,
            data.annual_interest_rate,
            data.loan_term_months
        )
        
        loan_data = {
            "principal": data.principal,
            "annual_interest_rate": data.annual_interest_rate,
            "loan_term_months": data.loan_term_months,
            "monthly_payment": monthly_payment,
            "total_interest": total_interest,
            "total_amount": total_amount,
            "calculation_method": "standard-amortization",
            "session_id": data.session_id,
        }
        
        loan_calc = repository.create(db, loan_data)
        db.flush()
        
        schedule_entries = _generate_amortization_schedule(
            loan_calc.id,
            data.principal,
            data.annual_interest_rate,
            data.loan_term_months,
            monthly_payment
        )
        
        repository.create_bulk_schedule_entries(db, schedule_entries)
        db.commit()
        db.refresh(loan_calc)
        return loan_calc
    except Exception:
        db.rollback()
        raise


def _calculate_loan_metrics(principal: Decimal, annual_rate: Decimal, term_months: int) -> tuple:
    principal = Decimal(str(principal))
    annual_rate = Decimal(str(annual_rate))
    
    if annual_rate == 0:
        monthly_payment = (principal / term_months).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
        total_amount = monthly_payment * term_months
        total_interest = Decimal("0.00")
    else:
        monthly_rate = (annual_rate / Decimal("100")) / Decimal("12")
        numerator = principal * monthly_rate * ((Decimal("1") + monthly_rate) ** term_months)
        denominator = ((Decimal("1") + monthly_rate) ** term_months) - Decimal("1")
        monthly_payment = (numerator / denominator).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
        total_amount = monthly_payment * term_months
        total_interest = total_amount - principal
    
    return monthly_payment, total_interest, total_amount


def _generate_amortization_schedule(
    loan_calc_id: str,
    principal: Decimal,
    annual_rate: Decimal,
    term_months: int,
    monthly_payment: Decimal
) -> List[dict]:
    principal = Decimal(str(principal))
    annual_rate = Decimal(str(annual_rate))
    monthly_payment = Decimal(str(monthly_payment))
    monthly_rate = (annual_rate / Decimal("100")) / Decimal("12") if annual_rate > 0 else Decimal("0")
    
    schedule = []
    balance = principal
    cumulative_interest = Decimal("0.00")
    cumulative_principal = Decimal("0.00")
    
    for payment_num in range(1, term_months + 1):
        beginning_balance = balance
        
        if annual_rate == 0:
            interest_portion = Decimal("0.00")
        else:
            interest_portion = (beginning_balance * monthly_rate).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
        
        if payment_num == term_months:
            principal_portion = balance
            payment_amount = principal_portion + interest_portion
            ending_balance = Decimal("0.00")
        else:
            principal_portion = monthly_payment - interest_portion
            payment_amount = monthly_payment
            ending_balance = beginning_balance - principal_portion
        
        cumulative_interest += interest_portion
        cumulative_principal += principal_portion
        
        schedule.append({
            "loan_calculation_id": loan_calc_id,
            "payment_number": payment_num,
            "payment_date": None,
            "beginning_balance": beginning_balance.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP),
            "payment_amount": payment_amount.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP),
            "principal_portion": principal_portion.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP),
            "interest_portion": interest_portion.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP),
            "ending_balance": ending_balance.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP),
            "cumulative_interest": cumulative_interest.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP),
            "cumulative_principal": cumulative_principal.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP),
        })
        
        balance = ending_balance
    
    return schedule


def create_schedule_entry(db: Session, data: AmortizationscheduleentryCreate) -> Amortizationscheduleentry:
    loan_calc = repository.get_by_id(db, data.loan_calculation_id)
    if not loan_calc:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Loan calculation with id {data.loan_calculation_id} not found")
    
    try:
        entry_data = data.model_dump()
        obj = repository.create_schedule_entry(db, entry_data)
        db.commit()
        db.refresh(obj)
        return obj
    except Exception:
        db.rollback()
        raise


def list_schedule_entries(db: Session, loan_calculation_id: str, limit: int, offset: int) -> List[Amortizationscheduleentry]:
    loan_calc = repository.get_by_id(db, loan_calculation_id)
    if not loan_calc:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Loan calculation with id {loan_calculation_id} not found")
    return repository.list_schedule_entries(db, loan_calculation_id, limit, offset)


def get_schedule_entry(db: Session, entry_id: str) -> Amortizationscheduleentry:
    obj = repository.get_schedule_entry_by_id(db, entry_id)
    if not obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Schedule entry with id {entry_id} not found")
    return obj


def update_schedule_entry(db: Session, entry_id: str, data: AmortizationscheduleentryUpdate) -> Amortizationscheduleentry:
    obj = repository.get_schedule_entry_by_id(db, entry_id)
    if not obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Schedule entry with id {entry_id} not found")
    
    update_data = data.model_dump(exclude_unset=True)
    if not update_data:
        return obj
    
    try:
        updated = repository.update_schedule_entry(db, entry_id, update_data)
        if not updated:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Schedule entry with id {entry_id} not found")
        db.commit()
        db.refresh(updated)
        return updated
    except Exception:
        db.rollback()
        raise


def delete_schedule_entry(db: Session, entry_id: str) -> bool:
    obj = repository.get_schedule_entry_by_id(db, entry_id)
    if not obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Schedule entry with id {entry_id} not found")
    
    try:
        result = repository.delete_schedule_entry(db, entry_id)
        db.commit()
        return result
    except Exception:
        db.rollback()
        raise