"""Outbox transaccional del carril CRM Agentes de Carga -> EFC.

DOS tablas separadas POR PROPÓSITO, igual que en el carril de referencia de Anexo22: una para los
expedientes (metadatos, JSON) y otra para los archivos (binarios que viven en MinIO y se referencian
por su ``s3_key``). Un worker de Celery las drena hacia EFC con reintentos.

**Diferencia con el original, y es necesaria:** aquí las filas se insertan en la MISMA transacción
que el expediente o el documento, porque el CRM es mono-base. En Anexo22 el outbox vivía en otra
base que el pedimento, y ese doble-commit es justamente lo que obligó a inventar el barrido de
huecos. Aquí el barrido se conserva —cubre lo creado antes de activar la integración y cualquier
crash— pero deja de ser el parche de una ventana estructural.
"""
from datetime import datetime
from typing import Optional

from sqlalchemy import JSON, Boolean, DateTime, ForeignKey, Index, Integer, String, Text, text
from sqlalchemy.orm import Mapped, mapped_column

from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base

# Tipo de trabajo (columna kind) del outbox de EXPEDIENTES.
KIND_EXPEDIENTE = "expediente"
KIND_COMPLETAR = "completar"

# Tipos del outbox de ARCHIVOS (efc_file_outbox).
FILE_KIND_DOCUMENTO = "documento"

# Tablas de origen posibles de un archivo. El CRM tiene DOS tablas de documentos con secuencias
# independientes, así que `source_id` por sí solo es ambiguo: crm.documents.id = 5 y
# ops.shipment_documents.id = 5 coexisten.
SOURCE_CRM_DOCUMENTS = "crm.documents"
SOURCE_OPS_SHIPMENT_DOCUMENTS = "ops.shipment_documents"
SOURCE_FIN_INVOICES = "fin.invoices"

# Estados (columna status).
STATUS_PENDING = "pending"
STATUS_SENT = "sent"
STATUS_FAILED = "failed"

# Tope de reintentos antes de marcar 'failed' (reconciliación / reintento manual).
# Heredado del carril de Anexo22. Con barridos de 120 s son ~17 minutos de insistencia antes de
# rendirse y dejar la fila visible para que una persona la reintente a mano.
MAX_ATTEMPTS = 8


class EfcSyncOutbox(Base, TenantScopedMixin, TimestampMixin):
    """Cola de metadatos hacia EFC: crear el expediente provisional y completarlo."""

    __tablename__ = "efc_sync_outbox"
    __table_args__ = (
        Index("ix_crm_efc_sync_outbox_status", "status"),
        Index("ix_crm_efc_sync_outbox_kind_status", "kind", "status"),
        {"schema": "crm"},
    )

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)

    kind: Mapped[str] = mapped_column(String(20), nullable=False)

    # Datos para construir el request a EFC (folio, storage_token, tenant slug, company, y la data
    # aduanera si el kind es 'completar').
    payload: Mapped[dict] = mapped_column(JSON, nullable=False)

    # id local del expediente (crm.cases.id) que originó la fila.
    expediente_ref: Mapped[Optional[int]] = mapped_column(Integer, nullable=True, index=True)

    # Ciclo de vida.
    status: Mapped[str] = mapped_column(String(10), nullable=False, server_default=text(f"'{STATUS_PENDING}'"))
    attempts: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("0"))
    last_error: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
    sent_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)

    # Acuse de EFC al confirmar (trazabilidad).
    efc_pedimento_id: Mapped[Optional[str]] = mapped_column(String(36), nullable=True)


class EfcFileOutbox(Base, TenantScopedMixin, TimestampMixin):
    """Cola de ARCHIVOS hacia EFC.

    El binario vive en el MinIO del CRM (durable); esta fila referencia su ``s3_key`` y el expediente
    destino. El worker lo sube a EFC y, con ``delete_local`` (corte directo), BORRA la copia local al
    confirmar la entrega.

    ``delete_local`` **es el mecanismo de «EFC es la fuente única»**: "solo EFC" es el estado FINAL
    (eventual), no el inmediato. Entre que el usuario sube el archivo y que EFC lo confirma, la copia
    local es lo único que hay, y borrarla antes perdería el archivo si la entrega fallara.

    ``source_table`` es un añadido necesario sobre el original de Anexo22, que solo llevaba
    ``source_id``. El CRM tiene dos tablas de documentos con secuencias independientes, así que un
    entero solo es ambiguo entre ellas. Es el mismo problema que Anexo22 resolvió con su mapa por
    ``kind``, y su comentario dice qué pasa si se ignora: un UPDATE con el id de otra tabla **vacía la
    columna de un documento ajeno** que tuviera ese mismo entero — daño en el dato de otro, sin un
    solo error visible. Un ``(kind, source_table)`` que no esté en el mapa **no toca nada**, en vez
    de caer por omisión.
    """

    __tablename__ = "efc_file_outbox"
    __table_args__ = (
        Index("ix_crm_efc_file_outbox_status", "status"),
        Index("ix_crm_efc_file_outbox_kind_status", "kind", "status"),
        Index("ix_crm_efc_file_outbox_source", "source_table", "source_id"),
        {"schema": "crm"},
    )

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    kind: Mapped[str] = mapped_column(String(30), nullable=False)

    # Objeto en MinIO a subir + metadata para el upload a EFC.
    s3_key: Mapped[str] = mapped_column(String(1024), nullable=False)
    file_name: Mapped[str] = mapped_column(String(255), nullable=False)
    content_type: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
    efc_tipo: Mapped[str] = mapped_column(String(40), nullable=False)  # tipo de documento en EFC

    # Origen: la pareja (tabla, id) desambigua entre las dos secuencias de documentos del CRM.
    source_table: Mapped[str] = mapped_column(String(30), nullable=False)
    source_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
    # El handle autoritativo que viaja a EFC y garantiza la idempotencia del lado de allá.
    crm_document_ref: Mapped[Optional[str]] = mapped_column(String(64), nullable=True)

    expediente_ref: Mapped[int] = mapped_column(
        Integer, ForeignKey("crm.cases.id"), nullable=False, index=True
    )
    delete_local: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=text("true"))

    # Ciclo de vida.
    status: Mapped[str] = mapped_column(String(10), nullable=False, server_default=text(f"'{STATUS_PENDING}'"))
    attempts: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("0"))
    last_error: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
    sent_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
    efc_document_id: Mapped[Optional[str]] = mapped_column(String(36), nullable=True)
