from __future__ import annotations

from dataclasses import dataclass
from hashlib import sha256
from time import sleep, time
from typing import Any

from .backend_client import BackendClient, BackendResponse
from .cloud_context import CloudMcpContext
from .cloud_redis import CloudAuthCache, RedisCloudAuthCache
from .config import (
    get_cloud_auth_context_lock_seconds,
    get_cloud_auth_context_lock_wait_seconds,
    get_cloud_auth_context_negative_ttl_seconds,
    get_cloud_auth_context_ttl_seconds,
    get_cloud_redis_url,
    get_default_base_url,
    get_default_qf_version,
    normalize_base_url,
)
from .errors import QingflowApiError, is_auth_like_error
from .json_types import JSONObject


AUTH_CONTEXT_KEY_PREFIX = "mcp:auth:context"
AUTH_CONTEXT_LOCK_KEY_PREFIX = "mcp:auth:context:lock"
AUTH_CONTEXT_NEGATIVE_KEY_PREFIX = "mcp:auth:context:negative"


@dataclass(slots=True)
class CloudAuthBroker:
    cache: CloudAuthCache
    backend: BackendClient
    base_url: str
    qf_version: str | None = None
    ttl_seconds: int = 604800
    lock_seconds: int = 10
    negative_ttl_seconds: int = 30
    lock_wait_seconds: float = 5.0

    @classmethod
    def from_config(cls, backend: BackendClient | None = None) -> "CloudAuthBroker":
        redis_url = get_cloud_redis_url()
        if not redis_url:
            raise QingflowApiError.config_error("cloud.redis_url is required for Cloud MCP mode")
        base_url = get_default_base_url()
        if not base_url:
            raise QingflowApiError.config_error(
                "default_base_url is required for Cloud MCP mode"
            )
        return cls(
            cache=RedisCloudAuthCache.from_url(redis_url),
            backend=backend or BackendClient(),
            base_url=base_url,
            qf_version=get_default_qf_version(),
            ttl_seconds=get_cloud_auth_context_ttl_seconds(),
            lock_seconds=get_cloud_auth_context_lock_seconds(),
            negative_ttl_seconds=get_cloud_auth_context_negative_ttl_seconds(),
            lock_wait_seconds=get_cloud_auth_context_lock_wait_seconds(),
        )

    @classmethod
    def from_env(cls, backend: BackendClient | None = None) -> "CloudAuthBroker":
        return cls.from_config(backend=backend)

    def resolve(self, credential: str, *, force_refresh: bool = False) -> CloudMcpContext:
        normalized_credential = str(credential or "").strip()
        if not normalized_credential:
            raise QingflowApiError(category="auth", message="MCP credential is required")

        credential_hash = self.credential_hash(normalized_credential)
        auth_key = self._auth_key(credential_hash)
        negative_key = self._negative_key(credential_hash)
        if force_refresh:
            self.cache.delete(auth_key)

        cached = None if force_refresh else self.cache.get_json(auth_key)
        if cached is not None:
            return self._context_from_cache(credential_hash, normalized_credential, cached)
        if self.cache.exists(negative_key):
            raise QingflowApiError(category="auth", message="MCP credential is invalid or expired")

        lock = self.cache.acquire_lock(self._lock_key(credential_hash), self.lock_seconds)
        if not lock.acquired:
            return self._wait_for_peer_context(credential_hash, normalized_credential)

        try:
            cached = None if force_refresh else self.cache.get_json(auth_key)
            if cached is not None:
                return self._context_from_cache(credential_hash, normalized_credential, cached)
            if self.cache.exists(negative_key):
                raise QingflowApiError(category="auth", message="MCP credential is invalid or expired")
            payload = self._fetch_auth_context(normalized_credential, credential_hash)
            self.cache.set_json(auth_key, payload, self.ttl_seconds)
            return self._context_from_cache(credential_hash, normalized_credential, payload)
        finally:
            lock.release()

    def refresh(self, context: CloudMcpContext) -> CloudMcpContext:
        self.invalidate(context.credential_hash)
        return self.resolve(context.credential, force_refresh=True)

    def invalidate(self, credential_hash: str) -> None:
        self.cache.delete(self._auth_key(credential_hash))

    @staticmethod
    def credential_hash(credential: str) -> str:
        return sha256(credential.encode("utf-8")).hexdigest()

    def _wait_for_peer_context(self, credential_hash: str, credential: str) -> CloudMcpContext:
        auth_key = self._auth_key(credential_hash)
        negative_key = self._negative_key(credential_hash)
        deadline = time() + self.lock_wait_seconds
        while time() < deadline:
            cached = self.cache.get_json(auth_key)
            if cached is not None:
                return self._context_from_cache(credential_hash, credential, cached)
            if self.cache.exists(negative_key):
                raise QingflowApiError(category="auth", message="MCP credential is invalid or expired")
            sleep(0.05)

        lock = self.cache.acquire_lock(self._lock_key(credential_hash), self.lock_seconds)
        if lock.acquired:
            try:
                cached = self.cache.get_json(auth_key)
                if cached is not None:
                    return self._context_from_cache(credential_hash, credential, cached)
                payload = self._fetch_auth_context(credential, credential_hash)
                self.cache.set_json(auth_key, payload, self.ttl_seconds)
                return self._context_from_cache(credential_hash, credential, payload)
            finally:
                lock.release()
        raise QingflowApiError(category="cache", message="Timed out waiting for MCP auth context cache")

    def _fetch_auth_context(self, credential: str, credential_hash: str) -> JSONObject:
        try:
            response = self.backend.public_request_with_meta(
                "POST",
                self.base_url,
                "/mcp/auth/context",
                json_body={"credential": credential},
                qf_version=self.qf_version,
            )
        except QingflowApiError as error:
            if is_auth_like_error(error):
                self.cache.set_json(
                    self._negative_key(credential_hash),
                    {"message": error.message, "createdAt": int(time())},
                    self.negative_ttl_seconds,
                )
            raise

        payload = self._unwrap_payload(response)
        token = self._normalize_text(payload.get("token"))
        uid = self._coerce_positive_int(payload.get("uid"))
        ws_id = self._coerce_positive_int(payload.get("wsId") or payload.get("ws_id"))
        if not token or uid is None or ws_id is None:
            raise QingflowApiError(category="auth", message="Credential context did not return token, uid, and wsId")

        base_url = normalize_base_url(self._normalize_text(payload.get("baseUrl"))) or self.base_url
        qf_version = self._normalize_text(payload.get("qfVersion")) or response.qf_response_version or self.qf_version
        qf_version_source = "backend_response" if qf_version else None
        return {
            "credentialId": self._coerce_positive_int(payload.get("credentialId") or payload.get("credential_id") or payload.get("id")),
            "token": token,
            "uid": uid,
            "wsId": ws_id,
            "baseUrl": base_url,
            "qfVersion": qf_version,
            "qfVersionSource": qf_version_source,
            "email": self._normalize_text(payload.get("email")),
            "nickName": self._normalize_text(
                payload.get("nickName") or payload.get("displayName") or payload.get("name")
            ),
            "wsName": self._normalize_text(payload.get("wsName") or payload.get("workspaceName")),
            "createdAt": int(time()),
        }

    def _context_from_cache(self, credential_hash: str, credential: str, payload: JSONObject) -> CloudMcpContext:
        token = self._normalize_text(payload.get("token"))
        uid = self._coerce_positive_int(payload.get("uid"))
        ws_id = self._coerce_positive_int(payload.get("wsId") or payload.get("ws_id"))
        base_url = normalize_base_url(self._normalize_text(payload.get("baseUrl"))) or self.base_url
        if not token or uid is None or ws_id is None:
            self.cache.delete(self._auth_key(credential_hash))
            raise QingflowApiError(category="auth", message="Cached MCP auth context is incomplete")
        context = CloudMcpContext(
            credential_hash=credential_hash,
            credential=credential,
            credential_id=self._coerce_positive_int(
                payload.get("credentialId") or payload.get("credential_id") or payload.get("id")
            ),
            token=token,
            uid=uid,
            ws_id=ws_id,
            base_url=base_url,
            qf_version=self._normalize_text(payload.get("qfVersion") or payload.get("qf_version")),
            qf_version_source=self._normalize_text(payload.get("qfVersionSource") or payload.get("qf_version_source")),
            email=self._normalize_text(payload.get("email")),
            nick_name=self._normalize_text(payload.get("nickName") or payload.get("nick_name")),
            ws_name=self._normalize_text(payload.get("wsName") or payload.get("ws_name")),
        )
        context.refresh_context = lambda: self.refresh(context)
        return context

    def _unwrap_payload(self, response: BackendResponse) -> JSONObject:
        data = response.data
        if not isinstance(data, dict):
            raise QingflowApiError(category="auth", message="Credential context did not return a valid result")
        for key in ("data", "result"):
            nested = data.get(key)
            if isinstance(nested, dict):
                return nested
        return data

    def _auth_key(self, credential_hash: str) -> str:
        return f"{AUTH_CONTEXT_KEY_PREFIX}:{credential_hash}"

    def _lock_key(self, credential_hash: str) -> str:
        return f"{AUTH_CONTEXT_LOCK_KEY_PREFIX}:{credential_hash}"

    def _negative_key(self, credential_hash: str) -> str:
        return f"{AUTH_CONTEXT_NEGATIVE_KEY_PREFIX}:{credential_hash}"

    def _normalize_text(self, value: Any) -> str | None:
        if value is None:
            return None
        text = str(value).strip()
        return text or None

    def _coerce_positive_int(self, value: Any) -> int | None:
        if isinstance(value, bool) or value is None:
            return None
        try:
            parsed = int(value)
        except (TypeError, ValueError):
            return None
        return parsed if parsed > 0 else None
