"""Modelos de los catálogos oficiales del SAT — schema ``sat``.

Son catálogos **globales**: los publica el SAT, valen igual para cualquier tenant y
compañía, por eso no heredan ``TenantScopedMixin``. Tampoco se borran: cuando el SAT
retira una clave, el registro se marca ``is_active = false`` para que las facturas
históricas que la usan sigan resolviendo su descripción (de ahí que se use
``BaseTimestampMixin``, sin ``deleted_at``).

La API los expone únicamente en modo lectura; el alta y la actualización pasan por
``seed_data.sync_catalogs()``.
"""

from sqlalchemy import Boolean, Integer, String, text
from sqlalchemy.orm import Mapped, mapped_column

from api.v1.common.base_models import BaseTimestampMixin
from core.database import Base


class SatCatalogMixin(BaseTimestampMixin):
    """Campos comunes a todo catálogo del SAT.

    ``code`` (la clave oficial) se declara en cada modelo porque su longitud
    cambia de catálogo en catálogo.
    """

    id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
    description: Mapped[str] = mapped_column(String(500), nullable=False)
    is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=text("true"))


class TaxRegime(Base, SatCatalogMixin):
    """``c_RegimenFiscal`` — régimen fiscal del emisor y del receptor del CFDI.

    Las banderas indican a qué tipo de persona aplica el régimen: una persona física
    no puede declararse en el 601 (General de Ley Personas Morales) y viceversa.
    """

    __tablename__ = "tax_regimes"
    __table_args__ = {"schema": "sat"}

    code: Mapped[str] = mapped_column(String(3), nullable=False, unique=True, index=True)
    applies_to_individual: Mapped[bool] = mapped_column(  # persona física
        Boolean, nullable=False, server_default=text("false")
    )
    applies_to_legal_entity: Mapped[bool] = mapped_column(  # persona moral
        Boolean, nullable=False, server_default=text("false")
    )


class Tax(Base, SatCatalogMixin):
    """``c_Impuesto`` — impuestos federales que pueden trasladarse o retenerse."""

    __tablename__ = "taxes"
    __table_args__ = {"schema": "sat"}

    code: Mapped[str] = mapped_column(String(3), nullable=False, unique=True, index=True)
    is_withholding: Mapped[bool] = mapped_column(  # puede retenerse
        Boolean, nullable=False, server_default=text("false")
    )
    is_transferred: Mapped[bool] = mapped_column(  # puede trasladarse
        Boolean, nullable=False, server_default=text("false")
    )
    # Los impuestos locales (ISH y similares) viajan en el complemento "Impuestos
    # Locales" con claves ajenas a c_Impuesto; la bandera queda disponible para
    # cuando el negocio defina ese catálogo.
    is_local: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=text("false"))


class PaymentForm(Base, SatCatalogMixin):
    """``c_FormaPago`` — con qué se pagó (efectivo, transferencia, tarjeta…)."""

    __tablename__ = "payment_forms"
    __table_args__ = {"schema": "sat"}

    code: Mapped[str] = mapped_column(String(2), nullable=False, unique=True, index=True)


class UnitOfMeasure(Base, SatCatalogMixin):
    """``c_ClaveUnidad`` — unidad de medida de la partida.

    Único catálogo que separa nombre corto y definición: ``name`` es lo que se
    muestra al capturar y ``description`` la nota larga del SAT, que puede venir
    vacía.
    """

    __tablename__ = "units_of_measure"
    __table_args__ = {"schema": "sat"}

    code: Mapped[str] = mapped_column(String(20), nullable=False, unique=True, index=True)
    name: Mapped[str] = mapped_column(String(255), nullable=False)
    symbol: Mapped[str | None] = mapped_column(String(20), nullable=True)
    # Se redeclara para permitir NULL: aquí la descripción es la nota del catálogo.
    description: Mapped[str | None] = mapped_column(String(500), nullable=True)


class ProductService(Base, SatCatalogMixin):
    """``c_ClaveProdServ`` — clave de producto o servicio de la partida."""

    __tablename__ = "products_services"
    __table_args__ = {"schema": "sat"}

    code: Mapped[str] = mapped_column(String(8), nullable=False, unique=True, index=True)


class VoucherType(Base, SatCatalogMixin):
    """``c_TipoDeComprobante`` — I ingreso, E egreso, T traslado, N nómina, P pago."""

    __tablename__ = "voucher_types"
    __table_args__ = {"schema": "sat"}

    code: Mapped[str] = mapped_column(String(1), nullable=False, unique=True, index=True)


class PaymentMethod(Base, SatCatalogMixin):
    """``c_MetodoPago`` — PUE (una sola exhibición) o PPD (parcialidades/diferido)."""

    __tablename__ = "payment_methods"
    __table_args__ = {"schema": "sat"}

    code: Mapped[str] = mapped_column(String(3), nullable=False, unique=True, index=True)


class TaxObject(Base, SatCatalogMixin):
    """``c_ObjetoImp`` — si la partida es o no objeto de impuesto."""

    __tablename__ = "tax_objects"
    __table_args__ = {"schema": "sat"}

    code: Mapped[str] = mapped_column(String(2), nullable=False, unique=True, index=True)


class CfdiUse(Base, SatCatalogMixin):
    """``c_UsoCFDI`` — uso que el receptor le dará al comprobante.

    Lo declara el receptor, no el emisor, y el SAT lo valida contra su régimen
    fiscal: por eso vive en la ficha del cliente (``crm.accounts.cfdi_use_id``).
    """

    __tablename__ = "cfdi_uses"
    __table_args__ = {"schema": "sat"}

    code: Mapped[str] = mapped_column(String(4), nullable=False, unique=True, index=True)
