from __future__ import annotations

import base64
import json
import logging
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional

from cryptography.hazmat.primitives import padding as crypto_padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from sqlalchemy import or_
from sqlalchemy.orm import Session

from core.config import settings
from core.storage_s3 import get_object_bytes, object_exists

from api.v1.modules.a76.customs_brokers import models as cb_models

from .models import Doda, DodaContainer, DodaAmericanPedimento, DodaPedimento
from .alta_log_models import DodaAltaLog
from .payload_normalizer import (
    normalize_aduana_despacho,
    normalize_aduana_seccion,
    normalize_caat,
    normalize_doda_pedimento_row,
    normalize_fast_id,
    normalize_id_transporte,
    normalize_numero_gafete,
    normalize_patente,
    normalize_tipo_operacion,
)

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Response schemas (inline para no añadir dependencias externas)
# ---------------------------------------------------------------------------

@dataclass
class ElegibilidadReason:
    field: str
    message: str
    solution: str = ""


@dataclass
class ElegibilidadResponse:
    can_alta: bool
    reasons: List[ElegibilidadReason] = field(default_factory=list)


# ---------------------------------------------------------------------------
# FIEL encryption (mismo esquema AES-256-CBC que COVE / expediente_archivos)
# ---------------------------------------------------------------------------

def _encrypt_fiel(raw_fiel: str) -> str:
    normalized = (raw_fiel or "").strip()
    if not normalized:
        return ""
    key_bytes = (settings.COVE_FIEL_HASH_KEY or "").encode("utf-8")
    iv_bytes = (settings.COVE_FIEL_HASH_IV or "").encode("utf-8")
    if not key_bytes or not iv_bytes:
        return normalized
    key32 = key_bytes[:32].ljust(32, b"\0")
    iv16 = iv_bytes[:16].ljust(16, b"\0")
    padder = crypto_padding.PKCS7(algorithms.AES.block_size).padder()
    padded = padder.update(normalized.encode("utf-8")) + padder.finalize()
    cipher = Cipher(algorithms.AES(key32), modes.CBC(iv16))
    enc = cipher.encryptor()
    encrypted = enc.update(padded) + enc.finalize()
    return base64.b64encode(encrypted).decode("ascii")


# ---------------------------------------------------------------------------
# Main service
# ---------------------------------------------------------------------------

