from __future__ import annotations

import asyncio
import importlib
import logging
import os
import threading
import time
import uuid
from datetime import datetime, timezone
from dataclasses import dataclass
from typing import Any, Dict, Literal, Optional, TypeVar

import httpx

from .adapters import LLMAdapter, OpenAIAdapter
from ._expected_languages import normalize_expected_languages
from .errors import DependencyError, SecurityBlockError
from .guard import (
    ActionTaken,
    CapabilityFlags,
    LocalPolicyBlockError,
    LocalSecurityEnforcer,
    StrictPiiStreamingError,
    ViolationType,
    build_security_policy_violation_metadata,
    ensure_capability_flags_async,
    ensure_capability_flags_sync,
    get_cached_capability_flags,
)
from .security import get_injection_scanner
from .types import GuardParams, GuardResponse, LogParams
from .pii import PIIManager

logger = logging.getLogger("agentid")

TClient = TypeVar("TClient")

_OPENAI_VERSION_CHECKED = False
_OPENAI_VERSION_LOCK = threading.Lock()
AGENTID_SDK_VERSION_HEADER = "py-1.1.0"
TRANSPARENCY_DISCLOSURE = "You are interacting with an AI."
TRANSPARENCY_ARTICLE = "EU_AI_ACT_ARTICLE_50"
TRANSPARENCY_INJECTION_MODE = "deterministic"


def _assert_openai_v1_or_higher() -> None:
    global _OPENAI_VERSION_CHECKED

    if _OPENAI_VERSION_CHECKED:
        return

    with _OPENAI_VERSION_LOCK:
        if _OPENAI_VERSION_CHECKED:
            return

        try:
            openai_module = importlib.import_module("openai")
        except Exception:
            # If openai is not installed yet, we defer to caller-side import/runtime behavior.
            _OPENAI_VERSION_CHECKED = True
            return

        version = str(getattr(openai_module, "__version__", "")).strip()
        if version.startswith("0."):
            raise RuntimeError(
                "AgentID requires openai v1.0.0+. Run 'pip install -U openai'."
            )

        _OPENAI_VERSION_CHECKED = True


def _normalize_base_url(base_url: str) -> str:
    return base_url.rstrip("/")


def _normalize_timeout_seconds(value: float, default: float) -> float:
    try:
        timeout = float(value)
    except (TypeError, ValueError):
        return default
    if timeout <= 0:
        return default
    return timeout


def _normalize_failure_mode(
    value: Optional[str],
) -> Optional[Literal["fail_open", "fail_close"]]:
    if value is None:
        return None
    normalized = value.strip().lower()
    if normalized in {"fail_open", "fail_close"}:
        return normalized  # type: ignore[return-value]
    raise ValueError("AgentID failure_mode must be 'fail_open' or 'fail_close'.")


def _resolve_initial_api_key(api_key: Optional[str]) -> str:
    explicit = api_key.strip() if isinstance(api_key, str) else ""
    from_env = (os.getenv("AGENTID_API_KEY") or "").strip()
    resolved = explicit or from_env
    if not resolved:
        raise ValueError("AgentID API key missing. Pass api_key or set AGENTID_API_KEY.")
    return resolved


def _is_infrastructure_guard_reason(reason: Optional[str]) -> bool:
    return reason in {
        "system_failure",
        "system_failure_db_unavailable",
        "logging_failed",
        "server_error",
        "guard_unreachable",
        "api_key_pepper_missing",
        "encryption_key_missing",
    }


def _is_fail_close_ingest_reason(reason: Optional[str]) -> bool:
    return reason in {
        "system_failure",
        "system_failure_db_unavailable",
        "server_error",
        "redis_rate_limit_unavailable",
        "redis_quota_unavailable",
        "redis_token_quota_unavailable",
    }


def _should_raise_ingest_dependency_error(
    *,
    strict_mode: bool,
    status_code: Optional[int],
    reason: Optional[str],
) -> bool:
    if not strict_mode:
        return False
    if status_code is None:
        return True
    if status_code >= 500:
        return True
    return _is_fail_close_ingest_reason(reason)


def _is_guard_failure_eligible_for_local_fallback(reason: Optional[str]) -> bool:
    return bool(
        reason == "network_error_strict_mode"
        or reason == "server_error"
        or _is_infrastructure_guard_reason(reason)
    )


def _duration_ms(started_at: float) -> int:
    return max(0, int((time.perf_counter() - started_at) * 1000))


def _set_finite_duration_metadata(
    metadata: Dict[str, Any], key: str, value: Optional[int]
) -> None:
    if value is None:
        return
    try:
        coerced = int(value)
    except (TypeError, ValueError):
        return
    if coerced < 0:
        return
    metadata[key] = coerced


def _build_sdk_timing_metadata(
    *,
    sdk_config_fetch_ms: Optional[int] = None,
    sdk_local_scan_ms: Optional[int] = None,
    sdk_guard_ms: Optional[int] = None,
    sdk_ingest_ms: Optional[int] = None,
) -> Dict[str, Any]:
    metadata: Dict[str, Any] = {}
    _set_finite_duration_metadata(metadata, "sdk_config_fetch_ms", sdk_config_fetch_ms)
    _set_finite_duration_metadata(metadata, "sdk_local_scan_ms", sdk_local_scan_ms)
    _set_finite_duration_metadata(metadata, "sdk_guard_ms", sdk_guard_ms)
    _set_finite_duration_metadata(metadata, "sdk_ingest_ms", sdk_ingest_ms)
    return metadata


def _is_effective_strict_mode(
    config: "_ClientConfig",
    capability_flags: Optional[CapabilityFlags] = None,
) -> bool:
    return bool(
        config.strict_mode
        or config.failure_mode == "fail_close"
        or bool(getattr(capability_flags, "strict_security_mode", False))
        or getattr(capability_flags, "failure_mode", None) == "fail_close"
    )


def _utc_now_iso() -> str:
    return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")


def _coerce_uuid_str(value: Any) -> Optional[str]:
    if not isinstance(value, str):
        return None
    candidate = value.strip()
    if not candidate:
        return None
    try:
        return str(uuid.UUID(candidate))
    except Exception:
        return None


def _coerce_transparency_metadata(value: Any) -> Optional[Dict[str, Any]]:
    if not isinstance(value, dict):
        return None
    if value.get("is_ai_generated") is not True:
        return None
    if value.get("disclosure") != TRANSPARENCY_DISCLOSURE:
        return None
    if value.get("article") != TRANSPARENCY_ARTICLE:
        return None
    if value.get("injection_mode") != TRANSPARENCY_INJECTION_MODE:
        return None
    return {
        "is_ai_generated": True,
        "disclosure": TRANSPARENCY_DISCLOSURE,
        "article": TRANSPARENCY_ARTICLE,
        "injection_mode": TRANSPARENCY_INJECTION_MODE,
    }


def _attach_transparency_metadata(target: Any, transparency: Optional[Dict[str, Any]]) -> Any:
    if not transparency:
        return target
    if isinstance(target, dict):
        target["agentid_transparency"] = transparency
        return target
    try:
        setattr(target, "agentid_transparency", transparency)
    except Exception:
        pass
    return target


def _headers(api_key: str, correlation_id: Optional[str] = None) -> Dict[str, str]:
    headers = {
        "Content-Type": "application/json",
        "x-agentid-api-key": api_key,
        "X-AgentID-SDK-Version": AGENTID_SDK_VERSION_HEADER,
    }
    coerced = _coerce_uuid_str(correlation_id)
    if coerced is not None:
        headers["x-correlation-id"] = coerced
    return headers


def _resolve_ingest_correlation_event_id(
    event_id: Optional[str], metadata: Optional[Dict[str, Any]]
) -> str:
    payload_event_id = _coerce_uuid_str(event_id) or str(uuid.uuid4())
    metadata_client_event_id = (
        _coerce_uuid_str((metadata or {}).get("client_event_id"))
        if isinstance(metadata, dict)
        else None
    )
    return metadata_client_event_id or payload_event_id


def _build_client_capabilities(
    *,
    pii_masking_enabled: bool,
    framework: str = "python_sdk",
    has_feedback_handler: bool = False,
) -> Dict[str, Any]:
    return {
        "capabilities": {
            "has_feedback_handler": bool(has_feedback_handler),
            "pii_masking_enabled": bool(pii_masking_enabled),
            "framework": framework.strip()[:64] if framework else "python_sdk",
        }
    }


@dataclass(frozen=True)
class _ClientConfig:
    api_key: str
    base_url: str
    guard_timeout_s: float
    ingest_timeout_s: float
    strict_mode: bool
    failure_mode: Optional[Literal["fail_open", "fail_close"]]
    pii_masking: Optional[bool]
    check_injection: bool
    ai_scan_enabled: bool
    store_pii: bool
    client_fast_fail: bool


def _mask_openai_messages(messages: Any, masked_text: str) -> Any:
    if not isinstance(messages, list):
        return messages

    last_user_idx: Optional[int] = None
    for idx, msg in enumerate(messages):
        if isinstance(msg, dict) and msg.get("role") == "user":
            last_user_idx = idx

    if last_user_idx is None:
        return messages

    new_messages = list(messages)
    last = new_messages[last_user_idx]
    if isinstance(last, dict):
        new_last = dict(last)
        new_last["content"] = masked_text
        new_messages[last_user_idx] = new_last
    return new_messages


def _with_masked_openai_request(
    args: tuple[Any, ...], kwargs: Dict[str, Any], masked_text: str
) -> tuple[tuple[Any, ...], Dict[str, Any]]:
    if args and isinstance(args[0], dict):
        req = dict(args[0])
        req["messages"] = _mask_openai_messages(req.get("messages"), masked_text)
        return (req,) + args[1:], kwargs

    if "messages" in kwargs:
        new_kwargs = dict(kwargs)
        new_kwargs["messages"] = _mask_openai_messages(kwargs.get("messages"), masked_text)
        return args, new_kwargs

    return args, kwargs


