"""Phase 4 loans: schedule generation (amortizing), activation posting,
installment payments and lender-schedule verification."""
import uuid
from calendar import monthrange
from datetime import date, datetime
from decimal import Decimal
from sqlalchemy.orm import Session
from app.models.finance import Account, JournalEntry
from app.models.accounting import CashbookMovement, Loan, LoanSchedule
from app.services.accounting import AccountingError, PostingService, d
from app.services.transaction_postings import resolve_cash_account, _next_movement_number


def _add_months(start: date, months: int) -> date:
    month_index = start.month - 1 + months
    year = start.year + month_index // 12
    month = month_index % 12 + 1
    last_day = monthrange(year, month)[1]
    return date(year, month, min(start.day, last_day))


def _next_loan_number(db: Session) -> str:
    year = datetime.now().year
    last = db.query(Loan).filter(
        Loan.loan_number.like(f"LN-{year}-%")
    ).order_by(Loan.loan_number.desc()).first()
    seq = (int(last.loan_number.split("-")[-1]) + 1) if last else 1
    return f"LN-{year}-{seq:05d}"


def _rate_per_period(loan: Loan) -> Decimal:
    nominal = Decimal(str(loan.interest_rate_nominal or 0))
    if loan.interest_rate_period == "year":
        return nominal / Decimal("12")
    return nominal


def _rebuild_schedule(db: Session, loan: Loan) -> None:
    for old in db.query(LoanSchedule).filter(LoanSchedule.loan_id == loan.id).all():
        db.delete(old)
    db.flush()
    n = loan.tenure_months
    principal = d(loan.principal)
    rate = _rate_per_period(loan)
    if rate > 0:
        factor = (Decimal("1") + rate) ** Decimal(n)
        installment = d(principal * rate * factor / (factor - 1))
    else:
        installment = d(principal / Decimal(n))
    balance = principal
    total_interest = Decimal("0")
    for i in range(1, n + 1):
        interest = d(balance * rate)
        principal_part = balance if i == n else d(installment - interest)
        if principal_part < 0:
            principal_part = Decimal("0")
        if principal_part > balance:
            principal_part = balance
        total_interest = d(total_interest + interest)
        db.add(LoanSchedule(
            loan_id=loan.id,
            installment_no=i,
            due_date=_add_months(loan.start_date, i),
            principal_due=principal_part,
            interest_due=interest,
            total_due=d(principal_part + interest),
            status="pending",
        ))
        balance = d(balance - principal_part)
    loan.total_interest = float(total_interest)
    loan.installment_amount = float(d((principal + total_interest) / Decimal(n)))
    db.flush()


def create_loan(db: Session, *, lender_name: str, lender_type: str, principal: float,
                interest_rate_nominal: float, interest_rate_period: str, start_date: date,
                tenure_months: int, repayment_frequency: str,
                branch_id: str | None = None, account_id: str | None = None,
                liability_account_id: str | None = None,
                interest_expense_account_id: str | None = None,
                notes: str | None = None, created_by: str) -> Loan:
    if d(principal) <= 0:
        raise AccountingError("Loan principal must be positive")
    if tenure_months <= 0:
        raise AccountingError("Loan tenure must be positive")
    loan = Loan(
        loan_number=_next_loan_number(db),
        lender_name=lender_name,
        lender_type=lender_type or "bank",
        principal=principal,
        interest_rate_nominal=interest_rate_nominal or 0,
        interest_rate_period=interest_rate_period or "year",
        start_date=start_date,
        tenure_months=tenure_months,
        repayment_frequency=repayment_frequency or "monthly",
        status="draft",
        branch_id=branch_id,
        account_id=account_id,
        liability_account_id=liability_account_id,
        interest_expense_account_id=interest_expense_account_id,
        notes=notes,
        created_by=created_by,
    )
    db.add(loan)
    db.flush()
    _rebuild_schedule(db, loan)
    return loan


def _loan_cash_account(db: Session, loan: Loan, method: str | None) -> Account:
    if loan.account_id:
        acct = db.query(Account).filter(Account.id == loan.account_id).first()
        if acct:
            return acct
    return resolve_cash_account(db, loan.branch_id, method or "bank")


def _loan_liability_account(db: Session, loan: Loan) -> Account:
    if loan.liability_account_id:
        acct = db.query(Account).filter(Account.id == loan.liability_account_id).first()
        if acct:
            return acct
    acct = db.query(Account).filter(Account.code == "2030").first()
    if not acct:
        raise AccountingError("COA account 2030 (Loans Payable) is missing")
    return acct


