"""Datos semilla de los catálogos del SAT y su sincronización idempotente.

Los catálogos viven aquí y no dentro de una migración concreta a propósito: cuando el
SAT corrige una descripción o publica una clave nueva, basta editar estas listas y
volver a correr :func:`sync_catalogs`, sin escribir una migración de esquema.

Las tablas se describen con ``sa.Table`` ligeros sobre un ``MetaData`` propio (no con
los modelos ORM) para que la migración pueda importar este módulo sin acoplarse a la
definición ORM, que sigue evolucionando.
"""

import sqlalchemy as sa

_metadata = sa.MetaData()


def _catalog_table(name: str, *extra_columns: sa.Column) -> sa.Table:
    """Tabla mínima de catálogo: las columnas que toca el upsert, nada más."""
    return sa.Table(
        name,
        _metadata,
        sa.Column("id", sa.Integer, primary_key=True),
        sa.Column("code", sa.String, nullable=False),
        sa.Column("description", sa.String),
        sa.Column("is_active", sa.Boolean),
        *extra_columns,
        schema="sat",
    )


tax_regimes_table = _catalog_table(
    "tax_regimes",
    sa.Column("applies_to_individual", sa.Boolean),
    sa.Column("applies_to_legal_entity", sa.Boolean),
)
taxes_table = _catalog_table(
    "taxes",
    sa.Column("is_withholding", sa.Boolean),
    sa.Column("is_transferred", sa.Boolean),
    sa.Column("is_local", sa.Boolean),
)
payment_forms_table = _catalog_table("payment_forms")
units_of_measure_table = _catalog_table(
    "units_of_measure",
    sa.Column("name", sa.String),
    sa.Column("symbol", sa.String),
)
products_services_table = _catalog_table("products_services")
voucher_types_table = _catalog_table("voucher_types")
payment_methods_table = _catalog_table("payment_methods")
tax_objects_table = _catalog_table("tax_objects")
cfdi_uses_table = _catalog_table("cfdi_uses")


# ---------------------------------------------------------------------------
# c_RegimenFiscal (CFDI 4.0)
# ---------------------------------------------------------------------------

def _regime(code: str, description: str, individual: bool, legal_entity: bool) -> dict:
    return {
        "code": code,
        "description": description,
        "applies_to_individual": individual,
        "applies_to_legal_entity": legal_entity,
        "is_active": True,
    }


TAX_REGIMES: list[dict] = [
    _regime("601", "General de Ley Personas Morales", False, True),
    _regime("603", "Personas Morales con Fines no Lucrativos", False, True),
    _regime("605", "Sueldos y Salarios e Ingresos Asimilados a Salarios", True, False),
    _regime("606", "Arrendamiento", True, False),
    _regime("607", "Régimen de Enajenación o Adquisición de Bienes", True, False),
    _regime("608", "Demás ingresos", True, False),
    _regime("610", "Residentes en el Extranjero sin Establecimiento Permanente en México", True, True),
    _regime("611", "Ingresos por Dividendos (socios y accionistas)", True, False),
    _regime("612", "Personas Físicas con Actividades Empresariales y Profesionales", True, False),
    _regime("614", "Ingresos por intereses", True, False),
    _regime("615", "Régimen de los ingresos por obtención de premios", True, False),
    _regime("616", "Sin obligaciones fiscales", True, False),
    _regime("620", "Sociedades Cooperativas de Producción que optan por diferir sus ingresos", False, True),
    _regime("621", "Incorporación Fiscal", True, False),
    _regime("622", "Actividades Agrícolas, Ganaderas, Silvícolas y Pesqueras", False, True),
    _regime("623", "Opcional para Grupos de Sociedades", False, True),
    _regime("624", "Coordinados", False, True),
    _regime("625", "Régimen de las Actividades Empresariales con ingresos a través de Plataformas Tecnológicas", True, False),
    _regime("626", "Régimen Simplificado de Confianza", True, True),
]


# ---------------------------------------------------------------------------
# c_Impuesto
# ---------------------------------------------------------------------------
# is_local queda en false para los tres: los impuestos locales (ISH y similares)
# se declaran en el complemento "Impuestos Locales" con claves que no pertenecen
# a c_Impuesto. No se siembran registros locales inventados.

TAXES: list[dict] = [
    {"code": "001", "description": "ISR", "is_withholding": True, "is_transferred": False, "is_local": False, "is_active": True},
    {"code": "002", "description": "IVA", "is_withholding": True, "is_transferred": True, "is_local": False, "is_active": True},
    {"code": "003", "description": "IEPS", "is_withholding": True, "is_transferred": True, "is_local": False, "is_active": True},
]


# ---------------------------------------------------------------------------
# c_FormaPago
# ---------------------------------------------------------------------------

