from __future__ import annotations

import base64
import logging
from datetime import datetime
from typing import 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.celery_app import celery_app
from core.config import settings
from core.database import CoreSessionLocal
from core.exceptions import ErrorCollector, ValidationException
from core.storage_s3 import get_object_bytes, object_exists

from api.v1.modules.a76.customs_brokers import models as cb_models
from api.v1.modules.a76.general_catalogs.company.models import Company

from .dto import (
    DigitalizacionErrorDetail,
    DigitalizacionResult,
    DigitalizacionTaskDetailResponse,
    ExpedienteArchivoCreateDTO,
    ExpedienteArchivoListResponse,
    ExpedienteArchivoResponseDTO,
    ExpedienteArchivoUpdateDTO,
)
from .models import ExpedienteArchivo

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# CRUD service
# ---------------------------------------------------------------------------

class ExpedienteArchivoService:

    @staticmethod
    def _get_task_record_metadata(task_id: str) -> dict:
        db = CoreSessionLocal()
        try:
            record = (
                db.query(ExpedienteArchivo)
                .filter(
                    ExpedienteArchivo.task_id == task_id,
                    ExpedienteArchivo.deleted_at.is_(None),
                )
                .order_by(ExpedienteArchivo.id.desc())
                .first()
            )
            if not record:
                return {"external_task_id": None}
            return {
                "external_task_id": record.external_task_id,
                "db_status": record.status,
                "e_document": record.e_document,
                "num_operacion": record.num_operacion,
                "nombre_archivo": record.nombre_archivo,
            }
        except Exception:
            logger.exception("No se pudo obtener metadata del expediente para task_id=%s", task_id)
            return {"external_task_id": None}
        finally:
            db.close()

    @staticmethod
    def list(
        db: Session,
        company_id: int,
        tenant_id: int,
        page: int = 1,
        page_size: int = 50,
        search: Optional[str] = None,
        status: Optional[str] = None,
        rfc_consulta: Optional[str] = None,
        e_document: Optional[str] = None,
    ) -> ExpedienteArchivoListResponse:
        query = (
            db.query(ExpedienteArchivo)
            .filter(
                ExpedienteArchivo.company_id == company_id,
                ExpedienteArchivo.tenant_id == tenant_id,
                ExpedienteArchivo.deleted_at.is_(None),
            )
        )
        if status:
            query = query.filter(ExpedienteArchivo.status == status)
        if rfc_consulta:
            query = query.filter(ExpedienteArchivo.rfc_consulta.ilike(f"%{rfc_consulta}%"))
        if e_document:
            query = query.filter(ExpedienteArchivo.e_document.ilike(f"%{e_document}%"))
        if search:
            like = f"%{search}%"
            query = query.filter(
                or_(
                    ExpedienteArchivo.e_document.ilike(like),
                    ExpedienteArchivo.tipo_documento.ilike(like),
                    ExpedienteArchivo.rfc_consulta.ilike(like),
                    ExpedienteArchivo.num_operacion.ilike(like),
                    ExpedienteArchivo.nombre_archivo.ilike(like),
                )
            )
        total = query.count()
        items = query.order_by(ExpedienteArchivo.id.desc()).offset((page - 1) * page_size).limit(page_size).all()
        return ExpedienteArchivoListResponse(
            items=[ExpedienteArchivoResponseDTO.model_validate(r) for r in items],
            total=total,
            page=page,
            page_size=page_size,
        )

    @staticmethod
    def get(db: Session, record_id: int, company_id: int, tenant_id: int) -> Optional[ExpedienteArchivo]:
        return (
            db.query(ExpedienteArchivo)
            .filter(
                ExpedienteArchivo.id == record_id,
                ExpedienteArchivo.company_id == company_id,
                ExpedienteArchivo.tenant_id == tenant_id,
                ExpedienteArchivo.deleted_at.is_(None),
            )
            .first()
        )

    @staticmethod
    def create(db: Session, dto: ExpedienteArchivoCreateDTO, company_id: int, tenant_id: int) -> ExpedienteArchivo:
        record = ExpedienteArchivo(
            company_id=company_id,
            tenant_id=tenant_id,
            **dto.model_dump(exclude_none=False),
        )
        db.add(record)
        db.commit()
        db.refresh(record)
        return record

    @staticmethod
    def update(
        db: Session,
        record: ExpedienteArchivo,
        dto: ExpedienteArchivoUpdateDTO,
    ) -> ExpedienteArchivo:
        for field, value in dto.model_dump(exclude_unset=True).items():
            setattr(record, field, value)
        db.commit()
        db.refresh(record)
        return record

    @staticmethod
    def delete(db: Session, record: ExpedienteArchivo) -> None:
        from datetime import datetime
        record.deleted_at = datetime.utcnow()
        db.commit()

    @staticmethod
    def get_task_status(task_id: str) -> DigitalizacionTaskDetailResponse:
        task_metadata = ExpedienteArchivoService._get_task_record_metadata(task_id)
        try:
            result = celery_app.AsyncResult(task_id)
            state = result.state or "PENDING"
            info = result.info or {}
        except Exception as exc:
            logger.exception("No se pudo consultar el estado de la tarea de digitalización task_id=%s", task_id)
            return DigitalizacionTaskDetailResponse(
                task_id=task_id,
                external_task_id=task_metadata.get("external_task_id"),
                state="FAILURE",
                status="failed",
                error="No se pudo consultar el estado de la digitalización.",
                error_type=type(exc).__name__,
                error_detail=DigitalizacionErrorDetail(
                    codigo="TASK_STATUS_ERROR",
                    descripcion=str(exc),
                    paso="Consulta de estado",
                    sugerencias=["Cierra el diálogo y vuelve a intentar la digitalización."],
                ),
            )

        if state == "SUCCESS":
            raw = result.result or {}
            return DigitalizacionTaskDetailResponse(
                task_id=task_id,
                external_task_id=task_metadata.get("external_task_id"),
                state="SUCCESS",
                status="success",
                request_id=raw.get("request_id"),
                result=DigitalizacionResult(**{k: raw.get(k) for k in DigitalizacionResult.model_fields}),
                progress=100,
                total_steps=4,
            )

        # Fallback: Celery puede tardar en propagar SUCCESS a Redis.
        # Si la DB ya tiene status=success, retornamos SUCCESS inmediatamente.
        if task_metadata.get("db_status") == "success":
            logger.info(
                "get_task_status: Celery state=%s but DB status=success — returning SUCCESS from DB task_id=%s",
                state, task_id,
            )
            return DigitalizacionTaskDetailResponse(
                task_id=task_id,
                external_task_id=task_metadata.get("external_task_id"),
                state="SUCCESS",
                status="success",
                result=DigitalizacionResult(
                    status="success",
                    message="Digitalización completada exitosamente.",
                    e_document=task_metadata.get("e_document"),
                    numero_operacion=task_metadata.get("num_operacion"),
                    nombre_archivo=task_metadata.get("nombre_archivo"),
                ),
                progress=100,
                total_steps=4,
            )

        if state in {"FAILURE", "FAILED"}:
            err = info if not isinstance(info, dict) else None
            error_detail_raw = info.get("error_detail") if isinstance(info, dict) else None
            error_text = str(err or info.get("error", "")) if isinstance(info, dict) else str(err or "")
            error_type = info.get("error_type") if isinstance(info, dict) else None
            if not error_type and isinstance(info, BaseException):
                error_type = type(info).__name__
            if error_type == "Ignore" and not error_text:
                error_text = "La digitalización no pudo completarse."
            if not error_text and isinstance(info, BaseException):
                error_text = "La digitalización no pudo completarse."
            if isinstance(info, BaseException) and not error_detail_raw:
                error_detail_raw = {
                    "codigo": "TASK_FAILED",
                    "descripcion": "La tarea terminó con error antes de completar la digitalización.",
                    "paso": "Proceso de digitalización",
                    "sugerencias": ["Revisa la configuración VU y vuelve a intentarlo."],
                }
            return DigitalizacionTaskDetailResponse(
                task_id=task_id,
                external_task_id=task_metadata.get("external_task_id"),
                state="FAILURE",
                status="failed",
                request_id=info.get("request_id") if isinstance(info, dict) else None,
                error=error_text,
                error_type=error_type,
                error_detail=DigitalizacionErrorDetail(**(error_detail_raw or {})) if error_detail_raw else None,
            )

        # PROGRESS / PENDING / STARTED
        if isinstance(info, dict):
            return DigitalizacionTaskDetailResponse(
                task_id=task_id,
                external_task_id=task_metadata.get("external_task_id"),
                state=state,
                status="processing",
                current_step=info.get("current_step") or info.get("status"),
                progress=info.get("progress") or info.get("current"),
                total_steps=info.get("total_steps") or 4,
                request_id=info.get("request_id"),
            )

        return DigitalizacionTaskDetailResponse(
            task_id=task_id,
            external_task_id=task_metadata.get("external_task_id"),
            state=state,
            status="pending",
        )