def _deanonymize_openai_response(
    response: Any, pii_manager: PIIManager, mapping: Dict[str, str]
) -> None:
    if not mapping:
        return

    try:
        choices = getattr(response, "choices", None)
        if not isinstance(choices, list):
            return

        for choice in choices:
            if isinstance(choice, dict):
                message = choice.get("message")
                if isinstance(message, dict) and isinstance(message.get("content"), str):
                    message["content"] = pii_manager.deanonymize(message["content"], mapping)
                delta = choice.get("delta")
                if isinstance(delta, dict) and isinstance(delta.get("content"), str):
                    delta["content"] = pii_manager.deanonymize(delta["content"], mapping)
                continue

            message = getattr(choice, "message", None)
            if message is not None:
                content = getattr(message, "content", None)
                if isinstance(content, str):
                    setattr(message, "content", pii_manager.deanonymize(content, mapping))

            delta = getattr(choice, "delta", None)
            if delta is not None:
                delta_content = getattr(delta, "content", None)
                if isinstance(delta_content, str):
                    setattr(delta, "content", pii_manager.deanonymize(delta_content, mapping))
    except Exception:
        # Fail-safe: never break completion flow due to post-processing.
        return


class _CompletionsCreateSyncWrapper:
    def __init__(
        self,
        agent: "AgentID",
        system_id: str,
        user_id: Optional[str],
        completions_obj: Any,
        adapter: LLMAdapter,
        api_key_override: Optional[str] = None,
        expected_languages: Optional[list[str]] = None,
        request_identity: Optional[Dict[str, Any]] = None,
    ):
        self._agent = agent
        self._system_id = system_id
        self._user_id = user_id
        self._completions = completions_obj
        self._adapter = adapter
        self._api_key_override = api_key_override.strip() if api_key_override else None
        self._expected_languages = expected_languages
        self._request_identity = request_identity

    def __getattr__(self, name: str) -> Any:
        return getattr(self._completions, name)

    def create(self, *args: Any, **kwargs: Any) -> Any:
        req = kwargs if not (args and isinstance(args[0], dict)) else args[0]
        stream = bool(req.get("stream")) if isinstance(req, dict) else bool(kwargs.get("stream"))
        effective_api_key = self._agent._resolve_api_key(self._api_key_override)

        user_text = self._adapter.extract_input(args, kwargs)
        capability_flags = self._agent._get_cached_capability_flags(api_key=effective_api_key)
        sdk_config_fetch_ms = 0
        sdk_local_scan_ms = 0

        if user_text:
            prepared = self._agent._prepare_input_for_dispatch(
                input_text=user_text,
                system_id=self._system_id,
                stream=stream,
                api_key=effective_api_key,
                client_event_id=None,
            )
            capability_flags = prepared["capability_flags"]
            sdk_config_fetch_ms = int(prepared.get("sdk_config_fetch_ms") or 0)
            sdk_local_scan_ms = int(prepared.get("sdk_local_scan_ms") or 0)

        masked_text = user_text
        pii_mapping: Dict[str, str] = {}
        should_deanonymize = False
        if user_text:
            masked_text = str(prepared.get("sanitized_input") or user_text)
            if masked_text != user_text:
                args, kwargs = _with_masked_openai_request(args, kwargs, masked_text)

            effective_pii_masking = self._agent._resolve_effective_pii_masking(
                capability_flags=capability_flags
            )
            if capability_flags.block_pii_leakage is False and effective_pii_masking:
                if stream:
                    logger.warning(
                        "AgentID: PII masking is disabled for streaming responses."
                    )
                else:
                    masked_text, pii_mapping = self._agent._get_pii_manager().anonymize(user_text)
                    should_deanonymize = bool(pii_mapping)
                    args, kwargs = _with_masked_openai_request(args, kwargs, masked_text)

        if user_text is None:
            raise ValueError(
                "AgentID: No user message found. Security guard requires string input."
            )

        request_client_event_id = str(uuid.uuid4())
        requested_model = self._adapter.get_model_name(args, kwargs)
        effective_strict_mode = _is_effective_strict_mode(
            self._agent._config, capability_flags
        )
        guard_started_at = time.perf_counter()
        verdict = self._agent.guard(
            input=masked_text or "",
            system_id=self._system_id,
            model=requested_model,
            user_id=self._user_id,
            client_event_id=request_client_event_id,
            expected_languages=self._expected_languages,
            request_identity=self._request_identity,
            api_key=effective_api_key,
            client_capabilities=self._agent._default_client_capabilities(
                framework="openai",
                capability_flags=capability_flags,
            ),
        )
        sdk_guard_ms = (
            int(verdict.get("guard_latency_ms"))
            if isinstance(verdict.get("guard_latency_ms"), int)
            else _duration_ms(guard_started_at)
        )
        local_fallback_applied = False
        local_fallback_reason: Optional[str] = None
        if not verdict.get("allowed", True):
            if (
                effective_strict_mode
                and _is_guard_failure_eligible_for_local_fallback(verdict.get("reason"))
            ):
                local_fallback_applied = True
                local_fallback_reason = str(
                    verdict.get("reason") or "guard_unreachable"
                )
                if sdk_local_scan_ms == 0:
                    fallback = self._agent._apply_local_fallback_for_guard_failure(
                        input_text=masked_text or "",
                        system_id=self._system_id,
                        stream=stream,
                        api_key=effective_api_key,
                        client_event_id=request_client_event_id,
                        capability_flags=capability_flags,
                        sdk_config_fetch_ms=sdk_config_fetch_ms,
                    )
                    masked_text = fallback["sanitized_input"]
                    capability_flags = fallback["capability_flags"]
                    sdk_config_fetch_ms = int(
                        fallback.get("sdk_config_fetch_ms") or sdk_config_fetch_ms
                    )
                    sdk_local_scan_ms = int(fallback.get("sdk_local_scan_ms") or 0)
            else:
                raise SecurityBlockError(verdict.get("reason"))
        current_request_input = self._adapter.extract_input(args, kwargs)
        if masked_text != current_request_input:
            args, kwargs = _with_masked_openai_request(args, kwargs, masked_text or "")
        canonical_client_event_id = (
            _coerce_uuid_str(verdict.get("client_event_id")) or request_client_event_id
        )
        guard_event_id = verdict.get("guard_event_id")
        guard_event_id_str = guard_event_id if isinstance(guard_event_id, str) and guard_event_id else None
        transparency = _coerce_transparency_metadata(verdict.get("transparency"))

        transformed_input = verdict.get("transformed_input")
        if isinstance(transformed_input, str) and transformed_input:
            if transformed_input != (masked_text or ""):
                masked_text = transformed_input
                args, kwargs = _with_masked_openai_request(args, kwargs, transformed_input)

        if stream:
            logger.warning(
                "AgentID: Automatic logging for streaming responses is not yet supported."
            )
            stream_response = self._completions.create(*args, **kwargs)
            return _attach_transparency_metadata(stream_response, transparency)

        start = time.perf_counter()
        resp = self._completions.create(*args, **kwargs)
        duration_ms = int((time.perf_counter() - start) * 1000)

        if masked_text is not None:
            usage = self._adapter.get_token_usage(resp)
            model = self._adapter.get_model_name(args, kwargs, resp) or "unknown"
            output_text = self._adapter.extract_output(resp)
            strict_for_ingest = _is_effective_strict_mode(
                self._agent._config, capability_flags
            )
            metadata: Dict[str, Any] = {
                "client_event_id": canonical_client_event_id,
                "guard_event_id": guard_event_id_str,
                "transparency": transparency,
                **_build_sdk_timing_metadata(
                    sdk_config_fetch_ms=sdk_config_fetch_ms,
                    sdk_local_scan_ms=sdk_local_scan_ms,
                    sdk_guard_ms=sdk_guard_ms,
                ),
            }
            if local_fallback_applied:
                metadata["sdk_local_fallback_applied"] = True
                metadata["sdk_local_fallback_reason"] = local_fallback_reason
            self._agent.log(
                system_id=self._system_id,
                user_id=self._user_id,
                request_identity=self._request_identity,
                input=masked_text,
                output=output_text,
                event_id=canonical_client_event_id,
                model=str(model),
                usage=usage,
                metadata=metadata,
                latency=duration_ms,
                api_key=effective_api_key,
                client_capabilities=self._agent._default_client_capabilities(
                    framework="openai",
                    capability_flags=capability_flags,
                ),
                raise_on_failure=strict_for_ingest,
            )

        if (
            capability_flags.block_pii_leakage is False
            and self._agent._resolve_effective_pii_masking(capability_flags=capability_flags)
            and should_deanonymize
        ):
            _deanonymize_openai_response(resp, self._agent._get_pii_manager(), pii_mapping)

        return _attach_transparency_metadata(resp, transparency)


class _ChatSyncWrapper:
    def __init__(
        self,
        agent: "AgentID",
        system_id: str,
        user_id: Optional[str],
        chat_obj: Any,
        adapter: LLMAdapter,
        api_key_override: Optional[str] = None,
        expected_languages: Optional[list[str]] = None,
        request_identity: Optional[Dict[str, Any]] = None,
    ):
        self._chat = chat_obj
        self._completions_wrapper = _CompletionsCreateSyncWrapper(
            agent=agent,
            system_id=system_id,
            user_id=user_id,
            completions_obj=getattr(chat_obj, "completions", None),
            adapter=adapter,
            api_key_override=api_key_override,
            expected_languages=expected_languages,
            request_identity=request_identity,
        )

    def __getattr__(self, name: str) -> Any:
        if name == "completions":
            return self._completions_wrapper
        return getattr(self._chat, name)


class SyncOpenAIWrapper:
    """
    Transparent proxy for openai.OpenAI that intercepts chat.completions.create.
    """

    def __init__(
        self,
        agent: "AgentID",
        client: Any,
        system_id: str,
        user_id: Optional[str],
        adapter: LLMAdapter,
        api_key_override: Optional[str] = None,
        expected_languages: Optional[list[str]] = None,
        request_identity: Optional[Dict[str, Any]] = None,
    ):
        self._agent = agent
        self._client = client
        self._system_id = system_id
        self._user_id = user_id
        self._adapter = adapter
        self._api_key_override = api_key_override.strip() if api_key_override else None
        self._expected_languages = expected_languages
        self._request_identity = request_identity

    def __getattr__(self, name: str) -> Any:
        if name == "chat":
            return _ChatSyncWrapper(
                self._agent,
                self._system_id,
                self._user_id,
                getattr(self._client, "chat"),
                self._adapter,
                api_key_override=self._api_key_override,
                expected_languages=self._expected_languages,
                request_identity=self._request_identity,
            )
        return getattr(self._client, name)


