from __future__ import annotations

import base64
import hashlib
import hmac
import json
import os
import time
from contextlib import asynccontextmanager
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, Iterable, Mapping, MutableMapping, Optional, Sequence
from urllib.parse import parse_qs, urlparse

import httpx

from .oauth_flow import ensure_access_token
from .errors import (
    AuthenticationError,
    MetaApiError,
    MetaApiRateLimitError,
    RateLimitError,
    map_meta_error,
)
from .logging import get_logger
from .rate_limiter import RateLimitExceededError
from .schemas import GraphBodyEncoding, GraphFile
from .util import async_retry, redact_sensitive

if TYPE_CHECKING:
    from .circuit_breaker import CircuitBreaker

logger = get_logger(__name__)

if TYPE_CHECKING:
    from .config import Settings
    from .auth.provider import TokenProvider


@asynccontextmanager
async def _noop_trace():
    yield


class MetaClient:
    """Async client for the Meta Graph API."""

    def __init__(
        self,
        *,
        access_token: str,
        app_secret: str | None = None,
        token_provider: "TokenProvider | None" = None,
        base_url: str = "https://graph.facebook.com",
        version: str = "v24.0",
        timeout: float = 30.0,
        max_connections: int = 100,  # Increased for high-volume accounts
        transport: httpx.AsyncBaseTransport | None = None,
        enable_caching: bool = True,
        enable_rate_limiting: bool = True,
    ) -> None:
        self.access_token = access_token
        self.app_secret = app_secret
        self.token_provider = token_provider
        self.base_url = base_url.rstrip("/")
        self.version = version
        self.enable_caching = enable_caching
        self.enable_rate_limiting = enable_rate_limiting
        self.observability_manager = None
        
        # Enhanced connection limits for enterprise use
        self._client = httpx.AsyncClient(
            base_url=f"{self.base_url}/{self.version}/",
            timeout=timeout,
            http2=transport is None,  # Enable HTTP/2 unless custom transport provided
            headers={"User-Agent": "meta-ads-mcp/1.0"},
            limits=httpx.Limits(
                max_connections=max_connections,
                max_keepalive_connections=max_connections,
                keepalive_expiry=30.0  # Keep connections alive longer
            ),
            transport=transport,
        )
        
        # Initialize caching and rate limiting
        if self.enable_caching:
            from .cache import get_cache_manager
            self.cache_manager = get_cache_manager()
        
        if self.enable_rate_limiting:
            from .rate_limiter import get_rate_limiter
            self.rate_limiter = get_rate_limiter()

        try:
            from .observability import get_observability_manager
            self.observability_manager = get_observability_manager()
        except Exception as exc:  # pragma: no cover - optional dependencies
            logger.debug("observability_manager_init_failed error=%s", exc)

        # Circuit breaker for cascading failure protection
        self.circuit_breaker: "CircuitBreaker | None" = None
        try:
            from .circuit_breaker import get_circuit_breaker
            self.circuit_breaker = get_circuit_breaker()
        except Exception as exc:  # pragma: no cover
            logger.debug("circuit_breaker_init_skipped error=%s", exc)

    async def aclose(self) -> None:
        await self._client.aclose()
        if self.token_provider and hasattr(self.token_provider, "aclose"):
            await self.token_provider.aclose()

    async def __aenter__(self) -> "MetaClient":
        return self

    async def __aexit__(self, *exc_info: object) -> None:  # noqa: ANN002, ANN003
        await self.aclose()

    def _inject_auth(
        self,
        params: MutableMapping[str, Any] | None,
        *,
        access_token: str | None = None,
        use_appsecret_proof: bool = True,
    ) -> MutableMapping[str, Any]:
        params = dict(params or {})
        resolved_token = access_token or self.access_token
        params["access_token"] = resolved_token
        if use_appsecret_proof and self.app_secret:
            params.setdefault("appsecret_proof", self._compute_appsecret_proof(resolved_token))
        return params

    def _compute_appsecret_proof(self, access_token: str | None = None) -> str:
        resolved_token = access_token or self.access_token
        return hmac.new(
            key=self.app_secret.encode("utf-8"),
            msg=resolved_token.encode("utf-8"),
            digestmod=hashlib.sha256,
        ).hexdigest()

    def _prepare_files(self, files: Sequence[GraphFile]) -> list[tuple[str, tuple[str, bytes, str]]]:
        prepared: list[tuple[str, tuple[str, bytes, str]]] = []
        for file in files:
            if file.encoding == "base64":
                content = base64.b64decode(file.data)
            else:
                content = file.data.encode("utf-8")
            prepared.append(
                (
                    file.field,
                    (
                        file.filename,
                        content,
                        file.content_type or "application/octet-stream",
                    ),
                )
            )
        return prepared

    async def request(
        self,
        method: str,
        path: str,
        *,
        params: Mapping[str, Any] | None = None,
        body: Mapping[str, Any] | None = None,
        encoding: GraphBodyEncoding = GraphBodyEncoding.JSON,
        files: Sequence[GraphFile] | None = None,
        headers: Mapping[str, str] | None = None,
        use_appsecret_proof: bool = True,
        cache_ttl: int | None = None,
        user_id: str | None = None,
    ) -> Dict[str, Any]:
        """Perform a Graph API request with caching, rate limiting, and retry semantics."""
        from .auth.request_context import get_current_user_id

        started_at = time.perf_counter()
        last_status_code: int = 0
        rate_token_acquired = False
        path = path.lstrip("/")
        effective_user_id = user_id or get_current_user_id()

        request_access_token = self.access_token
        if self.token_provider:
            if not effective_user_id:
                raise AuthenticationError(
                    "Authenticated user context is required in hosted mode",
                    reason="missing_user_context",
                )
            request_access_token = await self.token_provider.get_token(effective_user_id)
        
        # Generate cache key for GET requests
        cache_key_str = None
        if method == "GET" and self.enable_caching:
            from .cache import cache_key, cache_ttl_for_endpoint
            cache_key_str = cache_key(
                method, path,
                "params", str(sorted((params or {}).items())),
                "user", effective_user_id or "default"
            )
            
            # Try cache first
            cached_result = await self.cache_manager.get(cache_key_str)
            if cached_result is not None:
                logger.debug("Cache hit for request", path=path, method=method)
                if self.observability_manager:
                    self.observability_manager.record_cache_metrics(
                        cache_type="response_cache",
                        endpoint=path,
                        hit=True,
                    )
                return cached_result
            if self.observability_manager:
                self.observability_manager.record_cache_metrics(
                    cache_type="response_cache",
                    endpoint=path,
                    hit=False,
                )
        
        # Rate limiting
        if self.enable_rate_limiting:
            try:
                rate_info = await self.rate_limiter.acquire(path, effective_user_id)
                rate_token_acquired = True
                logger.debug(
                    "Rate limit acquired",
                    extra={
                        "path": path,
                        "remaining": rate_info.remaining,
                        "reset_time": rate_info.reset_time,
                    },
                )
                if self.observability_manager:
                    self.observability_manager.record_rate_limit_metrics(
                        endpoint=path,
                        remaining=rate_info.remaining,
                        user_id=effective_user_id,
                    )
            except RateLimitExceededError as exc:
                retry_after = max(0, int(getattr(exc, "retry_after", 0)))
                logger.warning(
                    "local_rate_limit_exceeded path=%s user_id=%s retry_after=%s",
                    path,
                    effective_user_id or "unknown",
                    retry_after,
                )
                if self.observability_manager:
                    if self.observability_manager.metrics_collector:
                        self.observability_manager.metrics_collector.record_rate_limit_exceeded(
                            endpoint=path,
                            user_id=effective_user_id,
                        )
                    self.observability_manager.record_error_metrics(
                        error_type=type(exc).__name__,
                        endpoint=path,
                        ad_account_id=effective_user_id,
                    )
                    self.observability_manager.record_api_metrics(
                        method=method,
                        endpoint=path,
                        status_code=429,
                        duration=time.perf_counter() - started_at,
                        ad_account_id=effective_user_id,
                    )
                raise RateLimitError(
                    f"Local rate limit reached for '{path}'. Retry after {retry_after} seconds.",
                    retry_after=retry_after,
                    endpoint=path,
                    user_id=effective_user_id or "unknown",
                    reason="local_rate_limiter_throttle",
                    hint=(
                        "Reduce request rate or retry after the provided "
                        "retry_after value."
                    ),
                ) from exc
            except Exception as exc:
                logger.warning(
                    "local_rate_limiter_degraded path=%s user_id=%s error=%s "
                    "proceeding_without_local_rate_limit=true",
                    path,
                    effective_user_id or "unknown",
                    exc,
                )
                if self.observability_manager:
                    self.observability_manager.record_error_metrics(
                        error_type=f"RateLimiterDegraded:{type(exc).__name__}",
                        endpoint=path,
                        ad_account_id=effective_user_id,
                    )

        request_headers = {**(headers or {})}
        query = self._inject_auth(
            dict(params or {}),
            access_token=request_access_token,
            use_appsecret_proof=use_appsecret_proof,
        )

        request_kwargs: Dict[str, Any] = {}
        if files:
            request_kwargs["files"] = self._prepare_files(files)
            request_kwargs["data"] = body or {}
        else:
            if encoding == GraphBodyEncoding.JSON:
                if body is not None:
                    request_kwargs["json"] = body
            else:
                request_kwargs["data"] = body or {}

        async def _execute() -> Dict[str, Any]:
            nonlocal last_status_code
            response = await self._client.request(
                method,
                url=path,
                params=query,
                headers=request_headers,
                **request_kwargs,
            )
            last_status_code = response.status_code
            
            # Update rate limits from response headers
            if self.enable_rate_limiting and hasattr(response, 'headers'):
                self.rate_limiter.update_limits_from_headers(dict(response.headers))
            
            if response.is_success:
                result = response.json() if response.content else {}
                
                # Cache successful GET responses
                if (method == "GET" and self.enable_caching and 
                    cache_key_str is not None and result):
                    ttl = cache_ttl or cache_ttl_for_endpoint(path)
                    await self.cache_manager.set(cache_key_str, result, ttl)
                    logger.debug("Cached response", path=path, ttl=ttl)
                
                return result

            try:
                error_payload = response.json()
            except ValueError:
                error_payload = {"error": {"message": response.text}}

            raise map_meta_error(response.status_code, error_payload)

        try:
            trace_context = _noop_trace()
            if self.observability_manager and self.observability_manager.tracing_provider:
                trace_context = self.observability_manager.tracing_provider.trace_operation(
                    f"meta_api_{method.lower()}",
                    attributes={
                        "http.method": method,
                        "http.url": path,
                        "meta.user_id": effective_user_id or "unknown",
                    },
                )

            safe_body = redact_sensitive(dict(body or {})) if body else None
            logger.debug(
                "meta_request method=%s path=%s params=%s body=%s has_files=%s",
                method,
                path,
                redact_sensitive(dict(query)),
                safe_body,
                bool(files),
            )
            async def _retryable() -> Dict[str, Any]:
                return await async_retry(
                    _execute,
                    retries=3,
                    retry_exceptions=(MetaApiRateLimitError, httpx.TransportError),
                )

            async with trace_context:
                if self.circuit_breaker is not None:
                    result = await self.circuit_breaker.call(_retryable)
                else:
                    result = await _retryable()

            if self.observability_manager:
                self.observability_manager.record_api_metrics(
                    method=method,
                    endpoint=path,
                    status_code=last_status_code or 200,
                    duration=time.perf_counter() - started_at,
                    ad_account_id=effective_user_id,
                )
            return result
        except MetaApiError as exc:
            if self.enable_rate_limiting and rate_token_acquired:
                try:
                    await self.rate_limiter.release_token(path, effective_user_id)
                except Exception as release_exc:
                    logger.debug(
                        "rate_limit_release_failed path=%s error=%s",
                        path,
                        release_exc,
                    )
            if self.observability_manager:
                self.observability_manager.record_error_metrics(
                    error_type=type(exc).__name__,
                    endpoint=path,
                    ad_account_id=effective_user_id,
                )
                self.observability_manager.record_api_metrics(
                    method=method,
                    endpoint=path,
                    status_code=exc.status_code,
                    duration=time.perf_counter() - started_at,
                    ad_account_id=effective_user_id,
                )
            logger.error(
                "meta_api_error status=%s payload=%s",
                exc.status_code,
                exc.to_dict(),
            )
            raise
        except httpx.TransportError as exc:
            if self.enable_rate_limiting and rate_token_acquired:
                try:
                    await self.rate_limiter.release_token(path, effective_user_id)
                except Exception as release_exc:
                    logger.debug(
                        "rate_limit_release_failed path=%s error=%s",
                        path,
                        release_exc,
                    )
            if self.observability_manager:
                self.observability_manager.record_error_metrics(
                    error_type=type(exc).__name__,
                    endpoint=path,
                    ad_account_id=effective_user_id,
                )
                self.observability_manager.record_api_metrics(
                    method=method,
                    endpoint=path,
                    status_code=599,
                    duration=time.perf_counter() - started_at,
                    ad_account_id=effective_user_id,
            )
            logger.error("meta_transport_error path=%s error=%s", path, exc)
            raise
        except Exception:
            if self.enable_rate_limiting and rate_token_acquired:
                try:
                    await self.rate_limiter.release_token(path, effective_user_id)
                except Exception as release_exc:
                    logger.debug(
                        "rate_limit_release_failed path=%s error=%s",
                        path,
                        release_exc,
                    )
            raise

    async def get(
        self,
        path: str,
        *,
        params: Mapping[str, Any] | None = None,
        headers: Mapping[str, str] | None = None,
    ) -> Dict[str, Any]:
        return await self.request("GET", path, params=params, headers=headers)

    async def post(
        self,
        path: str,
        *,
        params: Mapping[str, Any] | None = None,
        body: Mapping[str, Any] | None = None,
        encoding: GraphBodyEncoding = GraphBodyEncoding.FORM,
        files: Sequence[GraphFile] | None = None,
        headers: Mapping[str, str] | None = None,
    ) -> Dict[str, Any]:
        return await self.request(
            "POST",
            path,
            params=params,
            body=body,
            encoding=encoding,
            files=files,
            headers=headers,
        )

    async def delete(
        self,
        path: str,
        *,
        params: Mapping[str, Any] | None = None,
        body: Mapping[str, Any] | None = None,
        encoding: GraphBodyEncoding = GraphBodyEncoding.FORM,
        headers: Mapping[str, str] | None = None,
    ) -> Dict[str, Any]:
        return await self.request(
            "DELETE",
            path,
            params=params,
            body=body,
            encoding=encoding,
            headers=headers,
        )

    async def graph_request(
        self,
        method: str,
        path: str,
        *,
        params: Mapping[str, Any] | None = None,
        body: Mapping[str, Any] | None = None,
        json: Mapping[str, Any] | None = None,
        encoding: GraphBodyEncoding = GraphBodyEncoding.FORM,
        files: Sequence[GraphFile] | None = None,
        headers: Mapping[str, str] | None = None,
        use_appsecret_proof: bool = True,
    ) -> Dict[str, Any]:
        """Backward-compatible Graph API helper used by legacy engines."""
        request_method = method.upper()
        request_body = body or json
        request_params: Mapping[str, Any] | None = params

        # Older call sites pass POST/PATCH payload in ``params``.
        if request_method != "GET" and request_body is None and params:
            request_body = dict(params)
            request_params = None

        return await self.request(
            request_method,
            path,
            params=request_params,
            body=request_body,
            encoding=encoding,
            files=files,
            headers=headers,
            use_appsecret_proof=use_appsecret_proof,
        )

    async def get_insights(
        self,
        *,
        object_id: str,
        level: str | None = None,
        fields: Sequence[str] | None = None,
        time_range: Mapping[str, str] | None = None,
        time_increment: str | int | None = None,
        breakdowns: Sequence[str] | None = None,
        action_breakdowns: Sequence[str] | None = None,
        filtering: Sequence[Mapping[str, Any]] | None = None,
        date_preset: str | None = None,
        limit: int | None = None,
        after: str | None = None,
        attribution_windows: Sequence[str] | None = None,
        sort: Sequence[str] | str | None = None,
        summary: bool | None = None,
        **extra_params: Any,
    ) -> Dict[str, Any]:
        """Backward-compatible insights helper used by legacy engines."""
        normalized_object_id = object_id.lstrip("/")
        if level == "account" and not normalized_object_id.startswith("act_"):
            normalized_object_id = f"act_{normalized_object_id}"
        path = f"{normalized_object_id}/insights"

        params: Dict[str, Any] = {}
        if level:
            params["level"] = level
        if fields:
            params["fields"] = ",".join(fields)
        if time_range:
            params["time_range"] = json.dumps(dict(time_range))
        if time_increment is not None:
            params["time_increment"] = str(time_increment)
        if breakdowns:
            params["breakdowns"] = ",".join(breakdowns)
        if action_breakdowns:
            params["action_breakdowns"] = ",".join(action_breakdowns)
        if filtering:
            params["filtering"] = json.dumps(list(filtering))
        if date_preset:
            params["date_preset"] = date_preset
        if limit is not None:
            params["limit"] = limit
        if after:
            params["after"] = after
        if attribution_windows:
            params["attribution_windows"] = ",".join(attribution_windows)
        if sort:
            if isinstance(sort, str):
                params["sort"] = sort
            else:
                params["sort"] = ",".join(sort)
        if summary is not None:
            params["summary"] = summary

        for key, value in extra_params.items():
            if value is None:
                continue
            if isinstance(value, (dict, list)):
                params[key] = json.dumps(value)
                continue
            if isinstance(value, (tuple, set)):
                params[key] = ",".join(str(item) for item in value)
                continue
            params[key] = value

        return await self.get(path, params=params)

    async def get_ad_account(
        self,
        *,
        ad_account_id: str,
        fields: Sequence[str] | None = None,
    ) -> Dict[str, Any]:
        """Backward-compatible ad-account fetch helper used by legacy engines."""
        normalized_account_id = ad_account_id.lstrip("/")
        if not normalized_account_id.startswith("act_"):
            normalized_account_id = f"act_{normalized_account_id}"

        params: Dict[str, Any] = {}
        if fields:
            params["fields"] = ",".join(fields)

        return await self.get(normalized_account_id, params=params or None)

    async def get_all(
        self,
        path: str,
        *,
        params: Mapping[str, Any] | None = None,
        limit: int = 10000,  # Safety limit for $10M+ accounts
        batch_size: int = 100,  # Optimal for API stability
    ) -> list[Dict[str, Any]]:
        """
        Utilities for high-scale fetching: Automatically iterates through headers/cursors
        to retrieve all data without manual pagination loops.
        """
        all_results: list[Dict[str, Any]] = []
        current_params = dict(params or {})
        current_params["limit"] = batch_size
        seen_page_markers: set[str] = set()
        
        while True:
            response = await self.get(path, params=current_params)
            data = response.get("data", [])
            if not isinstance(data, list):
                data = []
            all_results.extend(data)
            
            # Check safety limit
            if len(all_results) >= limit:
                logger.warning("get_all hit safety limit", limit=limit, path=path)
                break
            
            # Pagination Logic
            paging = response.get("paging") or {}
            cursors = paging.get("cursors") or {}
            after_cursor = cursors.get("after")
            next_url = paging.get("next")
            
            if after_cursor:
                marker = f"after:{after_cursor}"
                if marker in seen_page_markers:
                    logger.warning("get_all detected repeated after cursor", path=path, after=after_cursor)
                    break
                seen_page_markers.add(marker)
                current_params["after"] = after_cursor
                current_params.pop("offset", None)
                current_params.pop("__paging_token", None)
                continue

            if not next_url:
                break

            marker = f"next:{next_url}"
            if marker in seen_page_markers:
                logger.warning("get_all detected repeated next URL", path=path)
                break
            seen_page_markers.add(marker)

            next_params = self._extract_next_query_params(next_url)
            if not next_params:
                logger.warning("get_all could not derive paging params from next URL", path=path)
                break

            # Avoid conflicting stale pagination values between requests.
            for stale_key in ("after", "before", "offset", "__paging_token"):
                if stale_key not in next_params:
                    current_params.pop(stale_key, None)
            current_params.update(next_params)
            current_params["limit"] = batch_size
             
        return all_results

    @staticmethod
    def _extract_next_query_params(next_url: str) -> Dict[str, Any]:
        """Parse query params from Graph paging.next URL for follow-up requests."""
        parsed = urlparse(next_url)
        if not parsed.query:
            return {}

        parsed_query = parse_qs(parsed.query, keep_blank_values=False)
        next_params: Dict[str, Any] = {}
        for key, values in parsed_query.items():
            if key in {"access_token", "appsecret_proof"}:
                continue
            if not values:
                continue
            next_params[key] = values[-1]
        return next_params

    async def batch(
        self,
        operations: Iterable[Dict[str, Any]],
        include_headers: bool = False,
    ) -> "BatchResult":
        """Execute a batch request and return structured per-operation results.

        Returns a ``BatchResult`` with ``.succeeded``, ``.failed``, and
        ``.all`` attributes for easy inspection of partial failures.
        """
        ops_list = list(operations)
        payload: Dict[str, Any] = {"batch": json.dumps(ops_list)}
        if include_headers:
            payload["include_headers"] = True
        raw = await self.post("", body=payload, encoding=GraphBodyEncoding.FORM)
        return BatchResult.from_response(raw, ops_list)


