from __future__ import annotations
import json
from datetime import date, datetime
from decimal import Decimal
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from pydantic import BaseModel
from app.database import get_db
from app.models.finance import Account
from app.models.accounting import CashbookMovement, BankReconciliation, AccountingPeriod
from app.models.audit import AuditLog
from app.services.auth import require_permission
from app.services.accounting import AccountingError, d
from app.services.finance_postings import deposit_funds, withdraw_funds, transfer_funds

router = APIRouter(prefix="/accounting", tags=["accounting"])

_SIGN_BY_TYPE = {
    "receipt": +1,
    "deposit": +1,
    "transfer_in": +1,
    "payment": -1,
    "withdrawal": -1,
    "transfer_out": -1,
    "adjustment": 0,
}


class DepositIn(BaseModel):
    amount: float
    date: date
    branch_id: Optional[str] = None
    to_account_id: Optional[str] = None
    notes: Optional[str] = None


class WithdrawIn(BaseModel):
    amount: float
    date: date
    branch_id: Optional[str] = None
    from_account_id: Optional[str] = None
    notes: Optional[str] = None


class TransferIn(BaseModel):
    from_account_id: str
    to_account_id: str
    amount: float
    date: date
    branch_id: Optional[str] = None
    notes: Optional[str] = None


class ReconciliationIn(BaseModel):
    account_id: str
    statement_balance: float
    as_of: Optional[date] = None
    period_id: Optional[str] = None
    lines: Optional[list] = None  # [{movement_number, type, date, amount, cleared}]


class ReconciliationResolveIn(BaseModel):
    status: str  # matched/adjusted
    note: Optional[str] = None


def _bank_book_balance(db: Session, account_id: str, as_of: date | None) -> Decimal:
    from app.models.finance import JournalEntry
    from app.models.accounting import JournalLine
    query = db.query(JournalLine).join(
        JournalEntry, JournalEntry.id == JournalLine.entry_id
    ).filter(
        JournalEntry.status == "posted", JournalLine.account_id == account_id
    )
    if as_of:
        query = query.filter(JournalEntry.entry_date <= as_of)
    total = Decimal("0")
    for line in query.all():
        total += d(line.debit) - d(line.credit)
    return d(total)


@router.get("/cashbook")
def list_cashbook(
    account_id: Optional[str] = None,
    type: Optional[str] = None,
    branch_id: Optional[str] = None,
    from_date: Optional[date] = None,
    to_date: Optional[date] = None,
    account_code: Optional[str] = None,
    db: Session = Depends(get_db),
    _=Depends(require_permission("accounting", "view")),
):
    q = db.query(CashbookMovement)
    if account_id:
        q = q.filter(CashbookMovement.account_id == account_id)
    if type:
        q = q.filter(CashbookMovement.type == type)
    if branch_id:
        q = q.filter((CashbookMovement.branch_id == branch_id) |
                     (CashbookMovement.branch_id.is_(None)))
    if from_date:
        q = q.filter(CashbookMovement.movement_date >= from_date)
    if to_date:
        q = q.filter(CashbookMovement.movement_date <= to_date)
    movements = q.order_by(CashbookMovement.movement_date,
                           CashbookMovement.movement_number).all()

    accounts = {}
    running = {}
    for m in movements:
        account = accounts.get(m.account_id)
        if account is None:
            account = db.query(Account).filter(Account.id == m.account_id).first()
            accounts[m.account_id] = account
        signed = Decimal(_SIGN_BY_TYPE.get(m.type, 0)) * d(m.amount)
        running[m.account_id] = d(running.get(m.account_id, Decimal("0")) + signed)

    rows = []
    per_account = {}
    for m in movements:
        account = accounts.get(m.account_id) or {}
        base = {
            "id": m.id,
            "movement_number": m.movement_number,
            "type": m.type,
            "amount": float(d(m.amount)),
            "sign": _SIGN_BY_TYPE.get(m.type, 0),
            "movement_date": m.movement_date.isoformat(),
            "branch_id": m.branch_id,
            "account_id": m.account_id,
            "account_code": (account.code if account else None),
            "account_name": (account.name if account else None),
            "from_account_id": m.from_account_id,
            "to_account_id": m.to_account_id,
            "journal_entry_id": m.journal_entry_id,
            "reference_type": m.reference_type,
            "reference_id": m.reference_id,
            "notes": m.notes,
            "created_by": m.created_by,
            "created_at": m.created_at.isoformat() if m.created_at else None,
        }
        rows.append(base)

    summary = []
    for account_id, balance in running.items():
        account = accounts.get(account_id)
        receipts = sum(d(x.amount) for x in movements
                       if x.account_id == account_id and x.type in ("receipt", "deposit", "transfer_in"))
        payments = sum(d(x.amount) for x in movements
                       if x.account_id == account_id and x.type in ("payment", "withdrawal", "transfer_out"))
        summary.append({
            "account_id": account_id,
            "code": account.code if account else None,
            "name": account.name if account else account_id,
            "closing_balance": float(balance),
            "total_in": float(d(receipts)),
            "total_out": float(d(payments)),
        })

    totals_in = sum((d(r["amount"]) for r in rows
                     if r["sign"] in (1,)), Decimal("0"))
    totals_out = sum((d(r["amount"]) for r in rows
                      if r["sign"] in (-1,)), Decimal("0"))
    return {
        "movements": rows,
        "accounts": summary,
        "totals": {
            "in": float(d(totals_in)),
            "out": float(d(totals_out)),
            "net": float(d(totals_in - totals_out)),
        },
    }