class _CompletionsCreateAsyncWrapper:
    def __init__(
        self,
        agent: "AsyncAgentID",
        system_id: str,
        user_id: Optional[str],
        completions_obj: Any,
        adapter: LLMAdapter,
        api_key_override: Optional[str] = None,
        expected_languages: Optional[list[str]] = None,
        request_identity: Optional[Dict[str, Any]] = None,
    ):
        self._agent = agent
        self._system_id = system_id
        self._user_id = user_id
        self._completions = completions_obj
        self._adapter = adapter
        self._api_key_override = api_key_override.strip() if api_key_override else None
        self._expected_languages = expected_languages
        self._request_identity = request_identity

    def __getattr__(self, name: str) -> Any:
        return getattr(self._completions, name)

    async def create(self, *args: Any, **kwargs: Any) -> Any:
        req = kwargs if not (args and isinstance(args[0], dict)) else args[0]
        stream = bool(req.get("stream")) if isinstance(req, dict) else bool(kwargs.get("stream"))
        effective_api_key = self._agent._resolve_api_key(self._api_key_override)

        user_text = self._adapter.extract_input(args, kwargs)
        capability_flags = self._agent._get_cached_capability_flags(api_key=effective_api_key)
        sdk_config_fetch_ms = 0
        sdk_local_scan_ms = 0

        if user_text:
            prepared = await self._agent._prepare_input_for_dispatch_async(
                input_text=user_text,
                system_id=self._system_id,
                stream=stream,
                api_key=effective_api_key,
                client_event_id=None,
            )
            capability_flags = prepared["capability_flags"]
            sdk_config_fetch_ms = int(prepared.get("sdk_config_fetch_ms") or 0)
            sdk_local_scan_ms = int(prepared.get("sdk_local_scan_ms") or 0)

        masked_text = user_text
        pii_mapping: Dict[str, str] = {}
        should_deanonymize = False
        if user_text:
            masked_text = str(prepared.get("sanitized_input") or user_text)
            if masked_text != user_text:
                args, kwargs = _with_masked_openai_request(args, kwargs, masked_text)

            effective_pii_masking = self._agent._resolve_effective_pii_masking(
                capability_flags=capability_flags
            )
            if capability_flags.block_pii_leakage is False and effective_pii_masking:
                if stream:
                    logger.warning(
                        "AgentID: PII masking is disabled for streaming responses."
                    )
                else:
                    masked_text, pii_mapping = self._agent._get_pii_manager().anonymize(user_text)
                    should_deanonymize = bool(pii_mapping)
                    args, kwargs = _with_masked_openai_request(args, kwargs, masked_text)

        if user_text is None:
            raise ValueError(
                "AgentID: No user message found. Security guard requires string input."
            )

        request_client_event_id = str(uuid.uuid4())
        requested_model = self._adapter.get_model_name(args, kwargs)
        effective_strict_mode = _is_effective_strict_mode(
            self._agent._config, capability_flags
        )
        guard_started_at = time.perf_counter()
        verdict = await self._agent.guard(
            input=masked_text or "",
            system_id=self._system_id,
            model=requested_model,
            user_id=self._user_id,
            client_event_id=request_client_event_id,
            expected_languages=self._expected_languages,
            request_identity=self._request_identity,
            api_key=effective_api_key,
            client_capabilities=self._agent._default_client_capabilities(
                framework="openai",
                capability_flags=capability_flags,
            ),
        )
        sdk_guard_ms = (
            int(verdict.get("guard_latency_ms"))
            if isinstance(verdict.get("guard_latency_ms"), int)
            else _duration_ms(guard_started_at)
        )
        local_fallback_applied = False
        local_fallback_reason: Optional[str] = None
        if not verdict.get("allowed", True):
            if (
                effective_strict_mode
                and _is_guard_failure_eligible_for_local_fallback(verdict.get("reason"))
            ):
                local_fallback_applied = True
                local_fallback_reason = str(
                    verdict.get("reason") or "guard_unreachable"
                )
                if sdk_local_scan_ms == 0:
                    fallback = await self._agent._apply_local_fallback_for_guard_failure_async(
                        input_text=masked_text or "",
                        system_id=self._system_id,
                        stream=stream,
                        api_key=effective_api_key,
                        client_event_id=request_client_event_id,
                        capability_flags=capability_flags,
                        sdk_config_fetch_ms=sdk_config_fetch_ms,
                    )
                    masked_text = fallback["sanitized_input"]
                    capability_flags = fallback["capability_flags"]
                    sdk_config_fetch_ms = int(
                        fallback.get("sdk_config_fetch_ms") or sdk_config_fetch_ms
                    )
                    sdk_local_scan_ms = int(fallback.get("sdk_local_scan_ms") or 0)
            else:
                raise SecurityBlockError(verdict.get("reason"))
        current_request_input = self._adapter.extract_input(args, kwargs)
        if masked_text != current_request_input:
            args, kwargs = _with_masked_openai_request(args, kwargs, masked_text or "")
        canonical_client_event_id = (
            _coerce_uuid_str(verdict.get("client_event_id")) or request_client_event_id
        )
        guard_event_id = verdict.get("guard_event_id")
        guard_event_id_str = guard_event_id if isinstance(guard_event_id, str) and guard_event_id else None
        transparency = _coerce_transparency_metadata(verdict.get("transparency"))

        transformed_input = verdict.get("transformed_input")
        if isinstance(transformed_input, str) and transformed_input:
            if transformed_input != (masked_text or ""):
                masked_text = transformed_input
                args, kwargs = _with_masked_openai_request(args, kwargs, transformed_input)

        if stream:
            logger.warning(
                "AgentID: Automatic logging for streaming responses is not yet supported."
            )
            stream_response = await self._completions.create(*args, **kwargs)
            return _attach_transparency_metadata(stream_response, transparency)

        start = time.perf_counter()
        resp = await self._completions.create(*args, **kwargs)
        duration_ms = int((time.perf_counter() - start) * 1000)

        if masked_text is not None:
            usage = self._adapter.get_token_usage(resp)
            model = self._adapter.get_model_name(args, kwargs, resp) or "unknown"
            output_text = self._adapter.extract_output(resp)
            strict_for_ingest = _is_effective_strict_mode(
                self._agent._config, capability_flags
            )
            metadata: Dict[str, Any] = {
                "client_event_id": canonical_client_event_id,
                "guard_event_id": guard_event_id_str,
                "transparency": transparency,
                **_build_sdk_timing_metadata(
                    sdk_config_fetch_ms=sdk_config_fetch_ms,
                    sdk_local_scan_ms=sdk_local_scan_ms,
                    sdk_guard_ms=sdk_guard_ms,
                ),
            }
            if local_fallback_applied:
                metadata["sdk_local_fallback_applied"] = True
                metadata["sdk_local_fallback_reason"] = local_fallback_reason
            if strict_for_ingest:
                await self._agent.log_async(
                    system_id=self._system_id,
                    user_id=self._user_id,
                    request_identity=self._request_identity,
                    input=masked_text,
                    output=output_text,
                    event_id=canonical_client_event_id,
                    model=str(model),
                    usage=usage,
                    metadata=metadata,
                    latency=duration_ms,
                    api_key=effective_api_key,
                    client_capabilities=self._agent._default_client_capabilities(
                        framework="openai",
                        capability_flags=capability_flags,
                    ),
                    raise_on_failure=True,
                )
            else:
                self._agent.log(
                    system_id=self._system_id,
                    user_id=self._user_id,
                    request_identity=self._request_identity,
                    input=masked_text,
                    output=output_text,
                    event_id=canonical_client_event_id,
                    model=str(model),
                    usage=usage,
                    metadata=metadata,
                    latency=duration_ms,
                    api_key=effective_api_key,
                    client_capabilities=self._agent._default_client_capabilities(
                        framework="openai",
                        capability_flags=capability_flags,
                    ),
                )

        if (
            capability_flags.block_pii_leakage is False
            and self._agent._resolve_effective_pii_masking(capability_flags=capability_flags)
            and should_deanonymize
        ):
            _deanonymize_openai_response(resp, self._agent._get_pii_manager(), pii_mapping)

        return _attach_transparency_metadata(resp, transparency)


class _ChatAsyncWrapper:
    def __init__(
        self,
        agent: "AsyncAgentID",
        system_id: str,
        user_id: Optional[str],
        chat_obj: Any,
        adapter: LLMAdapter,
        api_key_override: Optional[str] = None,
        expected_languages: Optional[list[str]] = None,
        request_identity: Optional[Dict[str, Any]] = None,
    ):
        self._chat = chat_obj
        self._completions_wrapper = _CompletionsCreateAsyncWrapper(
            agent=agent,
            system_id=system_id,
            user_id=user_id,
            completions_obj=getattr(chat_obj, "completions", None),
            adapter=adapter,
            api_key_override=api_key_override,
            expected_languages=expected_languages,
            request_identity=request_identity,
        )

    def __getattr__(self, name: str) -> Any:
        if name == "completions":
            return self._completions_wrapper
        return getattr(self._chat, name)


class AsyncOpenAIWrapper:
    """
    Transparent proxy for openai.AsyncOpenAI that intercepts chat.completions.create.
    """

    def __init__(
        self,
        agent: "AsyncAgentID",
        client: Any,
        system_id: str,
        user_id: Optional[str],
        adapter: LLMAdapter,
        api_key_override: Optional[str] = None,
        expected_languages: Optional[list[str]] = None,
        request_identity: Optional[Dict[str, Any]] = None,
    ):
        self._agent = agent
        self._client = client
        self._system_id = system_id
        self._user_id = user_id
        self._adapter = adapter
        self._api_key_override = api_key_override.strip() if api_key_override else None
        self._expected_languages = expected_languages
        self._request_identity = request_identity

    def __getattr__(self, name: str) -> Any:
        if name == "chat":
            return _ChatAsyncWrapper(
                self._agent,
                self._system_id,
                self._user_id,
                getattr(self._client, "chat"),
                self._adapter,
                api_key_override=self._api_key_override,
                expected_languages=self._expected_languages,
                request_identity=self._request_identity,
            )
        return getattr(self._client, name)