class BatchResult:
    """Structured result from a Meta Batch API call."""

    __slots__ = ("all", "succeeded", "failed", "raw")

    def __init__(
        self,
        all_results: list[Dict[str, Any]],
        succeeded: list[Dict[str, Any]],
        failed: list[Dict[str, Any]],
        raw: Any,
    ) -> None:
        self.all = all_results
        self.succeeded = succeeded
        self.failed = failed
        self.raw = raw

    @property
    def has_failures(self) -> bool:
        return len(self.failed) > 0

    def to_dict(self) -> Dict[str, Any]:
        """Return a JSON-serializable representation."""
        return {
            "all": self.all,
            "succeeded": self.succeeded,
            "failed": self.failed,
            "raw": self.raw,
            "has_failures": self.has_failures,
        }

    @classmethod
    def from_response(
        cls,
        raw: Any,
        operations: list[Dict[str, Any]] | None = None,
    ) -> "BatchResult":
        items: list[Dict[str, Any]] = raw if isinstance(raw, list) else []
        succeeded: list[Dict[str, Any]] = []
        failed: list[Dict[str, Any]] = []

        for idx, item in enumerate(items):
            code = item.get("code", 0)
            body_str = item.get("body", "{}")
            try:
                body = json.loads(body_str) if isinstance(body_str, str) else body_str
            except (json.JSONDecodeError, TypeError):
                body = {"raw_body": body_str}

            entry: Dict[str, Any] = {
                "index": idx,
                "code": code,
                "body": body,
            }
            if operations and idx < len(operations):
                entry["operation"] = operations[idx]

            if 200 <= code < 300:
                succeeded.append(entry)
            else:
                entry["error"] = body.get("error", {}) if isinstance(body, dict) else {}
                failed.append(entry)

        return cls(all_results=succeeded + failed, succeeded=succeeded, failed=failed, raw=raw)

    async def health_check(
        self,
        *,
        ad_account_id: str | None = None,
    ) -> Dict[str, Any]:
        """Call a lightweight endpoint to ensure credentials work."""

        if ad_account_id:
            path = f"{ad_account_id}/campaigns"
            params: Dict[str, Any] = {"limit": 1, "fields": "id"}
        else:
            path = "me/businesses"
            params = {"limit": 1, "fields": "id"}
        return await self.get(path, params=params)