def _loan_interest_account(db: Session, loan: Loan) -> Account:
    if loan.interest_expense_account_id:
        acct = db.query(Account).filter(Account.id == loan.interest_expense_account_id).first()
        if acct:
            return acct
    acct = db.query(Account).filter(Account.code == "6150").first()
    if not acct:
        raise AccountingError("COA account 6150 (Bank Charges & Interest) is missing")
    return acct


def activate_loan(db: Session, loan_id: str, user_id: str,
                  method: str | None = None) -> dict:
    loan = db.query(Loan).filter(Loan.id == loan_id).first()
    if not loan:
        raise AccountingError("Loan not found")
    if loan.status != "draft":
        raise AccountingError("Loan is not in draft state")
    amount = d(loan.principal)
    cash_acct = _loan_cash_account(db, loan, method)
    liability = _loan_liability_account(db, loan)
    existing = db.query(JournalEntry).filter(
        JournalEntry.source == "loan",
        JournalEntry.reference_type == "loan",
        JournalEntry.reference_id == loan.id,
        JournalEntry.is_reversal == False,  # noqa: E712
    ).first()
    if existing and existing.status == "posted":
        raise AccountingError("Loan is already activated")
    entry = PostingService(db).post_event(
        source="loan", reference_type="loan", reference_id=loan.id,
        entry_date=loan.start_date, branch_id=loan.branch_id,
        lines=[
            {"account_id": cash_acct.id, "debit": amount},
            {"account_id": liability.id, "credit": amount},
        ],
        description=f"Loan {loan.loan_number} - {loan.lender_name}",
        created_by=user_id,
    )
    db.add(CashbookMovement(
        movement_number=_next_movement_number(db),
        type="receipt",
        account_id=cash_acct.id,
        amount=amount,
        movement_date=loan.start_date,
        branch_id=loan.branch_id,
        journal_entry_id=entry.id,
        reference_type="loan",
        reference_id=loan.id,
        notes=f"Loan {loan.loan_number} - {loan.lender_name}",
        created_by=user_id,
    ))
    loan.status = "active"
    db.flush()
    return {"loan": loan, "entry": entry}


def pay_loan_installments(db: Session, loan_id: str, user_id: str, *,
                          installment_ids: list[str], pay_date: date,
                          method: str = "cash", branch_id: str | None = None) -> dict:
    loan = db.query(Loan).filter(Loan.id == loan_id).first()
    if not loan:
        raise AccountingError("Loan not found")
    if loan.status != "active":
        raise AccountingError("Loan is not active")
    cash_acct = resolve_cash_account(db, branch_id or loan.branch_id, method)
    liability = _loan_liability_account(db, loan)
    interest_acct = _loan_interest_account(db, loan)
    schedules = db.query(LoanSchedule).filter(
        LoanSchedule.loan_id == loan.id,
        LoanSchedule.id.in_(installment_ids),
    ).order_by(LoanSchedule.installment_no).all()
    if len(schedules) != len(set(installment_ids)):
        raise AccountingError("One or more installment ids do not belong to this loan")
    if not schedules:
        raise AccountingError("No installments selected")
    already_paid = [s for s in schedules if s.status == "paid"]
    if already_paid:
        raise AccountingError("Selected installments are already paid")

    entries = []
    total = Decimal("0")
    for s in schedules:
        principal = d(s.principal_due)
        interest = d(s.interest_due)
        due_amount = d(s.total_due)
        entry = PostingService(db).post_event(
            source="loan", reference_type="loan_payment", reference_id=s.id,
            entry_date=pay_date, branch_id=branch_id or loan.branch_id,
            lines=[
                {"account_id": liability.id, "debit": principal},
                {"account_id": interest_acct.id, "debit": interest},
                {"account_id": cash_acct.id, "credit": due_amount},
            ],
            description=f"Loan {loan.loan_number} installment {s.installment_no}",
            created_by=user_id,
        )
        s.principal_paid = principal
        s.interest_paid = interest
        s.paid_date = pay_date
        s.status = "paid"
        s.payment_id = str(uuid.uuid4())
        entries.append(entry)
        total += due_amount
        db.add(CashbookMovement(
            movement_number=_next_movement_number(db),
            type="payment",
            account_id=cash_acct.id,
            amount=due_amount,
            movement_date=pay_date,
            branch_id=branch_id or loan.branch_id,
            journal_entry_id=entry.id,
            reference_type="loan_payment",
            reference_id=s.id,
            notes=f"Loan {loan.loan_number} installment {s.installment_no}",
            created_by=user_id,
        ))
    db.flush()  # apply in-memory status changes before the open-count query
    open_count = db.query(LoanSchedule).filter(
        LoanSchedule.loan_id == loan.id, LoanSchedule.status == "pending"
    ).count()
    if open_count == 0:
        loan.status = "closed"
    db.flush()
    return {"entries": entries, "paid_amount": float(d(total)), "remaining_installments": open_count}


