import logging
import time
import httpx
from datetime import datetime, timezone
from typing import Callable, Optional
from cachetools import TTLCache
from fastapi import Request, Response
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware

from .config import settings
from .security import get_tenant_from_token, verify_token, get_active_system

logger = logging.getLogger(__name__)

# Caché de validación de licencia por tenant (patrón SIWEB): evita consultar al
# Hub en cada request. Valor: "valid" o "invalid:<mensaje>". TTL corto para que
# los cambios de licencia se propaguen en minutos.
_license_cache: TTLCache = TTLCache(maxsize=1000, ttl=600)


def _normalize_text(value: str | None) -> str:
    if not value:
        return ""
    return str(value).strip().lower()


def _is_token_issue_message(*values: str | None) -> bool:
    text = " ".join(_normalize_text(v) for v in values if v)
    if not text:
        return False

    token_markers = ["token", "jwt", "bearer", "access"]
    invalid_markers = [
        "invalido", "inválido", "invalid", "not valid", "malformed", "signature", "unauthorized"
    ]
    expired_markers = ["expirado", "expirada", "expired", "has expired", "caducado", "vencido"]

    has_token_context = any(marker in text for marker in token_markers)
    has_invalid_marker = any(marker in text for marker in invalid_markers)
    has_expired_marker = any(marker in text for marker in expired_markers)

    return (has_expired_marker and has_token_context) or (has_token_context and has_invalid_marker)


def _extract_company_id(request: Request) -> Optional[int]:
    """Obtiene ``company_id`` activa desde header ``X-Company-Id`` o cookie.

    El frontend guarda la compañía activa en la cookie ``active_company_id``
    (ver ``frontend/src/lib/stores/company.svelte.ts``). El header es la
    ruta explícita para clientes no-browser.
    """
    header_value = request.headers.get("X-Company-Id")
    raw = header_value or request.cookies.get("active_company_id")
    if not raw:
        return None
    try:
        return int(raw)
    except (TypeError, ValueError):
        return None


class TenantMiddleware(BaseHTTPMiddleware):
    """
    Middleware original para extraer tenant_id y user_info del token.
    """
    async def dispatch(self, request: Request, call_next: Callable):
        doc_prefixes = ["/api/redoc", "/api/openapi.json"]
        public_prefixes = [
            "/api/v1/auth",
            "/api/v1/status",
            "/api/health",
            "/api/",
            "/uploads",
            "/api/v1/core/help-center",
            "/api/v1/core/users/avatar",
        ]

        path = request.url.path

        if any(path == prefix or path.startswith(prefix + "/") for prefix in doc_prefixes):
            return await call_next(request)

        if any(path == prefix or (prefix != "/" and path.startswith(prefix)) for prefix in public_prefixes):
            return await call_next(request)

        auth_header = request.headers.get("Authorization")
        if not auth_header or not auth_header.startswith("Bearer "):
            return JSONResponse(
                status_code=401,
                content={
                    "error": "HTTP_ERROR",
                    "message": "Missing or invalid authorization header",
                    "status_code": 401,
                }
            )

        token = auth_header.split(" ")[1]
        try:
            user_info = await verify_token(token)
            tenant_id = get_tenant_from_token(user_info)

            request.state.tenant_id = tenant_id
            request.state.user_info = user_info
            request.state.company_id = _extract_company_id(request)
            request.state.active_system = get_active_system(request)
        except Exception as e:
            logger.error(f"❌ Tenant validation error: {str(e)}")
            return JSONResponse(
                status_code=401,
                content={
                    "error": "HTTP_ERROR",
                    "message": "Invalid authentication",
                    "status_code": 401,
                }
            )

        return await call_next(request)


