"""Impuestos de las partidas de la factura.

El CFDI exige el desglose **por partida**, pero la factura ya captura un porcentaje global de
impuesto. Duplicar la captura sería trabajo doble y una fuente de incoherencias, así que el
traslado de IVA se **deriva** de ``invoices.tax_rate`` y se recalcula cuando cambia el importe
o el porcentaje.

La derivación no pisa lo capturado a mano: en cuanto alguien ajusta los impuestos de una
partida (una retención, una tasa distinta, un exento), esa partida deja de recalcularse sola.
Es la diferencia entre un valor por defecto útil y un automatismo que borra trabajo ajeno.
"""

from decimal import ROUND_HALF_UP, Decimal

from fastapi import HTTPException, status
from sqlalchemy.orm import Session

from ..catalogs.models import Tax, TaxObject
from .models import Invoice, InvoiceItem, InvoiceItemTax

# c_ObjetoImp que obligan al desglose de impuestos en el comprobante.
_OBJETO_CON_DESGLOSE = {"02"}
# c_Impuesto del IVA.
_IVA = "002"


def _cents(value: Decimal) -> Decimal:
    return value.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)


def _item_taxes(db: Session, item_id: int) -> list[InvoiceItemTax]:
    return (
        db.query(InvoiceItemTax)
        .filter(InvoiceItemTax.invoice_item_id == item_id, InvoiceItemTax.deleted_at.is_(None))
        .order_by(InvoiceItemTax.id)
        .all()
    )


def _tax_object_code(db: Session, item: InvoiceItem) -> str:
    if not item.tax_object_id:
        return ""
    row = db.query(TaxObject).filter(TaxObject.id == item.tax_object_id).first()
    return row.code if row else ""


def sync_item_taxes(db: Session, item: InvoiceItem, invoice: Invoice) -> None:
    """Deja el traslado de IVA de la partida al día con el ``tax_rate`` de la factura.

    No hace nada si:
    - la partida no es objeto de impuesto con desglose (``ObjetoImp`` distinto de 02), o
    - ya hay impuestos que no son el traslado de IVA derivado — señal de captura manual.
    """
    if _tax_object_code(db, item) not in _OBJETO_CON_DESGLOSE:
        # Si dejó de ser objeto de impuesto, se retira el traslado derivado: un CFDI con
        # ObjetoImp 01 y nodo de impuestos es motivo de rechazo.
        for t in _item_taxes(db, item.id):
            db.delete(t)
        return

    iva = db.query(Tax).filter(Tax.code == _IVA).first()
    if not iva:
        return  # sin catálogo no hay nada que derivar; la validación del timbrado lo reportará

    existentes = _item_taxes(db, item.id)
    manuales = [t for t in existentes if t.is_withholding or t.tax_id != iva.id]
    if manuales:
        return  # hay captura manual: no se toca

    rate = (Decimal(invoice.tax_rate or 0) / Decimal(100)).quantize(Decimal("0.000001"))
    base = _cents(Decimal(item.quantity or 0) * Decimal(item.unit_amount or 0))
    amount = _cents(base * rate)

    traslado = next((t for t in existentes if t.tax_id == iva.id and not t.is_withholding), None)
    if rate == 0:
        # Tasa 0 no es lo mismo que exento, pero con el % en cero lo que hay es una factura sin
        # IVA capturado: se retira el traslado en vez de declarar 0.00 y que el SAT lo cuestione.
        if traslado:
            db.delete(traslado)
        return

    if traslado is None:
        traslado = InvoiceItemTax(
            invoice_item_id=item.id,
            tax_id=iva.id,
            is_withholding=False,
            tenant_id=item.tenant_id,
            company_id=item.company_id,
        )
        db.add(traslado)
    traslado.rate = rate
    traslado.amount = amount


def sync_invoice_taxes(db: Session, invoice: Invoice) -> None:
    """Recalcula el IVA derivado de todas las partidas. Se llama al cambiar ``tax_rate``."""
    items = (
        db.query(InvoiceItem)
        .filter(InvoiceItem.invoice_id == invoice.id, InvoiceItem.deleted_at.is_(None))
        .all()
    )
    for item in items:
        sync_item_taxes(db, item, invoice)


# --------------------------------------------------------------------------------------
# Ajuste manual
# --------------------------------------------------------------------------------------
def list_item_taxes(
    db: Session, item_id: int, tenant_id: int, company_id: int
) -> list[InvoiceItemTax]:
    _get_item(db, item_id, tenant_id, company_id)
    return _item_taxes(db, item_id)


def _get_item(db: Session, item_id: int, tenant_id: int, company_id: int) -> InvoiceItem:
    item = (
        db.query(InvoiceItem)
        .filter(
            InvoiceItem.id == item_id,
            InvoiceItem.tenant_id == tenant_id,
            InvoiceItem.company_id == company_id,
            InvoiceItem.deleted_at.is_(None),
        )
        .first()
    )
    if not item:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Partida no encontrada")
    return item


def set_item_tax(
    db: Session,
    item_id: int,
    tax_id: int,
    rate: Decimal,
    is_withholding: bool,
    tenant_id: int,
    company_id: int,
) -> InvoiceItemTax:
    """Alta o ajuste de un impuesto de la partida. El importe se calcula de la base y la tasa."""
    item = _get_item(db, item_id, tenant_id, company_id)
    tax = db.query(Tax).filter(Tax.id == tax_id).first()
    if not tax:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            detail="El impuesto indicado no existe en el catálogo del SAT",
        )
    if is_withholding and not tax.is_withholding:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            detail=f"El impuesto {tax.code} ({tax.description}) no puede retenerse",
        )
    if not is_withholding and not tax.is_transferred:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            detail=f"El impuesto {tax.code} ({tax.description}) no puede trasladarse",
        )

    base = _cents(Decimal(item.quantity or 0) * Decimal(item.unit_amount or 0))
    obj = (
        db.query(InvoiceItemTax)
        .filter(
            InvoiceItemTax.invoice_item_id == item_id,
            InvoiceItemTax.tax_id == tax_id,
            InvoiceItemTax.is_withholding == is_withholding,
            InvoiceItemTax.deleted_at.is_(None),
        )
        .first()
    )
    if obj is None:
        obj = InvoiceItemTax(
            invoice_item_id=item_id,
            tax_id=tax_id,
            is_withholding=is_withholding,
            tenant_id=tenant_id,
            company_id=company_id,
        )
        db.add(obj)
    obj.rate = Decimal(rate).quantize(Decimal("0.000001"))
    obj.amount = _cents(base * Decimal(rate))
    db.commit()
    db.refresh(obj)
    return obj


def delete_item_tax(db: Session, tax_row_id: int, tenant_id: int, company_id: int) -> None:
    obj = (
        db.query(InvoiceItemTax)
        .filter(
            InvoiceItemTax.id == tax_row_id,
            InvoiceItemTax.tenant_id == tenant_id,
            InvoiceItemTax.company_id == company_id,
            InvoiceItemTax.deleted_at.is_(None),
        )
        .first()
    )
    if not obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Impuesto no encontrado")
    db.delete(obj)
    db.commit()