def _env_bool(name: str, default: bool) -> bool:
    value = os.getenv(name)
    if value is None:
        return default
    return value.strip().lower() in {"1", "true", "yes", "on"}


async def build_client_from_env(settings: "Settings | None" = None) -> MetaClient:
    """Instantiate a MetaClient using environment variables."""

    if settings is not None:
        hosted_mode = settings.hosted_mode
        app_secret = settings.meta_app_secret
        timeout = settings.meta_api_timeout_seconds
        max_conn = settings.meta_max_concurrency
        api_version = settings.meta_api_version
        base_url = settings.meta_graph_base_url
        enable_caching = settings.cache_enabled
        enable_rate_limiting = settings.rate_limiting_enabled
        auth_broker_url = settings.auth_broker_url
        auth_broker_api_key = settings.auth_broker_api_key
        auth_broker_timeout = settings.auth_broker_timeout_seconds
        token_cache_path = (
            Path(os.path.expanduser(settings.meta_token_cache_path))
            if settings.meta_token_cache_path
            else None
        )
    else:
        hosted_mode = _env_bool("HOSTED_MODE", False)
        app_secret = os.getenv("META_APP_SECRET")
        timeout = float(os.getenv("META_API_TIMEOUT_SECONDS", "30"))
        max_conn = int(os.getenv("META_MAX_CONCURRENCY", "10"))
        api_version = os.getenv("META_API_VERSION", "v24.0")
        base_url = os.getenv("META_GRAPH_BASE_URL", "https://graph.facebook.com")
        enable_caching = _env_bool("CACHE_ENABLED", False)
        enable_rate_limiting = _env_bool("RATE_LIMITING_ENABLED", True)
        auth_broker_url = os.getenv("AUTH_BROKER_URL")
        auth_broker_api_key = os.getenv("AUTH_BROKER_API_KEY")
        auth_broker_timeout = float(os.getenv("AUTH_BROKER_TIMEOUT_SECONDS", "10"))
        token_cache_path = None

    token_provider: "TokenProvider | None" = None
    if hosted_mode:
        from .auth.provider import TokenProvider

        token_provider = TokenProvider(
            auth_broker_url=auth_broker_url,
            admin_api_key=auth_broker_api_key,
            timeout_seconds=auth_broker_timeout,
            hosted_mode=True,
        )
        access_token = os.getenv("META_ACCESS_TOKEN", "hosted-mode-placeholder-token")
    else:
        access_token = await ensure_access_token(api_version, cache_path=token_cache_path)

    return MetaClient(
        access_token=access_token,
        app_secret=app_secret,
        token_provider=token_provider,
        timeout=timeout,
        max_connections=max_conn,
        version=api_version,
        base_url=base_url,
        enable_caching=enable_caching,
        enable_rate_limiting=enable_rate_limiting,
    )


# Global client instance for singleton pattern
_global_client: Optional[MetaClient] = None


def set_client(client: MetaClient) -> None:
    """Set the global MetaClient instance."""
    global _global_client
    _global_client = client


def get_client() -> MetaClient:
    """Get the global MetaClient instance."""
    if _global_client is None:
        raise RuntimeError(
            "MetaClient not initialized. Call set_client() first or use build_client_from_env()."
        )
    return _global_client