@router.get("/cashbook/accounts")
def cashbook_accounts(db: Session = Depends(get_db),
                      _=Depends(require_permission("accounting", "view"))):
    accounts = db.query(Account).filter(Account.is_active == True).all()  # noqa: E712
    return [
        {
            "id": a.id,
            "code": a.code,
            "name": a.name,
            "category": a.category,
            "branch_id": a.branch_id,
            "parent_id": a.parent_id,
        }
        for a in accounts
        if (a.category or "") in ("cash", "bank") or (a.code or "").startswith(("1010", "1020"))
    ]


def _movement_json(entry, movements, reference_id) -> dict:
    return {
        "journal_entry_id": entry.id,
        "entry_number": entry.entry_number,
        "reference_id": reference_id,
        "movements": [
            {"movement_number": m.movement_number, "type": m.type,
             "account_id": m.account_id, "amount": float(d(m.amount))}
            for m in movements
        ],
        "posted": True,
    }


@router.post("/cashbook/deposit", status_code=201)
def create_deposit(data: DepositIn, db: Session = Depends(get_db),
                   current_user=Depends(require_permission("accounting", "create"))):
    try:
        result = deposit_funds(db, amount=data.amount, movement_date=data.date,
                               branch_id=data.branch_id, to_account_id=data.to_account_id,
                               notes=data.notes, created_by=current_user.id)
        db.commit()
    except AccountingError as exc:
        db.rollback()
        raise HTTPException(status_code=400, detail=str(exc))
    return _movement_json(result["journal_entry"], result["movements"], result["reference_id"])


@router.post("/cashbook/withdraw", status_code=201)
def create_withdrawal(data: WithdrawIn, db: Session = Depends(get_db),
                      current_user=Depends(require_permission("accounting", "create"))):
    try:
        result = withdraw_funds(db, amount=data.amount, movement_date=data.date,
                                branch_id=data.branch_id, from_account_id=data.from_account_id,
                                notes=data.notes, created_by=current_user.id)
        db.commit()
    except AccountingError as exc:
        db.rollback()
        raise HTTPException(status_code=400, detail=str(exc))
    return _movement_json(result["journal_entry"], result["movements"], result["reference_id"])


@router.post("/cashbook/transfer", status_code=201)
def create_transfer(data: TransferIn, db: Session = Depends(get_db),
                    current_user=Depends(require_permission("accounting", "create"))):
    try:
        result = transfer_funds(db, from_account_id=data.from_account_id,
                                to_account_id=data.to_account_id, amount=data.amount,
                                movement_date=data.date, branch_id=data.branch_id,
                                notes=data.notes, created_by=current_user.id)
        db.commit()
    except AccountingError as exc:
        db.rollback()
        raise HTTPException(status_code=400, detail=str(exc))
    return _movement_json(result["journal_entry"], result["movements"], result["reference_id"])