PAYMENT_FORMS: list[dict] = [
    {"code": code, "description": description, "is_active": True}
    for code, description in [
        ("01", "Efectivo"),
        ("02", "Cheque nominativo"),
        ("03", "Transferencia electrónica de fondos"),
        ("04", "Tarjeta de crédito"),
        ("05", "Monedero electrónico"),
        ("06", "Dinero electrónico"),
        ("08", "Vales de despensa"),
        ("12", "Dación en pago"),
        ("13", "Pago por subrogación"),
        ("14", "Pago por consignación"),
        ("15", "Condonación"),
        ("17", "Compensación"),
        ("23", "Novación"),
        ("24", "Confusión"),
        ("25", "Remisión de deuda"),
        ("26", "Prescripción o caducidad"),
        ("27", "A satisfacción del acreedor"),
        ("28", "Tarjeta de débito"),
        ("29", "Tarjeta de servicios"),
        ("30", "Aplicación de anticipos"),
        ("31", "Intermediario pagos"),
        ("99", "Por definir"),
    ]
]


# ---------------------------------------------------------------------------
# c_TipoDeComprobante
# ---------------------------------------------------------------------------

VOUCHER_TYPES: list[dict] = [
    {"code": code, "description": description, "is_active": True}
    for code, description in [
        ("I", "Ingreso"),
        ("E", "Egreso"),
        ("T", "Traslado"),
        ("N", "Nómina"),
        ("P", "Pago"),
    ]
]


# ---------------------------------------------------------------------------
# c_MetodoPago
# ---------------------------------------------------------------------------

PAYMENT_METHODS: list[dict] = [
    {"code": "PUE", "description": "Pago en una sola exhibición", "is_active": True},
    {"code": "PPD", "description": "Pago en parcialidades o diferido", "is_active": True},
]


# ---------------------------------------------------------------------------
# c_ObjetoImp
# ---------------------------------------------------------------------------
# Versiones posteriores del catálogo incorporan las claves 05–07; no se siembran
# hasta que el área Fiscal confirme la versión vigente (ver PENDIENTE DECISIÓN).

TAX_OBJECTS: list[dict] = [
    {"code": "01", "description": "No objeto de impuesto", "is_active": True},
    {"code": "02", "description": "Sí objeto de impuesto", "is_active": True},
    {"code": "03", "description": "Sí objeto del impuesto y no obligado al desglose", "is_active": True},
    {"code": "04", "description": "Sí objeto del impuesto y no causa impuesto", "is_active": True},
]


# ---------------------------------------------------------------------------
# c_ClaveUnidad — subset operativo
# ---------------------------------------------------------------------------
# description queda en NULL: es la nota larga del catálogo, que aquí no aporta.

UNITS_OF_MEASURE: list[dict] = [
    {"code": code, "name": name, "symbol": symbol, "description": None, "is_active": True}
    for code, name, symbol in [
        ("H87", "Pieza", "pz"),
        ("E48", "Unidad de servicio", None),
        ("ACT", "Actividad", None),
        ("C62", "Uno", None),
        ("KGM", "Kilogramo", "kg"),
        ("TNE", "Tonelada métrica", "t"),
        ("GRM", "Gramo", "g"),
        ("LTR", "Litro", "l"),
        ("MTR", "Metro", "m"),
        ("MTK", "Metro cuadrado", "m²"),
        ("MTQ", "Metro cúbico", "m³"),
        ("KMT", "Kilómetro", "km"),
        ("CMT", "Centímetro", "cm"),
        ("DAY", "Día", "d"),
        ("HUR", "Hora", "h"),
        ("MON", "Mes", None),
        ("XBX", "Caja", None),
        ("XPK", "Paquete", None),
        ("XPX", "Paleta / tarima", None),
        ("XLT", "Lote", None),
        ("E51", "Trabajo", None),
    ]
]


# ---------------------------------------------------------------------------
# c_ClaveProdServ — subset de logística
# ---------------------------------------------------------------------------
# Subset inicial de c_ClaveProdServ para agente de carga — pendiente validación con
# área Fiscal antes de producción. El catálogo completo son ~52,000 claves; aquí solo
# se siembran las del giro. Si falta una clave para un caso de uso, se documenta como
# PENDIENTE DECISIÓN: no se deduce ni se inventa.

PRODUCTS_SERVICES: list[dict] = [
    {"code": code, "description": description, "is_active": True}
    for code, description in [
        ("78101500", "Transporte de carga por carretera"),
        ("78101600", "Transporte de carga marítimo"),
        ("78101700", "Transporte de carga por ferrocarril"),
        ("78101800", "Transporte de carga aérea"),
        ("78102200", "Servicios postales de paqueteo y courrier"),
        ("78121600", "Embalaje"),
        ("78131600", "Almacenaje"),
        ("78141500", "Servicios de planificación logística"),
        ("78141600", "Servicios de expedición de fletes"),
        ("84131500", "Seguros de vida, salud y accidentes / seguros de carga"),
        ("80101500", "Servicios de consultoría de negocios y administración corporativa"),
    ]
]