def verify_loan(db: Session, loan_id: str) -> dict:
    loan = db.query(Loan).filter(Loan.id == loan_id).first()
    if not loan:
        raise AccountingError("Loan not found")
    schedules = db.query(LoanSchedule).filter(
        LoanSchedule.loan_id == loan.id
    ).order_by(LoanSchedule.installment_no).all()
    total_principal = d(sum((d(s.principal_due) for s in schedules), Decimal("0")))
    total_interest = d(sum((d(s.interest_due) for s in schedules), Decimal("0")))
    expected_interest = d(loan.total_interest)
    expected_total = d(Decimal(str(loan.principal)) + expected_interest)
    actual_total = d(total_principal + total_interest)
    expected_installment = d(expected_total / Decimal(max(loan.tenure_months, 1)))
    checks = [
        {"code": "count", "label": "Number of installments",
         "expected": loan.tenure_months, "actual": len(schedules),
         "ok": len(schedules) == loan.tenure_months},
        {"code": "principal", "label": "Principal matches loan",
         "expected": float(d(loan.principal)), "actual": float(total_principal),
         "ok": total_principal == d(loan.principal)},
        {"code": "interest", "label": "Total interest matches loan",
         "expected": float(expected_interest), "actual": float(total_interest),
         "ok": total_interest == expected_interest},
        {"code": "total", "label": "Repayment total = principal + interest",
         "expected": float(expected_total), "actual": float(actual_total),
         "ok": actual_total == expected_total},
        {"code": "installment", "label": "Installment amount matches schedule",
         "expected": float(expected_installment), "actual": float(loan.installment_amount),
         "ok": d(loan.installment_amount or 0) == expected_installment},
    ]
    return {
        "loan_id": loan.id,
        "loan_number": loan.loan_number,
        "valid": all(c["ok"] for c in checks),
        "summary": {
            "principal": float(d(loan.principal)),
            "total_interest": float(total_interest),
            "total_repayment": float(actual_total),
            "installments": len(schedules),
        },
        "checks": checks,
    }


def loan_detail(db: Session, loan: Loan) -> dict:
    schedules = db.query(LoanSchedule).filter(
        LoanSchedule.loan_id == loan.id
    ).order_by(LoanSchedule.installment_no).all()
    pending = [s for s in schedules if s.status == "pending"]
    remaining_principal = d(sum(
        (d(s.principal_due) - d(s.principal_paid) for s in pending), Decimal("0")
    ))
    total_paid = d(sum(
        (d(s.principal_paid) + d(s.interest_paid) for s in schedules), Decimal("0")
    ))
    overdue = [s for s in schedules if s.status == "pending" and s.due_date < date.today()]
    return {
        "id": loan.id,
        "loan_number": loan.loan_number,
        "lender_name": loan.lender_name,
        "lender_type": loan.lender_type,
        "principal": float(d(loan.principal)),
        "interest_rate_nominal": float(loan.interest_rate_nominal or 0),
        "interest_rate_period": loan.interest_rate_period,
        "start_date": loan.start_date.isoformat(),
        "tenure_months": loan.tenure_months,
        "repayment_frequency": loan.repayment_frequency,
        "installment_amount": float(d(loan.installment_amount or 0)),
        "total_interest": float(d(loan.total_interest or 0)),
        "total_paid": float(total_paid),
        "remaining_principal": float(remaining_principal),
        "status": loan.status,
        "branch_id": loan.branch_id,
        "account_id": loan.account_id,
        "notes": loan.notes,
        "created_at": loan.created_at.isoformat() if loan.created_at else None,
        "overdue_installments": len(overdue),
        "next_due_date": (pending[0].due_date.isoformat() if pending else None),
        "schedules": [
            {
                "id": s.id,
                "installment_no": s.installment_no,
                "due_date": s.due_date.isoformat(),
                "principal_due": float(d(s.principal_due)),
                "interest_due": float(d(s.interest_due)),
                "total_due": float(d(s.total_due)),
                "principal_paid": float(d(s.principal_paid)),
                "interest_paid": float(d(s.interest_paid)),
                "paid_date": s.paid_date.isoformat() if s.paid_date else None,
                "status": s.status,
            }
            for s in schedules
        ],
    }