# ---------------------------------------------------------------------------
# VU config builder (mirror of factura_cove/service.py _build_configuracion_vu)
# ---------------------------------------------------------------------------

def _encrypt_fiel(raw_fiel: str) -> str:
    """AES-256-CBC + PKCS7 + base64 — mismo esquema que factura_cove."""
    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 ""
    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")


def _resolve_broker_for_vu(
    db: Session,
    company_id: int,
    tenant_id: int,
    agente_aduanal_key: str,
) -> Optional[cb_models.CustomsBroker]:
    normalized_key = (agente_aduanal_key or "").strip()
    if not normalized_key:
        return None

    brokers = (
        db.query(cb_models.CustomsBroker)
        .filter(
            or_(
                cb_models.CustomsBroker.broker_key == normalized_key,
                cb_models.CustomsBroker.license == normalized_key,
            ),
            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_broker_key = next(
        (broker for broker in brokers if (broker.broker_key or "").strip() == normalized_key),
        None,
    )
    if exact_broker_key:
        return exact_broker_key

    if len(brokers) > 1:
        logger.warning(
            "Multiple customs brokers matched agente_aduanal=%s; falling back to first license match ids=%s broker_keys=%s",
            normalized_key,
            [broker.id for broker in brokers],
            [broker.broker_key for broker in brokers],
        )

    return brokers[0]


def resolve_rfc_consulta_value(
    db: Session,
    company_id: int,
    tenant_id: int,
    agente_aduanal_key: Optional[str],
    request_rfc_consulta: Optional[str],
    record_rfc_consulta: Optional[str],
    config_vu_rfc: Optional[str],
) -> str:
    explicit_rfc = (request_rfc_consulta or "").strip().upper()
    if explicit_rfc:
        return explicit_rfc

    broker_tax_id = ""
    if agente_aduanal_key:
        broker = _resolve_broker_for_vu(db, company_id, tenant_id, agente_aduanal_key)
        broker_tax_id = (getattr(broker, "tax_id", None) or "").strip().upper() if broker else ""
        if broker_tax_id:
            return broker_tax_id

    stored_rfc = (record_rfc_consulta or "").strip().upper()
    if stored_rfc:
        return stored_rfc

    config_rfc = (config_vu_rfc or "").strip().upper()
    if config_rfc:
        return config_rfc

    raise ValidationException(
        "RFC Consulta no disponible",
        errors=[
            {
                "field": "rfc_consulta",
                "message": "No se pudo resolver el RFC Consulta desde el expediente ni desde el customs broker.",
                "code": "MISSING_RFC_CONSULTA",
                "solution": [
                    "Configura el RFC del agente aduanal en el customs broker o captura el RFC directamente en el expediente."
                ],
            }
        ],
    )


def build_configuracion_vu(
    db: Session,
    company_id: int,
    tenant_id: int,
    agente_aduanal_key: Optional[str],
    errors: ErrorCollector,
) -> Optional[dict]:
    """
    Construye el dict de configuracion_vu para el servicio externo de digitalización.
    Prioridad: CustomsBrokerVU (agente) → company.ventanilla_unica → company_fiel_certificate.
    """
    vu: Optional[cb_models.CustomsBrokerVU] = None
    if agente_aduanal_key:
        broker = _resolve_broker_for_vu(db, company_id, tenant_id, agente_aduanal_key)
        if broker:
            vu = broker.vu
    else:
        broker = (
            db.query(cb_models.CustomsBroker)
            .filter(
                cb_models.CustomsBroker.company_id == company_id,
                cb_models.CustomsBroker.tenant_id == tenant_id,
                cb_models.CustomsBroker.deleted_at.is_(None),
            )
            .first()
        )
        if broker:
            vu = broker.vu

    company = db.get(Company, company_id)
    company_vu = company.ventanilla_unica if company else None

    company_fiel_certificate = None
    if company:
        for cert in (company.digital_certificates or []):
            if (cert.certificate_type or "").strip().lower() == "fiel":
                company_fiel_certificate = cert
                break

    if not vu and not company_vu and not company_fiel_certificate:
        errors.add_error(
            field="vu",
            message="La empresa no tiene configuración VU ni certificado FIEL.",
            solution=["Configura los datos VU del agente aduanal o de la empresa y sube certificado (.cer) y llave (.key)."],
            code="MISSING_VU_CONFIGURATION",
        )
        return None

    clave_fiel_value = ""
    if vu and getattr(vu, "fiel_access_key", None):
        clave_fiel_value = _encrypt_fiel(vu.fiel_access_key or "")
    elif company_fiel_certificate:
        secret = (
            getattr(company_fiel_certificate, "access_key", None)
            or getattr(company_fiel_certificate, "password", None)
            or ""
        )
        clave_fiel_value = _encrypt_fiel(str(secret))

    hardcoded_ws_key = (
        "RZGd+CB4R6PfSrstOyN8Is9FXL9AK9NPFisyGGaEWa0vVHoVOl8v2SBcHBoGbt3T/4uHTGcsFQO3b7EonWVfugQjBooywbz74K+jM68j8/Y="
    )
    vu_ws_key = (getattr(vu, "web_service_access_key", None) or "").strip() if vu else ""
    vu_access_key_raw = (getattr(vu, "access_key", None) or "").strip() if vu else ""
    vu_access_key_encrypted = _encrypt_fiel(vu_access_key_raw) if vu_access_key_raw else ""
    company_ws_key = (getattr(company_vu, "webservice_password", None) or "").strip() if company_vu else ""
    if vu_ws_key:
        ws_key_source = "web_service_access_key"
    elif vu_access_key_encrypted:
        ws_key_source = "access_key_encrypted"
    elif company_ws_key:
        ws_key_source = "company"
    else:
        ws_key_source = "fallback"
    clave_webservice = vu_ws_key or vu_access_key_encrypted or company_ws_key or hardcoded_ws_key

    if not clave_webservice:
        errors.add_error(
            field="vu.clave_webservice",
            message="La clave de web service no está configurada en VU ni en la empresa.",
            solution=["Captura la clave de web service en la pestaña VU o completa la configuración VU de la empresa."],
            code="MISSING_VU_WS_KEY",
        )

    if not clave_fiel_value:
        errors.add_error(
            field="vu.clave_fiel",
            message="La clave FIEL no está configurada en VU ni en la empresa.",
            solution=["Captura la clave FIEL en la configuración VU del agente aduanal o en los certificados de la empresa."],
            code="MISSING_FIEL_PASSWORD",
        )

    certificate_path = (
        (getattr(vu, "certificate_path", None) or "").strip() if vu else ""
    ) or (
        (getattr(company_fiel_certificate, "cer_file_path", None) or "").strip()
        if company_fiel_certificate
        else ""
    )
    key_path = (
        (getattr(vu, "key_path", None) or "").strip() if vu else ""
    ) or (
        (getattr(company_fiel_certificate, "key_file_path", None) or "").strip()
        if company_fiel_certificate
        else ""
    )

    if not (certificate_path and key_path):
        errors.add_error(
            field="vu",
            message="No hay rutas de certificado o llave en la configuración VU.",
            solution=["Sube el certificado (.cer) y la llave (.key) en la configuración VU del agente aduanal."],
            code="MISSING_VU_CERT_KEY",
        )
        return None

    cer_b64 = None
    key_b64 = None
    try:
        if not object_exists(certificate_path):
            errors.add_error(
                field="vu.certificate_path",
                message="El certificado VU no existe en el almacenamiento.",
                solution=["Vuelve a subir el certificado en la configuración VU."],
                code="VU_CERT_NOT_FOUND",
            )
        else:
            cer_b64 = base64.b64encode(get_object_bytes(certificate_path)).decode("ascii")

        if not object_exists(key_path):
            errors.add_error(
                field="vu.key_path",
                message="La llave VU no existe en el almacenamiento.",
                solution=["Vuelve a subir la llave en la configuración VU."],
                code="VU_KEY_NOT_FOUND",
            )
        else:
            key_b64 = base64.b64encode(get_object_bytes(key_path)).decode("ascii")
    except Exception:
        logger.exception("Error leyendo certificados VU desde almacenamiento")
        errors.add_error(
            field="vu",
            message="Error leyendo certificados VU.",
            solution=["Verifica la configuración de MinIO/S3."],
            code="VU_STORAGE_ERROR",
        )

    if errors.has_errors():
        return None

    rfc_usuario_vu = (
        (getattr(vu, "query_tax_id", None) or "").strip() if vu else ""
    ) or (
        (getattr(company_vu, "query_rfc", None) or "").strip() if company_vu else ""
    )

    email = (
        (getattr(vu, "vu_email", None) or "").strip() if vu else ""
    ) or (
        (getattr(company_vu, "email", None) or "").strip() if company_vu else ""
    ) or (
        (getattr(getattr(company, "address", None), "email", None) or "").strip() if company else ""
    )

    if ws_key_source == "fallback":
        logger.warning(
            "Expediente digitalization is using fallback web service key for agente_aduanal=%s company_id=%s tenant_id=%s",
            agente_aduanal_key,
            company_id,
            tenant_id,
        )

    return {
        "rfc_usuario_vu": rfc_usuario_vu,
        "clave_webservice": clave_webservice,
        "archivo_cer_base64": cer_b64 or "",
        "archivo_key_base64": key_b64 or "",
        "clave_fiel": clave_fiel_value,
        "email": email,
        "_ws_key_source": ws_key_source,
    }