class LicenseValidationMiddleware(BaseHTTPMiddleware):
    """
    Middleware que valida la licencia contra el Hub de Aduanasoft.
    El Hub siempre es requerido — tanto en SaaS como en self-hosted.
    Fail-closed: si el Hub no responde o la licencia es inválida, se bloquea el acceso.
    """
    async def dispatch(self, request: Request, call_next: Callable):
        # En modo local (DEV_LOCAL_AUTH) no hay Hub — saltar validación de licencia.
        if settings.DEV_LOCAL_AUTH:
            return await call_next(request)

        exempt_paths = [
            "/api/docs", "/api/redoc", "/openapi.json",
            "/api/v1/auth", "/api/v1/status", "/api/health",
            "/api/v1/core/help-center",
            "/api/v1/core/users/avatar",
        ]

        is_exempt = any(
            request.url.path == path or (path != "/" and request.url.path.startswith(path))
            for path in exempt_paths
        )

        if is_exempt:
            return await call_next(request)

        auth_header = request.headers.get("Authorization")
        if not auth_header or not auth_header.startswith("Bearer "):
            # Permitimos pasar para que TenantMiddleware maneje el 401
            return await call_next(request)
        
        token = auth_header.split(" ")[1]

        # Sesión local del CRM (patrón SIWEB): el Bearer es un JWT HS256 propio que
        # el Hub NO entiende. No se le reenvía: la licencia se valida con el token KC
        # guardado en valkey y se cachea por tenant.
        if getattr(settings, "SESSION_STORE_ENABLED", False):
            try:
                from core.local_session import verify_session_token
                local_claims = verify_session_token(token)
            except Exception:
                local_claims = None
            if local_claims is not None:
                return await self._handle_local_session_license(request, call_next, local_claims)

        tenant_override = request.headers.get("X-Tenant-Override")
        if not tenant_override:
            # Fallback para flujos SSO cuando el override no viaja en header.
            tenant_override = request.cookies.get("sso_tenant_id") or request.cookies.get("sso_tenant_pub")

        # TenantMiddleware (corre antes) ya resolvió el token y dejó tenant en user_info.
        # Sin esto, Swagger/curl sin cookies SSO llaman verify-license sin contexto y el Hub
        # puede devolver 401 aunque /auth/me con el mismo Bearer responda 200.
        if not tenant_override:
            user_info = getattr(request.state, "user_info", None)
            if isinstance(user_info, dict):
                tid = user_info.get("tenant_id")
                if tid is not None and str(tid).strip() != "":
                    tenant_override = str(tid)

        hub_headers = {"Authorization": f"Bearer {token}"}
        if tenant_override:
            hub_headers["X-Tenant-Override"] = str(tenant_override)
            logger.info("[license] tenant override propagated to Hub: %s", tenant_override)

        # Solo la petición HTTP al Hub va en try: los errores de rutas (p. ej. ContextVar RLS)
        # deben propagarse y no etiquetarse como fallo de licencia.
        try:
            async with httpx.AsyncClient(timeout=5.0) as client:
                response = await client.get(
                    f"{settings.HUB_URL}api/v1/auth/verify-license",
                    headers=hub_headers
                )
        except (httpx.ConnectError, httpx.TimeoutException) as e:
            logger.critical(f"❌ CRITICAL: Hub unreachable: {str(e)}")
            return JSONResponse(
                status_code=503,
                content={
                    "error": "HUB_OFFLINE",
                    "message": "Servicio de licencias fuera de línea. Acceso denegado.",
                    "status_code": 503,
                }
            )
        except Exception as e:
            logger.exception("Hub verify-license request failed: %s", e)
            return JSONResponse(
                status_code=500,
                content={
                    "error": "VALIDATION_ERROR",
                    "message": "Error interno al contactar el servicio de licencias.",
                    "status_code": 500,
                }
            )        

        if response.status_code == 404:
            # Endpoint no existe en este Hub — dejar pasar
            return await call_next(request)

        if response.status_code == 200:
            try:
                data = response.json()
            except Exception as e:
                logger.error(f"Hub verify-license JSON parse failed: {str(e)}")
                return JSONResponse(
                    status_code=503,
                    content={
                        "error": "HUB_ERROR",
                        "message": "Respuesta inválida del servidor de licencias.",
                        "status_code": 503,
                    }
                )

            # Escenario 1: sin licencia asignada o licencia inactiva
            if not data.get("valid", False):
                message = data.get("message", "Sin licencia asignada para este tenant")
                detail = data.get("detail")
                reason = data.get("reason")
                # Si el Hub reporta token inválido/expirado, devolver 401 para que
                # el frontend dispare el auto-refresh (solo se activa con 401/403, no 402).
                if _is_token_issue_message(message, detail, reason):
                    logger.warning(
                        "[license] token expirado/invalido detectado por verify-license; devolviendo 401 para silent refresh | message=%s detail=%s reason=%s",
                        message,
                        detail,
                        reason,
                    )
                    return JSONResponse(
                        status_code=401,
                        content={
                            "error": "TOKEN_EXPIRED",
                            "message": message,
                            "status_code": 401,
                        }
                    )

                logger.warning(
                    "[license] licencia invalida para tenant=%s | message=%s",
                    data.get("tenant_slug"),
                    message,
                )
                return JSONResponse(
                    status_code=402,
                    content={
                        "error": "LICENSE_ERROR",
                        "message": message,
                        "status_code": 402,
                    }
                )

            # Escenario 2: licencia vencida (verificación local de expires_at)
            expires_at_str = data.get("expires_at")
            if expires_at_str:
                try:
                    expires_at = datetime.fromisoformat(expires_at_str.replace("Z", "+00:00"))
                    if expires_at.tzinfo is None:
                        expires_at = expires_at.replace(tzinfo=timezone.utc)
                    if expires_at < datetime.now(timezone.utc):
                        logger.warning(
                            "[license] licencia expirada para tenant=%s | expires_at=%s",
                            data.get("tenant_slug"),
                            expires_at_str,
                        )
                        return JSONResponse(
                            status_code=402,
                            content={
                                "error": "LICENSE_EXPIRED",
                                "message": f"La licencia venció el {expires_at.strftime('%d/%m/%Y')}. Renueva tu suscripción.",
                                "status_code": 402,
                            }
                        )
                except (ValueError, TypeError):
                    pass  # Si no se puede parsear, dejamos pasar — el Hub es la fuente de verdad

            request.state.license_info = data
            return await call_next(request)

        if response.status_code == 401:
            logger.warning("[license] Hub verify-license devolvio 401 (token invalido/expirado)")
            return JSONResponse(
                status_code=401,
                content={
                    "error": "TOKEN_EXPIRED",
                    "message": "Token inválido o expirado.",
                    "status_code": 401,
                }
            )

        if response.status_code == 403:
            return JSONResponse(
                status_code=403,
                content={
                    "error": "FORBIDDEN",
                    "message": "El Tenant no tiene permisos en el Hub central.",
                    "status_code": 403,
                }
            )

        logger.error(f"Hub error status: {response.status_code}")
        return JSONResponse(
            status_code=503,
            content={
                "error": "HUB_ERROR",
                "message": "Error en el servidor de licencias.",
                "status_code": 503,
            }
        )

    async def _handle_local_session_license(self, request: Request, call_next: Callable, local_claims: dict):
        """
        Valida licencia para una sesión local del CRM (patrón SIWEB).

        El Hub no valida el JWT HS256 local, así que se usa el token KC guardado en
        valkey (refrescándolo si está vencido) para consultar verify-license, con
        caché por tenant. Si el Hub no es concluyente (p. ej. su refresh falla), se
        permite el paso: la sesión local se emitió tras un login válido (el App
        Launcher solo ofrece apps licenciadas), evitando bloquear por un problema
        transitorio del Hub. Los resultados concluyentes (válido/ inválido) sí se cachean.
        """
        from core import session_store

        tenant_key = str(local_claims.get("tenant_id") or "")

        cached = _license_cache.get(tenant_key) if tenant_key else None
        if cached == "valid":
            return await call_next(request)
        if isinstance(cached, str) and cached.startswith("invalid:"):
            return JSONResponse(
                status_code=402,
                content={"error": "LICENSE_ERROR", "message": cached[len("invalid:"):], "status_code": 402},
            )

        tenant_override = (
            tenant_key
            or request.cookies.get("sso_tenant_id")
            or request.cookies.get("sso_tenant_pub")
            or ""
        )

        sid = request.cookies.get("crm_sid")
        sess = session_store.get_session(sid) if sid else None
        kc_token = (sess or {}).get("access_token") or ""
        kc_refresh = (sess or {}).get("refresh_token") or ""

        async def _verify(tok: str):
            if not tok:
                return None
            headers = {"Authorization": f"Bearer {tok}"}
            if tenant_override:
                headers["X-Tenant-Override"] = str(tenant_override)
            try:
                async with httpx.AsyncClient(timeout=5.0) as client:
                    return await client.get(
                        f"{settings.HUB_URL}api/v1/auth/verify-license", headers=headers
                    )
            except Exception as exc:
                logger.warning("[license] verify-license (sesión local) error de red: %s", exc)
                return None

        resp = await _verify(kc_token)

        # ¿El KC token guardado está vencido? Refrescar una vez y reintentar.
        needs_refresh = resp is None or resp.status_code == 401
        if not needs_refresh and resp.status_code == 200:
            try:
                _d = resp.json()
            except Exception:
                _d = {}
            if not _d.get("valid", False) and _is_token_issue_message(
                _d.get("message"), _d.get("detail"), _d.get("reason")
            ):
                needs_refresh = True

        if needs_refresh and kc_refresh:
            try:
                async with httpx.AsyncClient(timeout=8.0) as client:
                    rr = await client.post(
                        f"{settings.HUB_URL}api/v1/auth/refresh",
                        json={"refresh_token": kc_refresh},
                    )
                if rr.status_code == 200:
                    nt = rr.json()
                    kc_token = nt.get("access_token") or kc_token
                    if sid:
                        session_store.update_session_tokens(
                            sid, kc_token, nt.get("refresh_token") or kc_refresh
                        )
                    resp = await _verify(kc_token)
                else:
                    logger.warning("[license] refresh KC para verify-license devolvió %s", rr.status_code)
            except Exception as exc:
                logger.warning("[license] refresh KC para verify-license falló: %s", exc)

        if resp is not None and resp.status_code == 200:
            try:
                data = resp.json()
            except Exception:
                data = {}
            if data.get("valid", False):
                expires_at_str = data.get("expires_at")
                if expires_at_str:
                    try:
                        expires_at = datetime.fromisoformat(expires_at_str.replace("Z", "+00:00"))
                        if expires_at.tzinfo is None:
                            expires_at = expires_at.replace(tzinfo=timezone.utc)
                        if expires_at < datetime.now(timezone.utc):
                            msg = f"La licencia venció el {expires_at.strftime('%d/%m/%Y')}. Renueva tu suscripción."
                            if tenant_key:
                                _license_cache[tenant_key] = f"invalid:{msg}"
                            return JSONResponse(
                                status_code=402,
                                content={"error": "LICENSE_EXPIRED", "message": msg, "status_code": 402},
                            )
                    except (ValueError, TypeError):
                        pass
                if tenant_key:
                    _license_cache[tenant_key] = "valid"
                request.state.license_info = data
                return await call_next(request)

            message = data.get("message", "Sin licencia asignada para este tenant")
            if not _is_token_issue_message(data.get("message"), data.get("detail"), data.get("reason")):
                if tenant_key:
                    _license_cache[tenant_key] = f"invalid:{message}"
                return JSONResponse(
                    status_code=402,
                    content={"error": "LICENSE_ERROR", "message": message, "status_code": 402},
                )

        # No concluyente (Hub no dio 200, o el problema de token persiste porque su
        # refresh falla): la sesión local es válida → permitir sin cachear. Evita el
        # bucle de 401 por el bug de refresh del Hub.
        logger.warning(
            "[license] verify-license no concluyente para sesión local (tenant=%s) — se permite",
            tenant_key,
        )
        return await call_next(request)


class RequestLoggingMiddleware(BaseHTTPMiddleware):
    """
    Middleware original para logging de performance.
    """
    async def dispatch(self, request: Request, call_next: Callable):
        start_time = time.time()
        excluded_paths = ["/api/docs", "/api/redoc", "/openapi.json", "/api/v1/status", "/api/health"]
        
        if any(request.url.path == path or request.url.path.startswith(path + "/") for path in excluded_paths):
            return await call_next(request)

        logger.info(f"Request: {request.method} {request.url.path}")
        response = await call_next(request)
        process_time = time.time() - start_time
        
        logger.info(
            f"Response: {request.method} {request.url.path} "
            f"Status: {response.status_code} "
            f"Duration: {process_time:.3f}s"
        )
        response.headers["X-Process-Time"] = str(process_time)
        return response