@router.get("/bank-reconciliations")
def list_reconciliations(account_id: Optional[str] = None,
                         db: Session = Depends(get_db),
                         _=Depends(require_permission("accounting", "view"))):
    q = db.query(BankReconciliation)
    if account_id:
        q = q.filter(BankReconciliation.account_id == account_id)
    rows = []
    for rec in q.order_by(BankReconciliation.created_at.desc()).all():
        account = db.query(Account).filter(Account.id == rec.account_id).first()
        rows.append({
            "id": rec.id,
            "account_id": rec.account_id,
            "account_code": account.code if account else None,
            "account_name": account.name if account else rec.account_id,
            "period_id": rec.period_id,
            "statement_balance": float(d(rec.statement_balance)),
            "book_balance": float(d(rec.book_balance)),
            "difference": float(d(rec.difference)),
            "status": rec.status,
            "lines": json.loads(rec.lines) if rec.lines else [],
            "reconciled_at": rec.reconciled_at.isoformat() if rec.reconciled_at else None,
            "reconciled_by": rec.reconciled_by,
            "created_at": rec.created_at.isoformat() if rec.created_at else None,
        })
    return rows


@router.post("/bank-reconciliations", status_code=201)
def create_reconciliation(data: ReconciliationIn, db: Session = Depends(get_db),
                          current_user=Depends(require_permission("accounting", "create"))):
    account = db.query(Account).filter(Account.id == data.account_id).first()
    if not account:
        raise HTTPException(404, detail="Account not found")
    as_of = data.as_of or date.today()
    book = _bank_book_balance(db, data.account_id, as_of)
    statement = d(data.statement_balance)
    rec = BankReconciliation(
        account_id=data.account_id,
        period_id=data.period_id,
        statement_balance=statement,
        book_balance=book,
        difference=d(statement - book),
        status="pending",
        lines=json.dumps(data.lines or []),
    )
    db.add(rec)
    db.add(AuditLog(user_id=current_user.id, action="create_reconciliation",
                    module="accounting", record_id=rec.id,
                    new_value=f"{account.code or account.name}: {statement} vs {book}"))
    db.commit()
    db.refresh(rec)
    account = db.query(Account).filter(Account.id == rec.account_id).first()
    return {
        "id": rec.id,
        "account_id": rec.account_id,
        "account_code": account.code if account else None,
        "account_name": account.name if account else rec.account_id,
        "period_id": rec.period_id,
        "statement_balance": float(rec.statement_balance),
        "book_balance": float(rec.book_balance),
        "difference": float(rec.difference),
        "status": rec.status,
        "reconciled_at": rec.reconciled_at,
        "reconciled_by": rec.reconciled_by,
        "created_at": rec.created_at,
    }


@router.post("/bank-reconciliations/{reconcile_id}/resolve")
def resolve_reconciliation(reconcile_id: str, data: ReconciliationResolveIn,
                           db: Session = Depends(get_db),
                           current_user=Depends(require_permission("accounting", "edit"))):
    rec = db.query(BankReconciliation).filter(BankReconciliation.id == reconcile_id).first()
    if not rec:
        raise HTTPException(404, detail="Reconciliation not found")
    if data.status not in ("matched", "adjusted"):
        raise HTTPException(422, detail="status must be 'matched' or 'adjusted'")
    if data.status == "adjusted" and d(rec.difference) != 0:
        raise HTTPException(400,
                            detail="Statement and book balances differ; adjust the bank account first")
    rec.status = data.status
    rec.reconciled_at = datetime.now()
    rec.reconciled_by = current_user.id
    if data.note:
        lines = json.loads(rec.lines) if rec.lines else []
        lines.append({"note": data.note, "at": datetime.now().isoformat()})
        rec.lines = json.dumps(lines)
    db.add(AuditLog(user_id=current_user.id, action="resolve_reconciliation",
                    module="accounting", record_id=rec.id, new_value=data.status))
    db.commit()
    db.refresh(rec)
    return {"id": rec.id, "status": rec.status, "difference": float(rec.difference)}