from __future__ import annotations

import base64
import logging
import time
from typing import Any, Dict
from xml.etree import ElementTree as ET

from celery import Task

from core.celery_app import celery_app
from core.database import CoreSessionLocal
from core.exceptions import ValidationException
from core import storage_s3
from core.s3_keys import cove_xml_key
from api.v1.modules.a76.invoices.models import InvoiceHeader, InvoiceComplianceMx

from .service import FacturaCoveDomainService
from .schemas import GenerateCoveResult
from .external_service import CoveExternalService, CoveExternalResult

logger = logging.getLogger(__name__)


def _progress(task: Task, current: int, status: str) -> None:
    task.update_state(state="PROGRESS", meta={"current": current, "status": status})


def _strip_ns(tag: str) -> str:
    """Elimina el namespace {uri} de un tag XML y lo devuelve en minúsculas."""
    if "}" in tag:
        tag = tag.split("}", 1)[1]
    return tag.strip().lower()


def _extract_edocument_from_xml(xml_bytes: bytes) -> str | None:
    """
    Extrae el número de e-document (COVE) de un XML de VUCEM.

    Busca los tags más comunes que VUCEM utiliza para el identificador
    del documento COVE: <eDocumento>, <e-document>, <edocument>, <cove>.
    """
    try:
        root = ET.fromstring(xml_bytes)
    except Exception:
        return None

    for node in root.iter():
        tag = _strip_ns(node.tag)
        if tag in {"edocumento", "e-document", "edocument", "cove", "numerocove", "numero_cove"}:
            value = (node.text or "").strip()
            if value:
                return value
    return None


def _parse_consulta_respuesta(b64_str: str) -> tuple[bool, str | None, list[str]]:
    """
    Parsea el XML de la respuesta de VUCEM a la consulta de estado (campo
    ``consulta_respuesta_base64``).

    Devuelve una tupla (tiene_error, edocumento, mensajes_error) donde:
      - tiene_error: True si VUCEM reportó <contieneError>true</>
      - edocumento: el número de COVE asignado por VUCEM (<eDocument>) si no hay error
      - mensajes_error: lista de textos de error si tiene_error es True
    """
    tiene_error = False
    edocumento: str | None = None
    mensajes_error: list[str] = []

    if not b64_str:
        return tiene_error, edocumento, mensajes_error

    try:
        xml_bytes = base64.b64decode(b64_str)
        root = ET.fromstring(xml_bytes)
    except Exception:
        logger.warning("No se pudo decodificar/parsear consulta_respuesta_base64", exc_info=True)
        return tiene_error, edocumento, mensajes_error

    for node in root.iter():
        tag = _strip_ns(node.tag)

        if tag == "contieneerror":
            tiene_error = (node.text or "").strip().lower() == "true"

        elif tag in {"edocument", "edocumento"}:
            # VUCEM usa <eDocument> (sin 'o') en la respuesta de consulta
            value = (node.text or "").strip()
            if value:
                edocumento = value

        elif tag in {"mensaje", "leyenda", "descripcionerror", "mensajeerror"}:
            # VUCEM puede usar <leyenda> como mensaje general o <mensaje>/<descripcionError> para errores
            text = (node.text or "").strip()
            if text and tiene_error:
                mensajes_error.append(text)

    return tiene_error, edocumento, mensajes_error


def _save_cove_result(
    db: "Session", invoice_id: int, final_external: CoveExternalResult
) -> None:
    """
    Persiste en la factura el número de COVE, el número de operación VUCEM
    y el XML de COVE (en S3) cuando el servicio externo reporta SUCCESS.
    """
    if final_external.status != "success":
        return

    invoice = db.get(InvoiceHeader, invoice_id)
    if not invoice:
        logger.error("No se encontró la factura %s para guardar COVE", invoice_id)
        return

    compliance = invoice.compliance_mx
    if not compliance:
        compliance = InvoiceComplianceMx(
            invoice_id=invoice.id,
            tenant_id=invoice.tenant_id,
            company_id=invoice.company_id,
        )
        db.add(compliance)

    # Guardar XML de COVE en S3 y extraer e-document si no está en la factura.
    if final_external.cove_xml_base64:
        try:
            xml_bytes = base64.b64decode(final_external.cove_xml_base64)
            s3_key = cove_xml_key(invoice.tenant_id, invoice.company_id, invoice_id)
            storage_s3.put_object_bytes(s3_key, xml_bytes, content_type="application/xml")
            logger.info("XML de COVE guardado en S3: %s", s3_key)

            # Extraer e-document del XML si la factura aún no lo tiene.
            if not (compliance.edocument or "").strip():
                edoc_from_xml = _extract_edocument_from_xml(xml_bytes)
                if edoc_from_xml:
                    compliance.edocument = edoc_from_xml
                    logger.info("e-document extraído del XML y guardado: %s", edoc_from_xml)
        except Exception:
            logger.exception("Error guardando XML de COVE en S3 para factura %s", invoice_id)

    # Idempotencia básica: solo sobrescribir si está vacío o coincide.
    if final_external.cove_number:
        current_cove = compliance.edocument or ""
        new_cove = final_external.cove_number or ""
        if not current_cove or current_cove == new_cove:
            compliance.edocument = new_cove

    if final_external.vucem_operation_num:
        current_op = compliance.vucem_operation_num or ""
        new_op = final_external.vucem_operation_num or ""
        if not current_op or current_op == new_op:
            compliance.vucem_operation_num = new_op

    db.commit()


