"""Tareas Celery del carril CRM Agentes de Carga -> EFC.

- ``deliver_outbox_row`` / ``sweep_outbox``: expedientes (alta del provisional y completado).
- ``deliver_file_outbox_row`` / ``sweep_file_outbox``: archivos.
- ``sweep_expediente_gaps``: reconciliación de expedientes que nunca se encolaron.

**La trampa de RLS, que es lo que más fácil se pasa por alto.** ``core/celery_app.py`` materializa el
contexto desde los headers ``rls_tenant_id`` / ``rls_company_id``. Por tanto:

- Las tareas **por fila** se despachan siempre con esos headers.
- Los **barridos corren sin contexto de tenant**: leen los ids pendientes con una sesión sin scope y
  despachan una tarea hija por fila con sus propios headers. Si un barrido abriera una sesión con
  scope e iterara, o no vería nada o se saltaría el aislamiento.

**Sin ``autoretry_for``, ``retry_backoff`` ni ``max_retries``**: duplicarían el mecanismo de
reintento que ya está en el cliente (3 intentos con backoff lineal) y en el barrido (cada 120 s
hasta ``MAX_ATTEMPTS``).
"""
import logging

from core.celery_app import celery_app
from core.config import settings
from core.database import scoped_core_db

from . import service
from .models import STATUS_PENDING, EfcFileOutbox, EfcSyncOutbox

# ── Registro de modelos: NO son imports decorativos, no los quites ──────────────────────
# El worker de Celery NO carga la app: importa este módulo y sus dependencias, y nada más.
# SQLAlchemy resuelve las ForeignKey por NOMBRE de tabla contra su registro global, así que
# si la clase del otro extremo nunca se importó, la configuración de mappers falla con
#
#   Foreign key associated with column 'cases.account_id' could not find table 'crm.accounts'
#
# y la tarea muere con PendingRollbackError. El síntoma es cruel: la fila del outbox se
# queda en `pending` con attempts=0 y SIN last_error —porque el fallo ocurre antes de poder
# registrarlo—, así que el carril se ve encolando bien y no entrega nunca. En la app web no
# pasa: `main.py` monta todos los routers y con ellos se importan todos los modelos.
#
# El juego es el mínimo verificado con `configure_mappers()` en un proceso limpio:
#   - accounts : cierra la FK cases.account_id, que es la que rompía;
#   - documents y ops.shipments : las dos fuentes del outbox de archivos;
#   - tenants  : lo consulta el resolver de organización al entregar.
from api.v1.modules.core.tenants import models as _m_tenants  # noqa: F401
from api.v1.modules.crm.accounts import models as _m_accounts  # noqa: F401
from api.v1.modules.crm.documents import models as _m_documents  # noqa: F401
from api.v1.modules.ops.shipments import models as _m_shipments  # noqa: F401

logger = logging.getLogger(__name__)


# ── Expedientes ─────────────────────────────────────────────────────────────

@celery_app.task(name="expediente_gateway.deliver_outbox_row")
def deliver_outbox_row(outbox_id: int, tenant_id: int, company_id: int) -> None:
    with scoped_core_db(tenant_id, company_id) as db:
        row = db.query(EfcSyncOutbox).filter(EfcSyncOutbox.id == outbox_id).first()
        if row is None:
            logger.warning(
                "expediente_gateway: outbox_id=%s no encontrado (tenant=%s)", outbox_id, tenant_id
            )
            return
        service.deliver_row(db, row)


@celery_app.task(name="expediente_gateway.sweep_outbox")
def sweep_outbox(limit: int = 100) -> int:
    """Re-despacha filas pendientes de expediente. Sin contexto de tenant: cada fila lleva el suyo."""
    with scoped_core_db() as db:
        rows = (
            db.query(EfcSyncOutbox.id, EfcSyncOutbox.tenant_id, EfcSyncOutbox.company_id)
            .filter(EfcSyncOutbox.status == STATUS_PENDING)
            .order_by(EfcSyncOutbox.created_at.asc())
            .limit(limit)
            .all()
        )

    for rid, tid, cid in rows:
        deliver_outbox_row.apply_async(
            args=[rid, tid, cid],
            headers={"rls_tenant_id": str(tid), "rls_company_id": str(cid) if cid is not None else ""},
        )
    if rows:
        logger.info("expediente_gateway: sweep (expedientes) re-despachó %s filas pendientes", len(rows))
    return len(rows)


# ── Archivos ────────────────────────────────────────────────────────────────

@celery_app.task(name="expediente_gateway.deliver_file_outbox_row")
def deliver_file_outbox_row(outbox_id: int, tenant_id: int, company_id: int) -> None:
    with scoped_core_db(tenant_id, company_id) as db:
        row = db.query(EfcFileOutbox).filter(EfcFileOutbox.id == outbox_id).first()
        if row is None:
            logger.warning(
                "expediente_gateway: file outbox_id=%s no encontrado (tenant=%s)", outbox_id, tenant_id
            )
            return
        service.deliver_file_row(db, row)


@celery_app.task(name="expediente_gateway.sweep_file_outbox")
def sweep_file_outbox(limit: int = 100) -> int:
    """Re-despacha archivos pendientes (EFC o el broker caídos cuando el usuario subió el archivo)."""
    with scoped_core_db() as db:
        rows = (
            db.query(EfcFileOutbox.id, EfcFileOutbox.tenant_id, EfcFileOutbox.company_id)
            .filter(EfcFileOutbox.status == STATUS_PENDING)
            .order_by(EfcFileOutbox.created_at.asc())
            .limit(limit)
            .all()
        )
    for rid, tid, cid in rows:
        deliver_file_outbox_row.apply_async(
            args=[rid, tid, cid],
            headers={"rls_tenant_id": str(tid), "rls_company_id": str(cid) if cid is not None else ""},
        )
    if rows:
        logger.info("expediente_gateway: sweep (archivos) re-despachó %s archivos pendientes", len(rows))
    return len(rows)


# ── Reconciliación de huecos ────────────────────────────────────────────────

@celery_app.task(name="expediente_gateway.sweep_expediente_gaps")
def sweep_expediente_gaps(limit: int = 200) -> int:
    """Detecta expedientes que nunca se encolaron a EFC y los re-encola.

    No-op si la integración está apagada.
    """
    if not settings.EFC_API_URL:
        return 0
    n = 0
    with scoped_core_db() as db:
        gaps = service.find_expediente_gaps(db, limit=limit)
        for expediente in gaps:
            service.replicate_expediente_best_effort(db, expediente)
            n += 1
        if n:
            db.commit()
    if n:
        logger.info("expediente_gateway: sweep de huecos re-encoló %s expedientes", n)
    return n