class DodaAltaService:
    """
    Servicio de dominio para construir el payload de alta DODA y verificar
    elegibilidad antes de enviarlo al servicio externo.
    """

    def __init__(self, db: Session) -> None:
        self.db = db

    # ------------------------------------------------------------------
    # Broker resolution
    # ------------------------------------------------------------------

    def _resolve_broker(
        self,
        responsible_key: Optional[str],
        company_id: int,
        tenant_id: int,
    ) -> Optional[cb_models.CustomsBroker]:
        """
        Resuelve el agente aduanal a partir de Doda.responsible (ClaveAA del legacy).
        Prioridad: broker_key exacto → license exacto.
        """
        normalized = (responsible_key or "").strip()
        if not normalized:
            return None

        brokers = (
            self.db.query(cb_models.CustomsBroker)
            .filter(
                or_(
                    cb_models.CustomsBroker.broker_key == normalized,
                    cb_models.CustomsBroker.license == normalized,
                ),
                cb_models.CustomsBroker.company_id == company_id,
                cb_models.CustomsBroker.tenant_id == tenant_id,
                cb_models.CustomsBroker.deleted_at.is_(None),
            )
            .order_by(cb_models.CustomsBroker.id.desc())
            .all()
        )
        if not brokers:
            return None

        exact = next(
            (b for b in brokers if (b.broker_key or "").strip() == normalized),
            None,
        )
        return exact or brokers[0]

    # ------------------------------------------------------------------
    # configuracion_vu DODA
    # ------------------------------------------------------------------

    def _build_configuracion_vu_doda(
        self,
        broker: cb_models.CustomsBroker,
        errors: List[ElegibilidadReason],
    ) -> Optional[Dict[str, Any]]:
        """
        Construye configuracion_vu usando los campos DODA del CustomsBrokerVU:
          doda_certificate_path, doda_key_path, doda_fiel_access_key.
        """
        vu = broker.vu if broker else None

        if not vu:
            errors.append(ElegibilidadReason(
                field="vu",
                message="El agente aduanal no tiene configuración VU.",
                solution="Configura la sección VU/DODA del agente aduanal.",
            ))
            return None

        doda_cert_path = (getattr(vu, "doda_certificate_path", None) or "").strip()
        doda_key_path = (getattr(vu, "doda_key_path", None) or "").strip()
        doda_fiel = (getattr(vu, "doda_fiel_access_key", None) or "").strip()

        if not doda_cert_path or not doda_key_path:
            errors.append(ElegibilidadReason(
                field="vu.doda_certificate_path",
                message="Faltan rutas de certificado o llave DODA en la configuración VU del agente.",
                solution="Sube el .cer y .key DODA en la pestaña DODA del agente aduanal.",
            ))
            return None

        if not doda_fiel:
            errors.append(ElegibilidadReason(
                field="vu.doda_fiel_access_key",
                message="La clave FIEL DODA no está configurada en la VU del agente.",
                solution="Captura la clave FIEL DODA en la configuración VU del agente aduanal.",
            ))
            return None

        cer_b64: Optional[str] = None
        key_b64: Optional[str] = None
        try:
            if not object_exists(doda_cert_path):
                errors.append(ElegibilidadReason(
                    field="vu.doda_certificate_path",
                    message="El certificado DODA no existe en el almacenamiento.",
                    solution="Vuelve a subir el .cer DODA en la configuración VU del agente.",
                ))
            else:
                cer_b64 = base64.b64encode(get_object_bytes(doda_cert_path)).decode("ascii")

            if not object_exists(doda_key_path):
                errors.append(ElegibilidadReason(
                    field="vu.doda_key_path",
                    message="La llave DODA no existe en el almacenamiento.",
                    solution="Vuelve a subir el .key DODA en la configuración VU del agente.",
                ))
            else:
                key_b64 = base64.b64encode(get_object_bytes(doda_key_path)).decode("ascii")
        except Exception:
            logger.exception("Error leyendo certificados DODA desde S3")
            errors.append(ElegibilidadReason(
                field="vu",
                message="Error leyendo certificados DODA desde el almacenamiento.",
                solution="Verifica la configuración de S3/MinIO y las rutas de los certificados.",
            ))
            return None

        if errors:
            return None

        rfc_ciec = (
            (getattr(vu, "query_tax_id", None) or "").strip()
            or (getattr(broker, "tax_id", None) or "").strip()
        )

        clave_fiel = _encrypt_fiel(doda_fiel)

        return {
            "rfc_ciec": rfc_ciec,
            "archivo_cer_base64": cer_b64 or "",
            "archivo_key_base64": key_b64 or "",
            "clave_fiel": clave_fiel,
        }

    def _attach_user_email(
        self, configuracion_vu: Dict[str, Any], user_email: str
    ) -> Dict[str, Any]:
        """Agrega el email del usuario autenticado al bloque configuracion_vu."""
        configuracion_vu["email"] = (user_email or "").strip()
        return configuracion_vu

    # ------------------------------------------------------------------
    # Payload builders for child collections
    # ------------------------------------------------------------------

    def _build_contenedores(
        self, containers: List[DodaContainer]
    ) -> List[Dict[str, Any]]:
        result = []
        for c in containers:
            candados = []
            for seal in (c.seals_detail or []):
                if seal.seal_value:
                    candados.append({"candado": seal.seal_value})
            # Fallback: si no hay filas en seals_detail pero hay string legado en seals
            if not candados and c.seals:
                for raw in c.seals.split(","):
                    val = raw.strip()
                    if val:
                        candados.append({"candado": val})
            val = (c.container_value or "").strip()
            result.append({
                "valor_contenedor": val,
                "candados": candados,
            })
        return result

    def _build_pedimentos_americanos(
        self, american_pedimentos: List[DodaAmericanPedimento]
    ) -> List[Dict[str, Any]]:
        return [
            {
                "tipo_pedimento_americano": p.american_pedimento_type or "",
                "valor_pedimento_americano": p.american_pedimento_value or "",
            }
            for p in american_pedimentos
        ]

    def _build_pedimentos(
        self, pedimentos_detail: List[DodaPedimento]
    ) -> List[Dict[str, Any]]:
        out: List[Dict[str, Any]] = []
        for p in pedimentos_detail:
            row = normalize_doda_pedimento_row(
                document=p.document,
                authorization_patent=p.authorization_patent,
                shipment=p.shipment,
                cove=p.cove,
                umc=p.umc,
                dta_niu=p.dta_niu,
                pedimento_type=p.pedimento_type,
                effective=p.effective_amount_usd,
                diff=p.difference_amount_usd,
            )
            out.append(row)
        return out

    # ------------------------------------------------------------------
    # Elegibilidad (validaciones del legacy Clarion activas)
    # ------------------------------------------------------------------

    def check_elegibilidad(
        self,
        doda_id: int,
        tenant_id: int,
        company_id: int,
        variant: str = "doda",
        user_email: Optional[str] = None,
    ) -> ElegibilidadResponse:
        """
        Verifica si el DODA cumple los requisitos para enviar el alta.
        Porta las validaciones activas del código Clarion legacy.
        """
        reasons: List[ElegibilidadReason] = []

        # --- Email del usuario autenticado ---
        if not (user_email or "").strip():
            reasons.append(ElegibilidadReason(
                field="user_email",
                message="El usuario no tiene correo electrónico registrado.",
                solution="Configura un correo electrónico en tu perfil de Keycloak.",
            ))

        doda = self.db.query(Doda).filter(
            Doda.id == doda_id,
            Doda.tenant_id == tenant_id,
            Doda.company_id == company_id,
        ).first()

        if not doda:
            reasons.append(ElegibilidadReason(
                field="doda_id",
                message=f"DODA {doda_id} no encontrado.",
            ))
            return ElegibilidadResponse(can_alta=False, reasons=reasons)

        # --- Campos obligatorios (ramas activas del legacy) ---
        if not (doda.responsible or "").strip():
            reasons.append(ElegibilidadReason(
                field="responsible",
                message="El campo Responsable se encuentra vacío.",
                solution="Captura la clave del agente aduanal responsable.",
            ))

        if not (doda.dispatch_customs or "").strip():
            reasons.append(ElegibilidadReason(
                field="dispatch_customs",
                message="El campo Aduana de Despacho se encuentra vacío.",
                solution="Captura la clave de la aduana de despacho.",
            ))

        if not (doda.customs_sections or "").strip():
            reasons.append(ElegibilidadReason(
                field="customs_sections",
                message="El campo Aduana Sección (E/S) se encuentra vacío.",
                solution="Captura la sección aduanera.",
            ))

        if not (doda.operation_type or "").strip():
            reasons.append(ElegibilidadReason(
                field="operation_type",
                message="El campo Tipo de Operación se encuentra vacío.",
                solution="Selecciona el tipo de operación.",
            ))

        if not (doda.caat or "").strip():
            reasons.append(ElegibilidadReason(
                field="caat",
                message="El campo CAAT se encuentra vacío.",
                solution="Captura el código CAAT del transportista.",
            ))

        if not (doda.transport_identification or "").strip():
            reasons.append(ElegibilidadReason(
                field="transport_identification",
                message="El campo Identificación de Transporte se encuentra vacío.",
                solution="Captura el número de identificación del transporte.",
            ))

        if not (doda.patent or "").strip():
            reasons.append(ElegibilidadReason(
                field="patent",
                message="El campo Patente se encuentra vacío.",
                solution="La patente se llena automáticamente al seleccionar el responsable.",
            ))

        # --- Gafete único: obligatorio si variant=doda ---
        if variant.lower() == "doda":
            if not (doda.unique_badge_number or "").strip():
                reasons.append(ElegibilidadReason(
                    field="unique_badge_number",
                    message="El campo Número de Gafete Único es obligatorio para el Alta DODA.",
                    solution="Captura el número de gafete único del conductor.",
                ))

        # --- Contenedores máximo 4 (legacy: IF SQL2:C2 = 4 THEN MESSAGE) ---
        containers = doda.containers or []
        if len(containers) > 4:
            reasons.append(ElegibilidadReason(
                field="containers",
                message=f"El DODA tiene {len(containers)} contenedores. El máximo permitido es 4.",
                solution="Elimina los contenedores sobrantes antes de enviar el alta.",
            ))

        # --- Precintos: máximo 8 en todo el DODA (legacy gDoda_Contenedores_Candados) ---
        seal_count = 0
        for c in containers:
            details = getattr(c, "seals_detail", None) or []
            if details:
                seal_count += len(details)
            else:
                legacy = (getattr(c, "seals", None) or "").strip()
                if legacy:
                    seal_count += len([s for s in legacy.split(",") if s.strip()])
        if seal_count > 8:
            reasons.append(ElegibilidadReason(
                field="containers",
                message=f"El DODA tiene {seal_count} precintos. El máximo permitido es 8.",
                solution="Elimina precintos hasta quedar en 8 o menos.",
            ))

        # --- Pedimentos americanos: tipo obligatorio y rango según operación (legacy, salvo PITA) ---
        clearance = getattr(doda, "customs_clearance", None)
        if clearance != 1:
            op = (doda.operation_type or "").strip().upper()
            if op in ("I", "1"):
                allowed_tipo = {"1", "2", "3", "4", "5"}
            elif op in ("E", "2"):
                allowed_tipo = {"6", "7", "8"}
            else:
                allowed_tipo = set()
            for idx, ap in enumerate(doda.american_pedimentos or [], 1):
                tipo = (ap.american_pedimento_type or "").strip()
                if not tipo:
                    reasons.append(ElegibilidadReason(
                        field="american_pedimentos",
                        message=f"Pedimento americano (línea {idx}): el tipo es obligatorio para este tipo de despacho.",
                        solution="Captura el tipo de pedimento americano (1–5 importación, 6–8 exportación).",
                    ))
                elif allowed_tipo and tipo not in allowed_tipo:
                    reasons.append(ElegibilidadReason(
                        field="american_pedimentos",
                        message=(
                            f"Pedimento americano (línea {idx}): el tipo '{tipo}' no corresponde al tipo de operación."
                        ),
                        solution="Corrige el tipo según importación (1–5) o exportación (6–8).",
                    ))

        # --- Patente vs. patente del agente aduanal (legacy: DODA:Patente <> AgeAdu:Patente) ---
        broker = self._resolve_broker(doda.responsible, company_id, tenant_id)

        if (doda.responsible or "").strip() and not broker:
            reasons.append(ElegibilidadReason(
                field="responsible",
                message=f"La clave de Responsable '{doda.responsible}' no existe en el catálogo de agentes aduanales.",
                solution="Selecciona un agente aduanal válido del catálogo.",
            ))
        elif broker and (doda.patent or "").strip():
            broker_patent = (broker.license or "").strip()
            doda_patent = (doda.patent or "").strip()
            if broker_patent and doda_patent and doda_patent != broker_patent:
                reasons.append(ElegibilidadReason(
                    field="patent",
                    message=(
                        f"La patente declarada en el DODA '{doda_patent}' es distinta "
                        f"a la patente del responsable '{broker_patent}'."
                    ),
                    solution="Verifica o actualiza la patente del DODA para que coincida con la del agente.",
                ))

        # --- Certificados DODA en VU del agente (equivalente a RutaArchivosXMLDODA) ---
        if broker:
            vu = broker.vu
            if not vu:
                reasons.append(ElegibilidadReason(
                    field="vu",
                    message="El agente aduanal no tiene configuración VU.",
                    solution="Configura la sección VU/DODA del agente aduanal.",
                ))
            else:
                if not (getattr(vu, "doda_certificate_path", None) or "").strip():
                    reasons.append(ElegibilidadReason(
                        field="vu.doda_certificate_path",
                        message="No hay certificado DODA (.cer) configurado en la VU del agente.",
                        solution="Sube el certificado .cer DODA en la pestaña DODA del agente aduanal.",
                    ))
                if not (getattr(vu, "doda_key_path", None) or "").strip():
                    reasons.append(ElegibilidadReason(
                        field="vu.doda_key_path",
                        message="No hay llave DODA (.key) configurada en la VU del agente.",
                        solution="Sube la llave .key DODA en la pestaña DODA del agente aduanal.",
                    ))
                if not (getattr(vu, "doda_fiel_access_key", None) or "").strip():
                    reasons.append(ElegibilidadReason(
                        field="vu.doda_fiel_access_key",
                        message="La clave FIEL DODA no está configurada en la VU del agente.",
                        solution="Captura la clave FIEL DODA en la configuración VU del agente.",
                    ))

        return ElegibilidadResponse(
            can_alta=len(reasons) == 0,
            reasons=reasons,
        )

    # ------------------------------------------------------------------
    # Build full payload
    # ------------------------------------------------------------------

    def _latest_alta_log(
        self,
        doda_id: int,
        tenant_id: int,
        company_id: int,
        variant: str,
    ) -> Optional[DodaAltaLog]:
        return (
            self.db.query(DodaAltaLog)
            .filter(
                DodaAltaLog.doda_id == doda_id,
                DodaAltaLog.tenant_id == tenant_id,
                DodaAltaLog.company_id == company_id,
                DodaAltaLog.variant == variant,
                DodaAltaLog.deleted_at.is_(None),
            )
            .order_by(DodaAltaLog.id.desc())
            .first()
        )

    @staticmethod
    def _extract_numero_transaccion(log_record: DodaAltaLog) -> str:
        raw_json = (log_record.result_json or "").strip()
        if raw_json:
            try:
                parsed = json.loads(raw_json)
                for key in ("numero_transaccion", "transaction_number"):
                    value = parsed.get(key)
                    if value:
                        return str(value).strip()
            except Exception:
                logger.warning("No se pudo parsear result_json de DodaAltaLog id=%s", log_record.id)
        return ""

    def build_alta_payload(
        self,
        doda_id: int,
        tenant_id: int,
        company_id: int,
        variant: str = "doda",
        user_email: str = "",
    ) -> Dict[str, Any]:
        """
        Construye el payload completo para POST /api/v1/doda/alta.
        Lanza ValueError si hay problemas de configuración críticos.
        """
        doda = self.db.query(Doda).filter(
            Doda.id == doda_id,
            Doda.tenant_id == tenant_id,
            Doda.company_id == company_id,
        ).first()

        if not doda:
            raise ValueError(f"DODA {doda_id} no encontrado.")

        broker = self._resolve_broker(doda.responsible, company_id, tenant_id)
        if not broker:
            raise ValueError(
                f"No se encontró el agente aduanal con clave '{doda.responsible}'."
            )

        errors: List[ElegibilidadReason] = []
        configuracion_vu = self._build_configuracion_vu_doda(broker, errors)
        if errors or not configuracion_vu:
            msgs = "; ".join(r.message for r in errors)
            raise ValueError(f"Error en configuración VU DODA: {msgs}")

        self._attach_user_email(configuracion_vu, user_email)

        containers = doda.containers or []
        american_pedimentos = doda.american_pedimentos or []
        pedimentos_detail = doda.pedimentos_detail or []

        # El API externo espera "1" para DODA y "2" para PITA (ver DODARequest.despacho_aduanero)
        despacho_aduanero = "1" if variant.lower() == "doda" else "2"

        payload: Dict[str, Any] = {
            "configuracion_vu": configuracion_vu,
            "despacho_aduanero": despacho_aduanero,
            "numero_gafete_unico": normalize_numero_gafete(
                doda.unique_badge_number
            ),
            "aduana_despacho": normalize_aduana_despacho(doda.dispatch_customs),
            "aduana_seccion": normalize_aduana_seccion(doda.customs_sections),
            "patente": normalize_patente(doda.patent),
            "caat": normalize_caat(doda.caat),
            "id_transporte": normalize_id_transporte(doda.transport_identification),
            "fast_id": normalize_fast_id(doda.fast_id),
            "tipo_operacion": normalize_tipo_operacion(doda.operation_type),
            "contenedores": self._build_contenedores(containers),
            "pedimentos_americanos": self._build_pedimentos_americanos(american_pedimentos),
            "cfdi_carta_porte": {"cfdi_carta_porte": ""},
            "pedimentos": self._build_pedimentos(pedimentos_detail),
        }

        return payload

    def build_consulta_payload(
        self,
        doda_id: int,
        tenant_id: int,
        company_id: int,
        variant: str = "doda",
        user_email: str = "",
    ) -> Dict[str, Any]:
        payload = self.build_alta_payload(
            doda_id=doda_id,
            tenant_id=tenant_id,
            company_id=company_id,
            variant=variant,
            user_email=user_email,
        )
        latest_log = self._latest_alta_log(doda_id, tenant_id, company_id, variant)
        if not latest_log:
            raise ValueError(
                "No existe un alta DODA previa para construir la consulta (falta task/log)."
            )
        numero_transaccion = self._extract_numero_transaccion(latest_log)
        if not numero_transaccion:
            raise ValueError(
                "No se encontro numero_transaccion en el ultimo resultado de alta DODA."
            )
        payload["numero_transaccion"] = numero_transaccion
        return payload

    def build_eliminar_payload(
        self,
        doda_id: int,
        tenant_id: int,
        company_id: int,
        variant: str = "doda",
        user_email: str = "",
    ) -> Dict[str, Any]:
        payload = self.build_alta_payload(
            doda_id=doda_id,
            tenant_id=tenant_id,
            company_id=company_id,
            variant=variant,
            user_email=user_email,
        )
        latest_log = self._latest_alta_log(doda_id, tenant_id, company_id, variant)
        if not latest_log:
            raise ValueError(
                "No existe un alta DODA previa para construir la eliminacion."
            )
        numero_integracion = (latest_log.integration_number or "").strip()
        if not numero_integracion:
            raise ValueError(
                "No se encontro numero_integracion en el historial de alta DODA."
            )
        payload["numero_integracion"] = numero_integracion
        return payload