def _poll_external_status(
    task: Task, external: CoveExternalService, external_task_id: str, timeout_seconds: int = 300
) -> CoveExternalResult:
    """
    Realiza polling al endpoint externo de status de COVE hasta obtener un estado final
    o agotar el timeout.
    """
    start = time.time()
    last_payload: Dict[str, Any] = {}

    while True:
        if time.time() - start > timeout_seconds:
            logger.error("Timeout consultando estado de COVE para external_task_id=%s", external_task_id)
            return CoveExternalResult(
                status="error",
                message="Timeout consultando estado de COVE en Ventanilla Única.",
                cove_number=None,
                vucem_operation_num=None,
                raw_response={"last_status": last_payload, "external_task_id": external_task_id},
            )

        try:
            status_payload = external.get_status(external_task_id)
        except Exception as exc:  # pragma: no cover - errores HTTP inesperados
            logger.exception("Error consultando estado externo de COVE")
            return CoveExternalResult(
                status="error",
                message=f"Error consultando estado de COVE en Ventanilla Única: {exc}",
                cove_number=None,
                vucem_operation_num=None,
                raw_response={"last_status": last_payload, "external_task_id": external_task_id},
            )

        last_payload = status_payload or {}
        state = str(last_payload.get("state") or "").upper()
        progress = last_payload.get("progress") or {}
        # En el ejemplo: progress.progress (float 0-100), progress.current_step (texto), numero_operacion
        try:
            percent = float(progress.get("progress", 0.0))
        except (TypeError, ValueError):
            percent = 0.0
        current_step = progress.get("current_step") or "Consultando estado de COVE en Ventanilla Única..."
        numero_operacion = progress.get("numero_operacion") or last_payload.get("numero_operacion")
        if numero_operacion:
            current_step = f"{current_step} (Operación: {numero_operacion})"

        # Actualizar progreso para que el frontend lo vea en el diálogo
        _progress(task, int(percent), str(current_step))

        # Estados intermedios: seguimos pollendo
        if state in {"PENDING", "STARTED", "PROGRESS"} or not state:
            time.sleep(5)
            continue

        # Estado final: SUCCESS / FAILURE u otros
        result_payload = last_payload.get("result") or {}
        error_text = last_payload.get("error")

        if state in {"SUCCESS", "COMPLETED"}:
            vucem_operation_num = result_payload.get("vucem_operation_num") or result_payload.get(
                "numero_operacion"
            )
            # SOAP request del COVE enviado a VUCEM (para el acuse PDF)
            cove_xml_base64 = result_payload.get("envio_base64") or result_payload.get("cove_xml_base64")

            # Extraer el e-document (COVE number) del XML de respuesta de VUCEM.
            # Solo se usa para capturar el número; la detección de errores sigue
            # basándose en el campo tiene_errores del VU API.
            consulta_b64 = result_payload.get("consulta_respuesta_base64", "") or ""
            _, vucem_edocumento, _ = _parse_consulta_respuesta(consulta_b64)

            cove_number = (
                vucem_edocumento
                or result_payload.get("cove_number")
                or result_payload.get("cove")
            )

            tiene_errores = bool(result_payload.get("tiene_errores"))
            mensajes = result_payload.get("mensajes")
            first_error_message = None
            if isinstance(mensajes, list) and mensajes:
                first_item = mensajes[0] or {}
                if isinstance(first_item, dict):
                    first_error_message = (
                        first_item.get("descripcion")
                        or first_item.get("mensaje")
                        or first_item.get("codigo")
                    )

            if tiene_errores:
                message = (
                    first_error_message
                    or result_payload.get("message")
                    or last_payload.get("message")
                    or "Factura COVE transmitida con errores."
                )
                return CoveExternalResult(
                    status="error",
                    message=str(message),
                    cove_number=cove_number,
                    vucem_operation_num=vucem_operation_num,
                    cove_xml_base64=cove_xml_base64,
                    raw_response={**last_payload, "external_task_id": external_task_id},
                )

            message = result_payload.get("message") or last_payload.get("message") or "COVE generado correctamente."

            return CoveExternalResult(
                status="success",
                message=message,
                cove_number=cove_number,
                vucem_operation_num=vucem_operation_num,
                cove_xml_base64=cove_xml_base64,
                raw_response={**last_payload, "external_task_id": external_task_id},
            )

        # Cualquier otro estado lo tratamos como error
        message = error_text or result_payload.get("message") or last_payload.get("message") or state

        return CoveExternalResult(
            status="error",
            message=str(message),
            cove_number=None,
            vucem_operation_num=None,
            raw_response={**last_payload, "external_task_id": external_task_id},
        )