# ---------------------------------------------------------------------------
# c_UsoCFDI
# ---------------------------------------------------------------------------
# Catálogo del uso que el receptor da al comprobante. Se siembran clave y
# descripción; **no** se cargan las banderas de persona física/moral ni la
# compatibilidad por régimen fiscal, porque esa matriz cambia entre versiones del
# catálogo y equivocarla provoca rechazos al timbrar.
#
# Pendiente validación con área Fiscal antes de producción, igual que el subset de
# c_ClaveProdServ.

CFDI_USES: list[dict] = [
    {"code": code, "description": description, "is_active": True}
    for code, description in [
        ("G01", "Adquisición de mercancías"),
        ("G02", "Devoluciones, descuentos o bonificaciones"),
        ("G03", "Gastos en general"),
        ("I01", "Construcciones"),
        ("I02", "Mobiliario y equipo de oficina por inversiones"),
        ("I03", "Equipo de transporte"),
        ("I04", "Equipo de cómputo y accesorios"),
        ("I05", "Dados, troqueles, moldes, matrices y herramental"),
        ("I06", "Comunicaciones telefónicas"),
        ("I07", "Comunicaciones satelitales"),
        ("I08", "Otra maquinaria y equipo"),
        ("D01", "Honorarios médicos, dentales y gastos hospitalarios"),
        ("D02", "Gastos médicos por incapacidad o discapacidad"),
        ("D03", "Gastos funerales"),
        ("D04", "Donativos"),
        ("D05", "Intereses reales efectivamente pagados por créditos hipotecarios (casa habitación)"),
        ("D06", "Aportaciones voluntarias al SAR"),
        ("D07", "Primas por seguros de gastos médicos"),
        ("D08", "Gastos de transportación escolar obligatoria"),
        ("D09", "Depósitos en cuentas para el ahorro, primas que tengan como base planes de pensiones"),
        ("D10", "Pagos por servicios educativos (colegiaturas)"),
        ("S01", "Sin efectos fiscales"),
        ("CP01", "Pagos"),
        ("CN01", "Nómina"),
    ]
]


# Orden estable de sincronización: (tabla, filas).
CATALOGS: list[tuple[sa.Table, list[dict]]] = [
    (tax_regimes_table, TAX_REGIMES),
    (taxes_table, TAXES),
    (payment_forms_table, PAYMENT_FORMS),
    (units_of_measure_table, UNITS_OF_MEASURE),
    (products_services_table, PRODUCTS_SERVICES),
    (voucher_types_table, VOUCHER_TYPES),
    (payment_methods_table, PAYMENT_METHODS),
    (tax_objects_table, TAX_OBJECTS),
    (cfdi_uses_table, CFDI_USES),
]


def sync_catalogs(connection) -> dict[str, int]:
    """Sincroniza los catálogos del SAT contra la base, de forma idempotente.

    Inserta las claves que faltan y actualiza descripción y banderas de las que ya
    existen. **Nunca borra**: una clave retirada por el SAT se desactiva a mano para
    no romper los CFDI históricos que la referencian.

    Devuelve un resumen ``{"sat.tabla": filas_insertadas}`` útil para la bitácora de
    la migración.

    Los catálogos cuya tabla todavía no existe se omiten: al correr el historial de
    migraciones desde cero, una migración antigua invoca esta misma función cuando los
    catálogos agregados después aún no se han creado. Cada uno se siembra en la
    migración que lo crea.

    Se usa contra el ``connection`` que da ``op.get_bind()`` en Alembic, o contra la
    conexión de una sesión en pruebas.
    """
    inspector = sa.inspect(connection)
    # La inspección no aplica el schema_translate_map (las pruebas mapean sat -> None
    # sobre SQLite), así que se resuelve el schema efectivo a mano.
    schema_map = connection.get_execution_options().get("schema_translate_map") or {}

    inserted: dict[str, int] = {}
    for table, rows in CATALOGS:
        effective_schema = schema_map.get(table.schema, table.schema)
        if not inspector.has_table(table.name, schema=effective_schema):
            continue
        key = f"sat.{table.name}"
        inserted[key] = 0
        for row in rows:
            existing = connection.execute(
                sa.select(table.c.id).where(table.c.code == row["code"])
            ).scalar()
            values = {k: v for k, v in row.items() if k != "code"}
            if existing is None:
                connection.execute(table.insert().values(code=row["code"], **values))
                inserted[key] += 1
            else:
                connection.execute(
                    table.update().where(table.c.id == existing).values(**values)
                )
    return inserted