def _is_openai_client(obj: Any, expected_name: str) -> bool:
    cls = obj.__class__
    return cls.__name__ == expected_name and (cls.__module__ or "").startswith("openai")


class AgentID:
    """
    Synchronous AgentID client.

    - guard(): blocking call (default fail-open on connectivity failure)
    - log(): fire-and-forget telemetry via background thread
    """

    def __init__(
        self,
        api_key: Optional[str] = None,
        base_url: str = "https://app.getagentid.com/api/v1",
        pii_masking: Optional[bool] = None,
        check_injection: bool = True,
        ai_scan_enabled: bool = True,
        store_pii: bool = False,
        strict_mode: bool = False,
        failure_mode: Optional[Literal["fail_open", "fail_close"]] = None,
        guard_timeout_s: float = 2.0,
        ingest_timeout_s: float = 5.0,
        client_fast_fail: bool = False,
    ):
        resolved_api_key = _resolve_initial_api_key(api_key)
        normalized_failure_mode = _normalize_failure_mode(failure_mode)
        normalized_strict_mode = bool(strict_mode) or normalized_failure_mode == "fail_close"
        if bool(strict_mode) and normalized_failure_mode == "fail_open":
            raise ValueError(
                "AgentID strict_mode=True conflicts with failure_mode='fail_open'."
            )
        self._config = _ClientConfig(
            api_key=resolved_api_key,
            base_url=_normalize_base_url(base_url),
            guard_timeout_s=_normalize_timeout_seconds(guard_timeout_s, 2.0),
            ingest_timeout_s=_normalize_timeout_seconds(ingest_timeout_s, 5.0),
            strict_mode=normalized_strict_mode,
            failure_mode=normalized_failure_mode,
            pii_masking=pii_masking if isinstance(pii_masking, bool) else None,
            check_injection=bool(check_injection),
            ai_scan_enabled=bool(ai_scan_enabled),
            store_pii=bool(store_pii),
            client_fast_fail=bool(client_fast_fail),
        )
        self._client = httpx.Client(
            base_url=self._config.base_url,
            headers=_headers(self._config.api_key),
        )
        self._closed = False
        self._pii_manager: Optional[PIIManager] = None
        self._local_enforcer = LocalSecurityEnforcer()

    def _resolve_api_key(self, api_key: Optional[str]) -> str:
        trimmed = api_key.strip() if isinstance(api_key, str) else ""
        return trimmed or self._config.api_key

    @property
    def _pii(self) -> PIIManager:
        return self._get_pii_manager()

    def _get_pii_manager(self) -> PIIManager:
        if self._pii_manager is None:
            self._pii_manager = PIIManager()
        return self._pii_manager

    def _default_client_capabilities(
        self,
        *,
        framework: str = "python_sdk",
        has_feedback_handler: bool = False,
        capability_flags: Optional[CapabilityFlags] = None,
    ) -> Dict[str, Any]:
        return _build_client_capabilities(
            pii_masking_enabled=self._resolve_effective_pii_masking(
                capability_flags=capability_flags
            ),
            framework=framework,
            has_feedback_handler=has_feedback_handler,
        )

    def _resolve_effective_pii_masking(
        self,
        *,
        capability_flags: Optional[CapabilityFlags] = None,
        api_key: Optional[str] = None,
    ) -> bool:
        if isinstance(self._config.pii_masking, bool):
            return self._config.pii_masking
        effective_flags = capability_flags
        if effective_flags is None:
            effective_flags = self._get_cached_capability_flags(api_key=api_key)
        return bool(getattr(effective_flags, "enable_sdk_pii_masking", False))

    def _log_security_policy_violation(
        self,
        *,
        system_id: str,
        violation_type: ViolationType,
        action_taken: ActionTaken,
        api_key: Optional[str] = None,
        sdk_config_fetch_ms: Optional[int] = None,
        sdk_local_scan_ms: Optional[int] = None,
    ) -> None:
        metadata = build_security_policy_violation_metadata(
            system_id=system_id,
            violation_type=violation_type,
            action_taken=action_taken,
        )
        metadata.update(
            _build_sdk_timing_metadata(
                sdk_config_fetch_ms=sdk_config_fetch_ms,
                sdk_local_scan_ms=sdk_local_scan_ms,
            )
        )
        self.log(
            system_id=system_id,
            input="[REDACTED_SAMPLE]",
            output="",
            model="agentid.policy.enforcer",
            event_type="security_policy_violation",
            severity="high",
            metadata=metadata,
            api_key=api_key,
        )

    def _get_cached_capability_flags(self, *, api_key: Optional[str] = None) -> CapabilityFlags:
        effective_api_key = self._resolve_api_key(api_key)
        return get_cached_capability_flags(
            api_key=effective_api_key,
            base_url=self._config.base_url,
        )

    def _resolve_capability_flags(self, *, api_key: Optional[str] = None) -> CapabilityFlags:
        effective_api_key = self._resolve_api_key(api_key)
        return ensure_capability_flags_sync(
            api_key=effective_api_key,
            base_url=self._config.base_url,
            timeout_seconds=self._config.guard_timeout_s,
            ttl_seconds=300.0,
        )

    def _resolve_capability_flags_with_telemetry(
        self, *, api_key: Optional[str] = None
    ) -> tuple[CapabilityFlags, int]:
        started_at = time.perf_counter()
        flags = self._resolve_capability_flags(api_key=api_key)
        return flags, _duration_ms(started_at)

    def _should_run_local_injection_scan(self, capability_flags: CapabilityFlags) -> bool:
        if not self._config.check_injection:
            return False
        if bool(getattr(capability_flags, "shadow_mode", False)):
            return False
        return bool(getattr(capability_flags, "block_on_heuristic", False))

    def _apply_local_policy_checks(
        self,
        *,
        system_id: str,
        input_text: str,
        stream: bool,
        api_key: Optional[str] = None,
        capability_flags: CapabilityFlags,
        client_event_id: Optional[str] = None,
        sdk_config_fetch_ms: Optional[int] = None,
        run_prompt_injection_check: bool,
    ) -> tuple[str, CapabilityFlags, int]:
        effective_api_key = self._resolve_api_key(api_key)
        local_scan_started_at = time.perf_counter()

        if (
            run_prompt_injection_check
            and input_text
            and self._should_run_local_injection_scan(capability_flags)
        ):
            get_injection_scanner().scan(
                prompt=input_text,
                api_key=effective_api_key,
                base_url=self._config.base_url,
                ai_scan_enabled=self._config.ai_scan_enabled,
                store_pii=self._config.store_pii,
                pii_manager=self._pii_manager,
                source="python_sdk",
                system_id=system_id,
                event_id=client_event_id,
                client_event_id=client_event_id,
                telemetry_metadata=_build_sdk_timing_metadata(
                    sdk_config_fetch_ms=sdk_config_fetch_ms
                ),
            )

        sanitized_input, effective_flags = self._apply_local_security(
            system_id=system_id,
            input_text=input_text,
            stream=stream,
            api_key=effective_api_key,
            capability_flags=capability_flags,
            sdk_config_fetch_ms=sdk_config_fetch_ms,
            local_scan_started_at=local_scan_started_at,
        )
        return sanitized_input, effective_flags, _duration_ms(local_scan_started_at)

    def _prepare_input_for_dispatch(
        self,
        *,
        input_text: str,
        system_id: str,
        stream: bool,
        api_key: Optional[str] = None,
        client_event_id: Optional[str] = None,
        skip_injection_scan: bool = False,
    ) -> Dict[str, Any]:
        capability_flags, sdk_config_fetch_ms = self._resolve_capability_flags_with_telemetry(
            api_key=api_key
        )
        if not self._config.client_fast_fail:
            return {
                "sanitized_input": input_text,
                "capability_flags": capability_flags,
                "sdk_config_fetch_ms": sdk_config_fetch_ms,
                "sdk_local_scan_ms": 0,
            }

        sanitized_input, capability_flags, sdk_local_scan_ms = self._apply_local_policy_checks(
            system_id=system_id,
            input_text=input_text,
            stream=stream,
            api_key=api_key,
            capability_flags=capability_flags,
            client_event_id=client_event_id,
            sdk_config_fetch_ms=sdk_config_fetch_ms,
            run_prompt_injection_check=not skip_injection_scan,
        )
        return {
            "sanitized_input": sanitized_input,
            "capability_flags": capability_flags,
            "sdk_config_fetch_ms": sdk_config_fetch_ms,
            "sdk_local_scan_ms": sdk_local_scan_ms,
        }

    def _apply_local_fallback_for_guard_failure(
        self,
        *,
        input_text: str,
        system_id: str,
        stream: bool,
        api_key: Optional[str] = None,
        client_event_id: Optional[str] = None,
        capability_flags: Optional[CapabilityFlags] = None,
        sdk_config_fetch_ms: Optional[int] = None,
    ) -> Dict[str, Any]:
        effective_flags = capability_flags
        effective_sdk_config_fetch_ms = sdk_config_fetch_ms
        if effective_flags is None or effective_sdk_config_fetch_ms is None:
            effective_flags, effective_sdk_config_fetch_ms = self._resolve_capability_flags_with_telemetry(
                api_key=api_key
            )

        sanitized_input, effective_flags, sdk_local_scan_ms = self._apply_local_policy_checks(
            system_id=system_id,
            input_text=input_text,
            stream=stream,
            api_key=api_key,
            capability_flags=effective_flags,
            client_event_id=client_event_id,
            sdk_config_fetch_ms=effective_sdk_config_fetch_ms,
            run_prompt_injection_check=True,
        )
        return {
            "sanitized_input": sanitized_input,
            "capability_flags": effective_flags,
            "sdk_config_fetch_ms": effective_sdk_config_fetch_ms,
            "sdk_local_scan_ms": sdk_local_scan_ms,
        }

    def _resolve_effective_strict_mode(self, *, api_key: str) -> bool:
        if self._config.strict_mode or self._config.failure_mode == "fail_close":
            return True
        if self._config.failure_mode == "fail_open":
            return False
        flags = ensure_capability_flags_sync(
            api_key=api_key,
            base_url=self._config.base_url,
            timeout_seconds=self._config.guard_timeout_s,
            ttl_seconds=300.0,
        )
        if not isinstance(flags, CapabilityFlags):
            return False
        return bool(flags.strict_security_mode or flags.failure_mode == "fail_close")

    def _apply_local_security(
        self,
        *,
        system_id: str,
        input_text: str,
        stream: bool,
        api_key: Optional[str] = None,
        capability_flags: Optional[CapabilityFlags] = None,
        sdk_config_fetch_ms: Optional[int] = None,
        local_scan_started_at: Optional[float] = None,
    ) -> tuple[str, CapabilityFlags]:
        effective_api_key = self._resolve_api_key(api_key)
        effective_flags = capability_flags or ensure_capability_flags_sync(
            api_key=effective_api_key,
            base_url=self._config.base_url,
            timeout_seconds=2.0,
            ttl_seconds=300.0,
        )

        try:
            result = self._local_enforcer.enforce(
                input_text=input_text,
                stream=stream,
                flags=effective_flags,
            )
        except (LocalPolicyBlockError, StrictPiiStreamingError) as exc:
            self._log_security_policy_violation(
                system_id=system_id,
                violation_type=exc.violation_type,
                action_taken=exc.action_taken,
                api_key=effective_api_key,
                sdk_config_fetch_ms=sdk_config_fetch_ms,
                sdk_local_scan_ms=(
                    _duration_ms(local_scan_started_at)
                    if local_scan_started_at is not None
                    else None
                ),
            )
            raise

        for event in result.events:
            self._log_security_policy_violation(
                system_id=system_id,
                violation_type=event.violation_type,
                action_taken=event.action_taken,
                api_key=effective_api_key,
                sdk_config_fetch_ms=sdk_config_fetch_ms,
                sdk_local_scan_ms=(
                    _duration_ms(local_scan_started_at)
                    if local_scan_started_at is not None
                    else None
                ),
            )

        return result.sanitized_input, effective_flags

    def close(self) -> None:
        if self._closed:
            return
        self._client.close()
        self._closed = True

    def __enter__(self) -> "AgentID":
        return self

    def __exit__(self, exc_type, exc, tb) -> None:
        self.close()

    def wrap_openai(
        self,
        client: TClient,
        system_id: str,
        user_id: Optional[str] = None,
        api_key: Optional[str] = None,
        expected_languages: Optional[list[str]] = None,
        expectedLanguages: Optional[list[str]] = None,
        request_identity: Optional[Dict[str, Any]] = None,
    ) -> TClient:
        _assert_openai_v1_or_higher()
        return self.wrap_client(
            client,
            system_id=system_id,
            user_id=user_id,
            adapter=OpenAIAdapter(),
            api_key=api_key,
            expected_languages=expected_languages,
            expectedLanguages=expectedLanguages,
            request_identity=request_identity,
        )

    def wrap_client(
        self,
        client: TClient,
        system_id: str,
        adapter: LLMAdapter,
        user_id: Optional[str] = None,
        api_key: Optional[str] = None,
        expected_languages: Optional[list[str]] = None,
        expectedLanguages: Optional[list[str]] = None,
        request_identity: Optional[Dict[str, Any]] = None,
    ) -> TClient:
        """
        Generic wrapper entrypoint for any supported provider adapter.
        """
        effective_api_key = self._resolve_api_key(api_key)
        normalized_expected_languages = normalize_expected_languages(
            expected_languages if expected_languages is not None else expectedLanguages
        )
        if _is_openai_client(client, "OpenAI"):
            _assert_openai_v1_or_higher()
            return SyncOpenAIWrapper(
                self,
                client,
                system_id,
                user_id,
                adapter,
                api_key_override=effective_api_key,
                expected_languages=normalized_expected_languages,
                request_identity=request_identity,
            )  # type: ignore[return-value]
        if _is_openai_client(client, "AsyncOpenAI"):
            _assert_openai_v1_or_higher()
            async_agent = AsyncAgentID(
                self._config.api_key,
                self._config.base_url,
                pii_masking=self._config.pii_masking,
                check_injection=self._config.check_injection,
                ai_scan_enabled=self._config.ai_scan_enabled,
                store_pii=self._config.store_pii,
                strict_mode=self._config.strict_mode,
                failure_mode=self._config.failure_mode,
                guard_timeout_s=self._config.guard_timeout_s,
                ingest_timeout_s=self._config.ingest_timeout_s,
                client_fast_fail=self._config.client_fast_fail,
            )
            return AsyncOpenAIWrapper(
                async_agent,
                client,
                system_id,
                user_id,
                adapter,
                api_key_override=effective_api_key,
                expected_languages=normalized_expected_languages,
                request_identity=request_identity,
            )  # type: ignore[return-value]
        raise TypeError("wrap_client() currently supports OpenAI clients only")

    def guard(
        self,
        input: str,
        system_id: str,
        model: Optional[str] = None,
        user_id: Optional[str] = None,
        client_event_id: Optional[str] = None,
        expected_languages: Optional[list[str]] = None,
        request_identity: Optional[Dict[str, Any]] = None,
        api_key: Optional[str] = None,
        client_capabilities: Optional[Dict[str, Any]] = None,
    ) -> GuardResponse:
        guard_started_at = time.perf_counter()

        def _with_guard_latency(response: GuardResponse) -> GuardResponse:
            if isinstance(response.get("guard_latency_ms"), int):
                return response
            enriched = dict(response)
            enriched["guard_latency_ms"] = _duration_ms(guard_started_at)
            return enriched

        params: GuardParams = {"input": input, "system_id": system_id}
        if isinstance(model, str) and model.strip():
            params["model"] = model.strip()
        if user_id:
            params["user_id"] = user_id
        coerced_client_event_id = _coerce_uuid_str(client_event_id)
        if coerced_client_event_id is not None:
            params["client_event_id"] = coerced_client_event_id
        normalized_expected_languages = normalize_expected_languages(expected_languages)
        if normalized_expected_languages is not None:
            params["expected_languages"] = normalized_expected_languages
        if isinstance(request_identity, dict):
            params["request_identity"] = request_identity
        params["client_capabilities"] = (
            client_capabilities
            if isinstance(client_capabilities, dict)
            else self._default_client_capabilities(framework="python_sdk")
        )
        effective_api_key = self._resolve_api_key(api_key)
        request_headers = _headers(effective_api_key, coerced_client_event_id)
        effective_strict_mode = self._resolve_effective_strict_mode(api_key=effective_api_key)
        use_default_client = effective_api_key == self._config.api_key

        try:
            if use_default_client:
                res = self._client.post(
                    "/guard",
                    json=params,
                    timeout=httpx.Timeout(self._config.guard_timeout_s),
                    headers=request_headers,
                )
            else:
                with httpx.Client(
                    base_url=self._config.base_url,
                    headers=request_headers,
                ) as override_client:
                    res = override_client.post(
                        "/guard",
                        json=params,
                        timeout=httpx.Timeout(self._config.guard_timeout_s),
                    )
            data = res.json()
            if isinstance(data, dict) and isinstance(data.get("allowed"), bool):
                transparency = _coerce_transparency_metadata(data.get("transparency"))
                if transparency is not None:
                    data["transparency"] = transparency
                infrastructure_failure = (
                    data.get("allowed") is False
                    and (
                        _is_infrastructure_guard_reason(data.get("reason"))
                        or (not data.get("reason") and res.status_code >= 500)
                    )
                )
                if infrastructure_failure:
                    reason = str(data.get("reason") or f"http_{res.status_code}")
                    if effective_strict_mode:
                        logger.warning(
                            "[AgentID] Guard API infrastructure failure in strict mode (%s). Blocking request.",
                            reason,
                        )
                        return _with_guard_latency(
                            {
                                "allowed": False,
                                "reason": str(
                                    data.get("reason") or "network_error_strict_mode"
                                ),
                            }
                        )
                    logger.warning(
                        "[AgentID] Guard API infrastructure fallback in fail-open mode (%s).",
                        reason,
                    )
                    return _with_guard_latency(
                        {"allowed": True, "reason": "system_failure_fail_open"}
                    )
                return _with_guard_latency(data)  # type: ignore[return-value]

            if res.is_error:
                res.raise_for_status()

            raise ValueError("Invalid guard response")
        except Exception as exc:
            if isinstance(exc, httpx.TimeoutException):
                logger.warning("AgentID API Warning: Connection timeout exceeded.")
                if effective_strict_mode:
                    return _with_guard_latency(
                        {"allowed": False, "reason": "network_error_strict_mode"}
                    )
                return _with_guard_latency({"allowed": True, "reason": "timeout_fallback"})

            if isinstance(exc, httpx.HTTPStatusError) and exc.response.status_code >= 500:
                if effective_strict_mode:
                    return _with_guard_latency({"allowed": False, "reason": "server_error"})
                return _with_guard_latency(
                    {"allowed": True, "reason": "system_failure_fail_open"}
                )

            logger.warning(
                "[AgentID] Guard check failed (%s): %s",
                "Strict mode active" if effective_strict_mode else "Fail-Open active",
                exc.__class__.__name__,
            )
            if effective_strict_mode:
                return _with_guard_latency(
                    {"allowed": False, "reason": "network_error_strict_mode"}
                )
            return _with_guard_latency({"allowed": True, "reason": "guard_unreachable"})

    def _schedule_finalize_ingest_telemetry(
        self,
        *,
        api_key: str,
        client_event_id: str,
        system_id: Optional[str],
        sdk_ingest_ms: int,
    ) -> None:
        def _send() -> None:
            try:
                with httpx.Client(
                    base_url=self._config.base_url,
                    headers=_headers(api_key, client_event_id),
                ) as client:
                    response = client.post(
                        "/ingest/finalize",
                        json={
                            "client_event_id": client_event_id,
                            "system_id": system_id,
                            "sdk_ingest_ms": sdk_ingest_ms,
                        },
                        timeout=httpx.Timeout(min(self._config.ingest_timeout_s, 2.0)),
                    )
                if response.status_code >= 400:
                    logger.warning(
                        "[AgentID] Ingest telemetry finalize failed (status=%s, client_event_id=%s).",
                        response.status_code,
                        client_event_id,
                    )
            except Exception:
                logger.warning(
                    "[AgentID] Ingest telemetry finalize failed (client_event_id=%s).",
                    client_event_id,
                )

        threading.Thread(
            target=_send,
            daemon=True,
            name="agentid-ingest-finalize",
        ).start()

    def log(
        self,
        system_id: str,
        input: str,
        output: str,
        event_id: Optional[str] = None,
        model: Optional[str] = None,
        usage: Optional[Dict[str, Any]] = None,
        tokens: Optional[Dict[str, Any]] = None,
        metadata: Optional[Dict[str, Any]] = None,
        latency: Optional[int] = None,
        user_id: Optional[str] = None,
        request_identity: Optional[Dict[str, Any]] = None,
        event_type: Optional[str] = None,
        severity: Optional[str] = None,
        timestamp: Optional[str] = None,
        api_key: Optional[str] = None,
        client_capabilities: Optional[Dict[str, Any]] = None,
        raise_on_failure: bool = False,
        _disable_finalize: bool = False,
    ) -> None:
        merged_metadata: Dict[str, Any] = dict(metadata or {})
        merged_metadata.setdefault("agentid_base_url", self._config.base_url)
        correlation_event_id = _resolve_ingest_correlation_event_id(event_id, merged_metadata)
        merged_metadata["client_event_id"] = correlation_event_id
        payload: LogParams = {
            "event_id": correlation_event_id,
            "system_id": system_id,
            "input": input,
            "output": output,
            # Server requires model; default to a safe placeholder.
            "model": model or "unknown",
            "timestamp": timestamp or _utc_now_iso(),
            "metadata": merged_metadata,
            "client_capabilities": (
                client_capabilities
                if isinstance(client_capabilities, dict)
                else self._default_client_capabilities(framework="python_sdk")
            ),
        }
        if usage is not None:
            payload["usage"] = usage
        if tokens is not None:
            payload["tokens"] = tokens
        if latency is not None:
            payload["latency"] = int(latency)
        if user_id is not None:
            payload["user_id"] = user_id
        if isinstance(request_identity, dict):
            payload["request_identity"] = request_identity
        if event_type is not None:
            payload["event_type"] = event_type
        if severity is not None:
            payload["severity"] = severity
        if timestamp is not None:
            payload["timestamp"] = timestamp
        effective_api_key = self._resolve_api_key(api_key)
        effective_strict_mode = self._resolve_effective_strict_mode(api_key=effective_api_key)

        def _send() -> None:
            ingest_started_at = time.perf_counter()
            try:
                ensure_capability_flags_sync(
                    api_key=effective_api_key,
                    base_url=self._config.base_url,
                    timeout_seconds=self._config.guard_timeout_s,
                    ttl_seconds=300.0,
                )
                with httpx.Client(
                    base_url=self._config.base_url,
                    headers=_headers(effective_api_key, correlation_event_id),
                ) as client:
                    response = client.post(
                        "/ingest",
                        json=payload,
                        timeout=httpx.Timeout(self._config.ingest_timeout_s),
                    )

                status_code = int(response.status_code)
                reason: Optional[str] = None
                try:
                    body = response.json()
                    if isinstance(body, dict) and isinstance(body.get("reason"), str):
                        reason = body["reason"]
                except Exception:
                    reason = None

                if status_code == 409 and reason == "replay_detected":
                    return
                if 200 <= status_code < 300:
                    if not _disable_finalize:
                        self._schedule_finalize_ingest_telemetry(
                            api_key=effective_api_key,
                            client_event_id=correlation_event_id,
                            system_id=system_id,
                            sdk_ingest_ms=_duration_ms(ingest_started_at),
                        )
                    return

                if _should_raise_ingest_dependency_error(
                    strict_mode=effective_strict_mode,
                    status_code=status_code,
                    reason=reason,
                ):
                    raise DependencyError(
                        reason or "ingest_failed",
                        status_code=status_code,
                        dependency="ingest",
                    )
            except Exception as exc:
                if raise_on_failure:
                    raise
                logger.error("[AgentID] Log failed: %s", exc.__class__.__name__)

        if raise_on_failure:
            _send()
            return

        t = threading.Thread(target=_send, daemon=True, name="agentid-ingest")
        t.start()