@celery_app.task(bind=True, name="factura_cove_generate")
def factura_cove_generate(
    self: Task,
    invoice_id: int,
    tenant_id: int,
    company_id: int,
    recipient_email: str | None = None,
) -> dict:
    """
    Tarea Celery para generar COVE a partir de una factura.

    Flujo actual:
      - Valida prerrequisitos de factura y configuración VU.
      - Construye el payload FacturaCoveRequest.
      - Invoca el servicio externo de COVE.
      - Si el externo devuelve task_id, hace polling hasta estado final.
      - Persiste número de COVE / operación VUCEM cuando aplica.
    """
    db = CoreSessionLocal()

    try:
        _progress(self, 5, "Validando factura para COVE...")
        service = FacturaCoveDomainService(db)

        # Esta llamada valida todo y construye el payload; si algo falla, lanza ValidationException
        request_payload = service.build_factura_cove_request(
            invoice_id=invoice_id,
            tenant_id=tenant_id,
            company_id=company_id,
            recipient_email=recipient_email,
        )

        _progress(self, 80, "Enviando solicitud al servicio COVE...")

        # Integración externa que encola la generación de COVE en Ventanilla Única
        external = CoveExternalService()
        external_result = external.generate_cove(request_payload)

        # Si el servicio externo devolvió un error inmediato (por ejemplo 422),
        # devolvemos ese resultado tal cual sin hacer polling adicional.
        if external_result.status in {"error", "validation_error"}:
            result = GenerateCoveResult(
                status=external_result.status,
                message=external_result.message,
                invoice_id=invoice_id,
                cove_number=external_result.cove_number,
                vucem_operation_num=external_result.vucem_operation_num,
                external_task_id=(
                    external_result.raw_response.get("task_id") if external_result.raw_response else None
                ),
                external_response=external_result.raw_response,
                errors=None,
            )
            return result.model_dump()

        external_task_id = (
            external_result.raw_response.get("task_id") if external_result.raw_response else None
        )

        # Si la factura quedó encolada en VU y tenemos un task_id externo, hacemos polling
        # al endpoint de status para acompañar el progreso completo hasta obtener COVE.
        if external_task_id and external_result.status in {"external_queued", "queued", "success"}:
            _progress(
                self,
                85,
                "Factura enviada a Ventanilla Única, consultando estado de COVE...",
            )
            final_external = _poll_external_status(self, external, external_task_id)
        else:
            # Fallback: usamos el resultado tal cual devolvió el endpoint de generación
            final_external = external_result

        # Intentar persistir COVE / número de operación en la factura cuando sea éxito.
        try:
            _save_cove_result(db, invoice_id, final_external)
        except Exception:
            # No fallamos la tarea por errores de persistencia; solo los registramos.
            logger.exception("Error guardando COVE en la factura %s", invoice_id)

        _progress(self, 100, "Proceso de COVE finalizado.")

        result = GenerateCoveResult(
            status=final_external.status,
            message=final_external.message,
            invoice_id=invoice_id,
            cove_number=final_external.cove_number,
            vucem_operation_num=final_external.vucem_operation_num,
            external_task_id=(
                final_external.raw_response.get("external_task_id")
                or final_external.raw_response.get("task_id")
                if final_external.raw_response
                else None
            ),
            external_response=final_external.raw_response,
            errors=None,
        )
        return result.model_dump()

    except ValidationException as exc:
        db.rollback()
        logger.info("Validation error in factura_cove_generate: %s", exc.message)
        result = GenerateCoveResult(
            status="validation_error",
            message=exc.message,
            invoice_id=invoice_id,
            errors=exc.errors,
        )
        return result.model_dump()
    except Exception as exc:  # pragma: no cover - errores inesperados de runtime
        db.rollback()
        logger.exception("Unexpected error in factura_cove_generate")
        result = GenerateCoveResult(
            status="error",
            message=str(exc),
            invoice_id=invoice_id,
            errors=None,
        )
        return result.model_dump()
    finally:
        db.close()

