"""Lógica del catálogo de conceptos de facturación.

Todas las consultas filtran por ``tenant_id``, ``company_id`` y ``deleted_at IS NULL``:
el catálogo es privado de cada empresa dentro de cada tenant.
"""

from datetime import datetime, timezone

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

from ..catalogs.models import ProductService, TaxObject, UnitOfMeasure
from .dto import ConceptCreate, ConceptUpdate
from .models import Concept


def _check_sat_refs(db: Session, data: dict) -> None:
    """Verifica que las claves del SAT referidas existan antes de guardar."""
    for field, model, msg in [
        ("product_service_id", ProductService, "La clave de producto/servicio del SAT no existe"),
        ("unit_of_measure_id", UnitOfMeasure, "La unidad de medida del SAT no existe"),
        ("tax_object_id", TaxObject, "El objeto de impuesto del SAT no existe"),
    ]:
        value = data.get(field)
        if field in data and value is not None:
            if db.query(model.id).filter(model.id == value).first() is None:
                raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=msg)


def _check_unique(
    db: Session,
    tenant_id: int,
    company_id: int,
    code: str | None,
    product_service_id: int | None,
    exclude_id: int | None = None,
) -> None:
    """Aplica en el service las mismas reglas que los índices únicos parciales.

    Sin esto el conflicto llegaría al cliente como un IntegrityError crudo; aquí se
    traduce a un 409 con mensaje en español.
    """
    base = db.query(Concept).filter(
        Concept.tenant_id == tenant_id,
        Concept.company_id == company_id,
        Concept.deleted_at.is_(None),
    )
    if exclude_id is not None:
        base = base.filter(Concept.id != exclude_id)

    if code is not None and base.filter(Concept.code == code).first() is not None:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=f"Ya existe un concepto con la clave '{code}' en esta empresa",
        )
    # Regla 1:1 — una clave ProdServ no puede repetirse entre conceptos de la empresa.
    if product_service_id is not None and base.filter(
        Concept.product_service_id == product_service_id
    ).first() is not None:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail="La clave de producto/servicio del SAT ya está asignada a otro concepto de esta empresa",
        )


def get_concepts(
    db: Session,
    tenant_id: int,
    company_id: int,
    search: str | None = None,
    active_only: bool | None = None,
    product_service_id: int | None = None,
) -> list[Concept]:
    q = db.query(Concept).filter(
        Concept.tenant_id == tenant_id,
        Concept.company_id == company_id,
        Concept.deleted_at.is_(None),
    )
    if active_only is not None:
        q = q.filter(Concept.is_active.is_(active_only))
    if product_service_id is not None:
        q = q.filter(Concept.product_service_id == product_service_id)
    if search:
        term = f"%{search.strip()}%"
        q = q.filter(or_(Concept.code.ilike(term), Concept.description.ilike(term)))
    return q.order_by(Concept.code.asc()).all()


def get_concept(db: Session, concept_id: int, tenant_id: int, company_id: int) -> Concept:
    obj = db.query(Concept).filter(
        Concept.id == concept_id,
        Concept.tenant_id == tenant_id,
        Concept.company_id == company_id,
        Concept.deleted_at.is_(None),
    ).first()
    if not obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Concepto no encontrado")
    return obj


def create_concept(
    db: Session, payload: ConceptCreate, tenant_id: int, company_id: int, user_id: str | None = None
) -> Concept:
    data = payload.model_dump()
    _check_sat_refs(db, data)
    _check_unique(db, tenant_id, company_id, data["code"], data["product_service_id"])
    obj = Concept(**data, tenant_id=tenant_id, company_id=company_id, created_by=user_id, updated_by=user_id)
    db.add(obj)
    db.commit()
    db.refresh(obj)
    return obj


def update_concept(
    db: Session,
    concept_id: int,
    payload: ConceptUpdate,
    tenant_id: int,
    company_id: int,
    user_id: str | None = None,
) -> Concept:
    obj = get_concept(db, concept_id, tenant_id, company_id)
    data = payload.model_dump(exclude_unset=True)
    _check_sat_refs(db, data)
    _check_unique(
        db,
        tenant_id,
        company_id,
        data.get("code"),
        data.get("product_service_id"),
        exclude_id=obj.id,
    )
    for field, value in data.items():
        setattr(obj, field, value)
    obj.updated_by = user_id
    db.commit()
    db.refresh(obj)
    return obj


def delete_concept(db: Session, concept_id: int, tenant_id: int, company_id: int) -> None:
    """Baja lógica: libera la clave ProdServ y el código para un concepto nuevo."""
    obj = get_concept(db, concept_id, tenant_id, company_id)
    obj.deleted_at = datetime.now(timezone.utc)
    db.commit()