class AsyncAgentID:
    """
    Asynchronous AgentID client.

    - guard(): awaitable (default fail-open on connectivity failure)
    - log(): schedules a background task (fire-and-forget)
    """

    def __init__(
        self,
        api_key: Optional[str] = None,
        base_url: str = "https://app.getagentid.com/api/v1",
        pii_masking: Optional[bool] = None,
        check_injection: bool = True,
        ai_scan_enabled: bool = True,
        store_pii: bool = False,
        strict_mode: bool = False,
        failure_mode: Optional[Literal["fail_open", "fail_close"]] = None,
        guard_timeout_s: float = 2.0,
        ingest_timeout_s: float = 5.0,
        client_fast_fail: bool = False,
    ):
        resolved_api_key = _resolve_initial_api_key(api_key)
        normalized_failure_mode = _normalize_failure_mode(failure_mode)
        normalized_strict_mode = bool(strict_mode) or normalized_failure_mode == "fail_close"
        if bool(strict_mode) and normalized_failure_mode == "fail_open":
            raise ValueError(
                "AgentID strict_mode=True conflicts with failure_mode='fail_open'."
            )
        self._config = _ClientConfig(
            api_key=resolved_api_key,
            base_url=_normalize_base_url(base_url),
            guard_timeout_s=_normalize_timeout_seconds(guard_timeout_s, 2.0),
            ingest_timeout_s=_normalize_timeout_seconds(ingest_timeout_s, 5.0),
            strict_mode=normalized_strict_mode,
            failure_mode=normalized_failure_mode,
            pii_masking=pii_masking if isinstance(pii_masking, bool) else None,
            check_injection=bool(check_injection),
            ai_scan_enabled=bool(ai_scan_enabled),
            store_pii=bool(store_pii),
            client_fast_fail=bool(client_fast_fail),
        )
        self._client: Optional[httpx.AsyncClient] = None
        self._closed = False
        self._pii_manager: Optional[PIIManager] = None
        self._local_enforcer = LocalSecurityEnforcer()

    def _resolve_api_key(self, api_key: Optional[str]) -> str:
        trimmed = api_key.strip() if isinstance(api_key, str) else ""
        return trimmed or self._config.api_key

    @property
    def _pii(self) -> PIIManager:
        return self._get_pii_manager()

    def _get_pii_manager(self) -> PIIManager:
        if self._pii_manager is None:
            self._pii_manager = PIIManager()
        return self._pii_manager

    def _default_client_capabilities(
        self,
        *,
        framework: str = "python_sdk",
        has_feedback_handler: bool = False,
        capability_flags: Optional[CapabilityFlags] = None,
    ) -> Dict[str, Any]:
        return _build_client_capabilities(
            pii_masking_enabled=self._resolve_effective_pii_masking(
                capability_flags=capability_flags
            ),
            framework=framework,
            has_feedback_handler=has_feedback_handler,
        )

    def _resolve_effective_pii_masking(
        self,
        *,
        capability_flags: Optional[CapabilityFlags] = None,
        api_key: Optional[str] = None,
    ) -> bool:
        if isinstance(self._config.pii_masking, bool):
            return self._config.pii_masking
        effective_flags = capability_flags
        if effective_flags is None:
            effective_flags = self._get_cached_capability_flags(api_key=api_key)
        return bool(getattr(effective_flags, "enable_sdk_pii_masking", False))

    def _log_security_policy_violation(
        self,
        *,
        system_id: str,
        violation_type: ViolationType,
        action_taken: ActionTaken,
        api_key: Optional[str] = None,
        sdk_config_fetch_ms: Optional[int] = None,
        sdk_local_scan_ms: Optional[int] = None,
    ) -> None:
        metadata = build_security_policy_violation_metadata(
            system_id=system_id,
            violation_type=violation_type,
            action_taken=action_taken,
        )
        metadata.update(
            _build_sdk_timing_metadata(
                sdk_config_fetch_ms=sdk_config_fetch_ms,
                sdk_local_scan_ms=sdk_local_scan_ms,
            )
        )
        self.log(
            system_id=system_id,
            input="[REDACTED_SAMPLE]",
            output="",
            model="agentid.policy.enforcer",
            event_type="security_policy_violation",
            severity="high",
            metadata=metadata,
            api_key=api_key,
        )

    def _get_cached_capability_flags(self, *, api_key: Optional[str] = None) -> CapabilityFlags:
        effective_api_key = self._resolve_api_key(api_key)
        return get_cached_capability_flags(
            api_key=effective_api_key,
            base_url=self._config.base_url,
        )

    async def _resolve_capability_flags(self, *, api_key: Optional[str] = None) -> CapabilityFlags:
        effective_api_key = self._resolve_api_key(api_key)
        return await ensure_capability_flags_async(
            api_key=effective_api_key,
            base_url=self._config.base_url,
            timeout_seconds=self._config.guard_timeout_s,
            ttl_seconds=300.0,
        )

    async def _resolve_capability_flags_with_telemetry(
        self, *, api_key: Optional[str] = None
    ) -> tuple[CapabilityFlags, int]:
        started_at = time.perf_counter()
        flags = await self._resolve_capability_flags(api_key=api_key)
        return flags, _duration_ms(started_at)

    def _should_run_local_injection_scan(self, capability_flags: CapabilityFlags) -> bool:
        if not self._config.check_injection:
            return False
        if bool(getattr(capability_flags, "shadow_mode", False)):
            return False
        return bool(getattr(capability_flags, "block_on_heuristic", False))

    async def _apply_local_policy_checks_async(
        self,
        *,
        system_id: str,
        input_text: str,
        stream: bool,
        api_key: Optional[str] = None,
        capability_flags: CapabilityFlags,
        client_event_id: Optional[str] = None,
        sdk_config_fetch_ms: Optional[int] = None,
        run_prompt_injection_check: bool,
    ) -> tuple[str, CapabilityFlags, int]:
        effective_api_key = self._resolve_api_key(api_key)
        local_scan_started_at = time.perf_counter()

        if (
            run_prompt_injection_check
            and input_text
            and self._should_run_local_injection_scan(capability_flags)
        ):
            get_injection_scanner().scan(
                prompt=input_text,
                api_key=effective_api_key,
                base_url=self._config.base_url,
                ai_scan_enabled=self._config.ai_scan_enabled,
                store_pii=self._config.store_pii,
                pii_manager=self._pii_manager,
                source="python_sdk",
                system_id=system_id,
                event_id=client_event_id,
                client_event_id=client_event_id,
                telemetry_metadata=_build_sdk_timing_metadata(
                    sdk_config_fetch_ms=sdk_config_fetch_ms
                ),
            )

        sanitized_input, effective_flags = await self._apply_local_security_async(
            system_id=system_id,
            input_text=input_text,
            stream=stream,
            api_key=effective_api_key,
            capability_flags=capability_flags,
            sdk_config_fetch_ms=sdk_config_fetch_ms,
            local_scan_started_at=local_scan_started_at,
        )
        return sanitized_input, effective_flags, _duration_ms(local_scan_started_at)

    async def _prepare_input_for_dispatch_async(
        self,
        *,
        input_text: str,
        system_id: str,
        stream: bool,
        api_key: Optional[str] = None,
        client_event_id: Optional[str] = None,
        skip_injection_scan: bool = False,
    ) -> Dict[str, Any]:
        capability_flags, sdk_config_fetch_ms = await self._resolve_capability_flags_with_telemetry(
            api_key=api_key
        )
        if not self._config.client_fast_fail:
            return {
                "sanitized_input": input_text,
                "capability_flags": capability_flags,
                "sdk_config_fetch_ms": sdk_config_fetch_ms,
                "sdk_local_scan_ms": 0,
            }

        sanitized_input, capability_flags, sdk_local_scan_ms = await self._apply_local_policy_checks_async(
            system_id=system_id,
            input_text=input_text,
            stream=stream,
            api_key=api_key,
            capability_flags=capability_flags,
            client_event_id=client_event_id,
            sdk_config_fetch_ms=sdk_config_fetch_ms,
            run_prompt_injection_check=not skip_injection_scan,
        )
        return {
            "sanitized_input": sanitized_input,
            "capability_flags": capability_flags,
            "sdk_config_fetch_ms": sdk_config_fetch_ms,
            "sdk_local_scan_ms": sdk_local_scan_ms,
        }

    async def _apply_local_fallback_for_guard_failure_async(
        self,
        *,
        input_text: str,
        system_id: str,
        stream: bool,
        api_key: Optional[str] = None,
        client_event_id: Optional[str] = None,
        capability_flags: Optional[CapabilityFlags] = None,
        sdk_config_fetch_ms: Optional[int] = None,
    ) -> Dict[str, Any]:
        effective_flags = capability_flags
        effective_sdk_config_fetch_ms = sdk_config_fetch_ms
        if effective_flags is None or effective_sdk_config_fetch_ms is None:
            (
                effective_flags,
                effective_sdk_config_fetch_ms,
            ) = await self._resolve_capability_flags_with_telemetry(api_key=api_key)

        sanitized_input, effective_flags, sdk_local_scan_ms = await self._apply_local_policy_checks_async(
            system_id=system_id,
            input_text=input_text,
            stream=stream,
            api_key=api_key,
            capability_flags=effective_flags,
            client_event_id=client_event_id,
            sdk_config_fetch_ms=effective_sdk_config_fetch_ms,
            run_prompt_injection_check=True,
        )
        return {
            "sanitized_input": sanitized_input,
            "capability_flags": effective_flags,
            "sdk_config_fetch_ms": effective_sdk_config_fetch_ms,
            "sdk_local_scan_ms": sdk_local_scan_ms,
        }

    async def _resolve_effective_strict_mode(self, *, api_key: str) -> bool:
        if self._config.strict_mode or self._config.failure_mode == "fail_close":
            return True
        if self._config.failure_mode == "fail_open":
            return False
        flags = await ensure_capability_flags_async(
            api_key=api_key,
            base_url=self._config.base_url,
            timeout_seconds=self._config.guard_timeout_s,
            ttl_seconds=300.0,
        )
        if not isinstance(flags, CapabilityFlags):
            return False
        return bool(flags.strict_security_mode or flags.failure_mode == "fail_close")

    async def _apply_local_security_async(
        self,
        *,
        system_id: str,
        input_text: str,
        stream: bool,
        api_key: Optional[str] = None,
        capability_flags: Optional[CapabilityFlags] = None,
        sdk_config_fetch_ms: Optional[int] = None,
        local_scan_started_at: Optional[float] = None,
    ) -> tuple[str, CapabilityFlags]:
        effective_api_key = self._resolve_api_key(api_key)
        effective_flags = capability_flags or await ensure_capability_flags_async(
            api_key=effective_api_key,
            base_url=self._config.base_url,
            timeout_seconds=2.0,
            ttl_seconds=300.0,
        )

        try:
            result = self._local_enforcer.enforce(
                input_text=input_text,
                stream=stream,
                flags=effective_flags,
            )
        except (LocalPolicyBlockError, StrictPiiStreamingError) as exc:
            self._log_security_policy_violation(
                system_id=system_id,
                violation_type=exc.violation_type,
                action_taken=exc.action_taken,
                api_key=effective_api_key,
                sdk_config_fetch_ms=sdk_config_fetch_ms,
                sdk_local_scan_ms=(
                    _duration_ms(local_scan_started_at)
                    if local_scan_started_at is not None
                    else None
                ),
            )
            raise

        for event in result.events:
            self._log_security_policy_violation(
                system_id=system_id,
                violation_type=event.violation_type,
                action_taken=event.action_taken,
                api_key=effective_api_key,
                sdk_config_fetch_ms=sdk_config_fetch_ms,
                sdk_local_scan_ms=(
                    _duration_ms(local_scan_started_at)
                    if local_scan_started_at is not None
                    else None
                ),
            )

        return result.sanitized_input, effective_flags

    async def aclose(self) -> None:
        if self._closed:
            return
        if self._client is not None:
            await self._client.aclose()
        self._closed = True

    async def __aenter__(self) -> "AsyncAgentID":
        if self._client is None:
            self._client = httpx.AsyncClient(
                base_url=self._config.base_url,
                headers=_headers(self._config.api_key),
            )
        return self

    async def __aexit__(self, exc_type, exc, tb) -> None:
        await self.aclose()

    def wrap_openai(
        self,
        client: TClient,
        system_id: str,
        user_id: Optional[str] = None,
        api_key: Optional[str] = None,
        expected_languages: Optional[list[str]] = None,
        expectedLanguages: Optional[list[str]] = None,
        request_identity: Optional[Dict[str, Any]] = None,
    ) -> TClient:
        _assert_openai_v1_or_higher()
        return self.wrap_client(
            client,
            system_id=system_id,
            user_id=user_id,
            adapter=OpenAIAdapter(),
            api_key=api_key,
            expected_languages=expected_languages,
            expectedLanguages=expectedLanguages,
            request_identity=request_identity,
        )

    def wrap_client(
        self,
        client: TClient,
        system_id: str,
        adapter: LLMAdapter,
        user_id: Optional[str] = None,
        api_key: Optional[str] = None,
        expected_languages: Optional[list[str]] = None,
        expectedLanguages: Optional[list[str]] = None,
        request_identity: Optional[Dict[str, Any]] = None,
    ) -> TClient:
        effective_api_key = self._resolve_api_key(api_key)
        normalized_expected_languages = normalize_expected_languages(
            expected_languages if expected_languages is not None else expectedLanguages
        )
        if _is_openai_client(client, "AsyncOpenAI"):
            _assert_openai_v1_or_higher()
            return AsyncOpenAIWrapper(
                self,
                client,
                system_id,
                user_id,
                adapter,
                api_key_override=effective_api_key,
                expected_languages=normalized_expected_languages,
                request_identity=request_identity,
            )  # type: ignore[return-value]
        if _is_openai_client(client, "OpenAI"):
            _assert_openai_v1_or_higher()
            sync_agent = AgentID(
                self._config.api_key,
                self._config.base_url,
                pii_masking=self._config.pii_masking,
                check_injection=self._config.check_injection,
                ai_scan_enabled=self._config.ai_scan_enabled,
                store_pii=self._config.store_pii,
                strict_mode=self._config.strict_mode,
                failure_mode=self._config.failure_mode,
                guard_timeout_s=self._config.guard_timeout_s,
                ingest_timeout_s=self._config.ingest_timeout_s,
                client_fast_fail=self._config.client_fast_fail,
            )
            return SyncOpenAIWrapper(
                sync_agent,
                client,
                system_id,
                user_id,
                adapter,
                api_key_override=effective_api_key,
                expected_languages=normalized_expected_languages,
                request_identity=request_identity,
            )  # type: ignore[return-value]
        raise TypeError("wrap_client() currently supports OpenAI clients only")

    async def guard(
        self,
        input: str,
        system_id: str,
        model: Optional[str] = None,
        user_id: Optional[str] = None,
        client_event_id: Optional[str] = None,
        expected_languages: Optional[list[str]] = None,
        request_identity: Optional[Dict[str, Any]] = None,
        api_key: Optional[str] = None,
        client_capabilities: Optional[Dict[str, Any]] = None,
    ) -> GuardResponse:
        guard_started_at = time.perf_counter()

        def _with_guard_latency(response: GuardResponse) -> GuardResponse:
            if isinstance(response.get("guard_latency_ms"), int):
                return response
            enriched = dict(response)
            enriched["guard_latency_ms"] = _duration_ms(guard_started_at)
            return enriched

        params: GuardParams = {"input": input, "system_id": system_id}
        if isinstance(model, str) and model.strip():
            params["model"] = model.strip()
        if user_id:
            params["user_id"] = user_id
        coerced_client_event_id = _coerce_uuid_str(client_event_id)
        if coerced_client_event_id is not None:
            params["client_event_id"] = coerced_client_event_id
        normalized_expected_languages = normalize_expected_languages(expected_languages)
        if normalized_expected_languages is not None:
            params["expected_languages"] = normalized_expected_languages
        if isinstance(request_identity, dict):
            params["request_identity"] = request_identity
        params["client_capabilities"] = (
            client_capabilities
            if isinstance(client_capabilities, dict)
            else self._default_client_capabilities(framework="python_sdk")
        )

        effective_api_key = self._resolve_api_key(api_key)
        request_headers = _headers(effective_api_key, coerced_client_event_id)
        effective_strict_mode = await self._resolve_effective_strict_mode(api_key=effective_api_key)
        client = self._client
        close_after = False
        if client is None or effective_api_key != self._config.api_key:
            client = httpx.AsyncClient(
                base_url=self._config.base_url,
                headers=request_headers,
            )
            close_after = True

        try:
            res = await client.post(
                "/guard",
                json=params,
                timeout=httpx.Timeout(self._config.guard_timeout_s),
                headers=request_headers,
            )
            data = res.json()
            if isinstance(data, dict) and isinstance(data.get("allowed"), bool):
                transparency = _coerce_transparency_metadata(data.get("transparency"))
                if transparency is not None:
                    data["transparency"] = transparency
                infrastructure_failure = (
                    data.get("allowed") is False
                    and (
                        _is_infrastructure_guard_reason(data.get("reason"))
                        or (not data.get("reason") and res.status_code >= 500)
                    )
                )
                if infrastructure_failure:
                    reason = str(data.get("reason") or f"http_{res.status_code}")
                    if effective_strict_mode:
                        logger.warning(
                            "[AgentID] Guard API infrastructure failure in strict mode (%s). Blocking request.",
                            reason,
                        )
                        return _with_guard_latency(
                            {
                                "allowed": False,
                                "reason": str(
                                    data.get("reason") or "network_error_strict_mode"
                                ),
                            }
                        )
                    logger.warning(
                        "[AgentID] Guard API infrastructure fallback in fail-open mode (%s).",
                        reason,
                    )
                    return _with_guard_latency(
                        {"allowed": True, "reason": "system_failure_fail_open"}
                    )
                return _with_guard_latency(data)  # type: ignore[return-value]

            if res.is_error:
                res.raise_for_status()

            raise ValueError("Invalid guard response")
        except Exception as exc:
            if isinstance(exc, httpx.TimeoutException):
                logger.warning("AgentID API Warning: Connection timeout exceeded.")
                if effective_strict_mode:
                    return _with_guard_latency(
                        {"allowed": False, "reason": "network_error_strict_mode"}
                    )
                return _with_guard_latency({"allowed": True, "reason": "timeout_fallback"})

            if isinstance(exc, httpx.HTTPStatusError) and exc.response.status_code >= 500:
                if effective_strict_mode:
                    return _with_guard_latency({"allowed": False, "reason": "server_error"})
                return _with_guard_latency(
                    {"allowed": True, "reason": "system_failure_fail_open"}
                )

            logger.warning(
                "[AgentID] Guard check failed (%s): %s",
                "Strict mode active" if effective_strict_mode else "Fail-Open active",
                exc.__class__.__name__,
            )
            if effective_strict_mode:
                return _with_guard_latency(
                    {"allowed": False, "reason": "network_error_strict_mode"}
                )
            return _with_guard_latency({"allowed": True, "reason": "guard_unreachable"})
        finally:
            if close_after:
                await client.aclose()

    async def _finalize_ingest_telemetry_async(
        self,
        *,
        api_key: str,
        client_event_id: str,
        system_id: Optional[str],
        sdk_ingest_ms: int,
    ) -> None:
        client = httpx.AsyncClient(
            base_url=self._config.base_url,
            headers=_headers(api_key, client_event_id),
        )
        try:
            response = await client.post(
                "/ingest/finalize",
                json={
                    "client_event_id": client_event_id,
                    "system_id": system_id,
                    "sdk_ingest_ms": sdk_ingest_ms,
                },
                timeout=httpx.Timeout(min(self._config.ingest_timeout_s, 2.0)),
                headers=_headers(api_key, client_event_id),
            )
            if response.status_code >= 400:
                logger.warning(
                    "[AgentID] Ingest telemetry finalize failed (status=%s, client_event_id=%s).",
                    response.status_code,
                    client_event_id,
                )
        except Exception:
            logger.warning(
                "[AgentID] Ingest telemetry finalize failed (client_event_id=%s).",
                client_event_id,
            )
        finally:
            await client.aclose()

    async def log_async(
        self,
        system_id: str,
        input: str,
        output: str,
        event_id: Optional[str] = None,
        model: Optional[str] = None,
        usage: Optional[Dict[str, Any]] = None,
        tokens: Optional[Dict[str, Any]] = None,
        metadata: Optional[Dict[str, Any]] = None,
        latency: Optional[int] = None,
        user_id: Optional[str] = None,
        request_identity: Optional[Dict[str, Any]] = None,
        event_type: Optional[str] = None,
        severity: Optional[str] = None,
        timestamp: Optional[str] = None,
        api_key: Optional[str] = None,
        client_capabilities: Optional[Dict[str, Any]] = None,
        raise_on_failure: bool = False,
        _disable_finalize: bool = False,
    ) -> None:
        merged_metadata: Dict[str, Any] = dict(metadata or {})
        merged_metadata.setdefault("agentid_base_url", self._config.base_url)
        correlation_event_id = _resolve_ingest_correlation_event_id(event_id, merged_metadata)
        merged_metadata["client_event_id"] = correlation_event_id
        payload: LogParams = {
            "event_id": correlation_event_id,
            "system_id": system_id,
            "input": input,
            "output": output,
            "model": model or "unknown",
            "timestamp": timestamp or _utc_now_iso(),
            "metadata": merged_metadata,
            "client_capabilities": (
                client_capabilities
                if isinstance(client_capabilities, dict)
                else self._default_client_capabilities(framework="python_sdk")
            ),
        }
        if usage is not None:
            payload["usage"] = usage
        if tokens is not None:
            payload["tokens"] = tokens
        if latency is not None:
            payload["latency"] = int(latency)
        if user_id is not None:
            payload["user_id"] = user_id
        if isinstance(request_identity, dict):
            payload["request_identity"] = request_identity
        if event_type is not None:
            payload["event_type"] = event_type
        if severity is not None:
            payload["severity"] = severity
        if timestamp is not None:
            payload["timestamp"] = timestamp
        effective_api_key = self._resolve_api_key(api_key)
        effective_strict_mode = await self._resolve_effective_strict_mode(
            api_key=effective_api_key
        )
        client = self._client
        close_after = False
        if client is None or effective_api_key != self._config.api_key:
            client = httpx.AsyncClient(
                base_url=self._config.base_url,
                headers=_headers(effective_api_key, correlation_event_id),
            )
            close_after = True

        try:
            ingest_started_at = time.perf_counter()
            await ensure_capability_flags_async(
                api_key=effective_api_key,
                base_url=self._config.base_url,
                timeout_seconds=self._config.guard_timeout_s,
                ttl_seconds=300.0,
            )
            response = await client.post(
                "/ingest",
                json=payload,
                timeout=httpx.Timeout(self._config.ingest_timeout_s),
                headers=_headers(effective_api_key, correlation_event_id),
            )
            status_code = int(response.status_code)
            reason: Optional[str] = None
            try:
                body = response.json()
                if isinstance(body, dict) and isinstance(body.get("reason"), str):
                    reason = body["reason"]
            except Exception:
                reason = None

            if status_code == 409 and reason == "replay_detected":
                return
            if 200 <= status_code < 300:
                if not _disable_finalize:
                    asyncio.create_task(
                        self._finalize_ingest_telemetry_async(
                            api_key=effective_api_key,
                            client_event_id=correlation_event_id,
                            system_id=system_id,
                            sdk_ingest_ms=_duration_ms(ingest_started_at),
                        )
                    )
                return

            if _should_raise_ingest_dependency_error(
                strict_mode=effective_strict_mode,
                status_code=status_code,
                reason=reason,
            ):
                raise DependencyError(
                    reason or "ingest_failed",
                    status_code=status_code,
                    dependency="ingest",
                )
        except Exception as exc:
            if raise_on_failure:
                raise
            logger.error("[AgentID] Log failed: %s", exc.__class__.__name__)
        finally:
            if close_after:
                await client.aclose()

    def log(
        self,
        system_id: str,
        input: str,
        output: str,
        event_id: Optional[str] = None,
        model: Optional[str] = None,
        usage: Optional[Dict[str, Any]] = None,
        tokens: Optional[Dict[str, Any]] = None,
        metadata: Optional[Dict[str, Any]] = None,
        latency: Optional[int] = None,
        user_id: Optional[str] = None,
        request_identity: Optional[Dict[str, Any]] = None,
        event_type: Optional[str] = None,
        severity: Optional[str] = None,
        timestamp: Optional[str] = None,
        api_key: Optional[str] = None,
        client_capabilities: Optional[Dict[str, Any]] = None,
    ) -> None:
        try:
            loop = asyncio.get_running_loop()
        except RuntimeError:
            logger.warning("[AgentID] Async log skipped: no running event loop")
            return

        loop.create_task(
            self.log_async(
                system_id=system_id,
                input=input,
                output=output,
                event_id=event_id,
                model=model,
                usage=usage,
                tokens=tokens,
                metadata=metadata,
                latency=latency,
                user_id=user_id,
                request_identity=request_identity,
                event_type=event_type,
                severity=severity,
                timestamp=timestamp,
                api_key=api_key,
                client_capabilities=client_capabilities,
                raise_on_failure=False,
            )
        )

