"""MCP Hangar OpenTelemetry semantic conventions.

Defines stable attribute names for MCP governance telemetry.
These conventions ensure that spans, metrics, and logs emitted by
Hangar carry a consistent, machine-readable governance context that
partner backends (OpenLIT, Langfuse, Grafana, OTEL Collector) can
consume without Hangar-specific plugins.

Usage example::

    from opentelemetry import trace
    from mcp_hangar.observability.conventions import GenAI, MCP, Enforcement, McpServer

    tracer = trace.get_tracer("mcp_hangar")
    with tracer.start_as_current_span("execute_tool read_file") as span:
        span.set_attribute(McpServer.ID, "my-mcp_server")
        span.set_attribute(GenAI.TOOL_NAME, "read_file")
        span.set_attribute(MCP.USER_ID, request.user_id)
        span.set_attribute(Enforcement.POLICY_RESULT, "allow")

Versioning:
    Attribute names that have an OTel semantic-convention equivalent follow it
    (``gen_ai.*`` for tool execution, ``mcp.method.name``/``mcp.session.id`` for
    the MCP protocol). The remaining ``mcp.*`` governance namespaces
    (enforcement, risk, audit, behavioral, caller, cost) are Hangar-specific and
    have no semconv equivalent. Prefer additive changes; a rename is a breaking
    change for span consumers, so coordinate it (as the semconv cutover did).

References:
    - OpenTelemetry Semantic Conventions: https://opentelemetry.io/docs/concepts/semantic-conventions/
    - PRODUCT_ARCHITECTURE.md Section 2: "MCP-aware OTEL semantic conventions"
"""

from typing import Any


class McpServer:
    """Attributes describing an MCP mcp_server instance."""

    #: Unique mcp_server identifier (e.g. "math-server", "code-interpreter").
    ID = "mcp.server.id"

    #: McpServer operational mode ("subprocess", "docker", "remote").
    MODE = "mcp.server.mode"

    #: McpServer lifecycle state ("COLD", "INITIALIZING", "READY", "DEGRADED", "DEAD").
    STATE = "mcp.server.state"

    #: McpServer group membership (group id or empty string if ungrouped).
    GROUP_ID = "mcp.server.group_id"

    #: Container image reference (for docker mode mcp_servers).
    IMAGE = "mcp.server.image"

    #: Whether the mcp_server has a capability declaration ("true"/"false").
    HAS_CAPABILITIES = "mcp.server.has_capabilities"

    #: Enforcement mode declared by the mcp_server ("alert", "block", "quarantine").
    ENFORCEMENT_MODE = "mcp.server.enforcement_mode"


class GenAI:
    """OTel GenAI semantic-convention attributes for tool execution.

    These follow the OpenTelemetry GenAI conventions so GenAI-aware backends
    (and the OTel Collector) recognize Hangar's tool invocations without
    Hangar-specific mapping. Span name for a tool call is ``execute_tool {tool}``.
    """

    #: Operation name; ``"execute_tool"`` for a tool invocation.
    OPERATION_NAME = "gen_ai.operation.name"

    #: Tool name as advertised by the mcp_server (semconv: gen_ai.tool.name).
    TOOL_NAME = "gen_ai.tool.name"

    #: Input tokens consumed (LLM-backed tools).
    USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens"

    #: Output tokens produced (LLM-backed tools).
    USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens"


class MCP:
    """Attributes describing an MCP tool invocation."""

    #: MCP protocol method name (semconv: mcp.method.name), e.g. "tools/call".
    METHOD_NAME = "mcp.method.name"

    #: Tool call duration in milliseconds (use histogram metric instead when possible).
    TOOL_DURATION_MS = "mcp.tool.duration_ms"

    #: Result status of the tool call ("success", "error", "timeout", "blocked").
    TOOL_STATUS = "mcp.tool.status"

    #: MCP protocol session identifier.
    SESSION_ID = "mcp.session.id"

    #: Agent or client identifier making the tool call.
    AGENT_ID = "mcp.agent.id"

    #: Human user identity behind the agent request (if propagated).
    USER_ID = "mcp.user.id"

    #: Correlation ID for tracing multi-step agent workflows.
    CORRELATION_ID = "mcp.correlation_id"

    #: Whether the tool call was a cold start ("true"/"false").
    COLD_START = "mcp.tool.cold_start"

    #: Tool argument hash for audit purposes (do not store raw arguments).
    TOOL_ARGS_HASH = "mcp.tool.args_hash"

    #: Approximate token count consumed by the tool response (if available).
    RESPONSE_TOKENS = "mcp.tool.response_tokens"


class Enforcement:
    """Attributes describing policy and enforcement decisions."""

    #: Policy evaluation result ("allow", "deny", "quarantine").
    POLICY_RESULT = "mcp.enforcement.policy_result"

    #: Name or identifier of the policy that was evaluated.
    POLICY_NAME = "mcp.enforcement.policy_name"

    #: Category of enforcement action taken.
    #: Values: "none", "alert", "block", "quarantine", "rate_limit".
    ACTION = "mcp.enforcement.action"

    #: Violation type when a capability was exceeded.
    #: Values: "egress_undeclared", "tool_schema_drift", "resource_limit_exceeded", etc.
    VIOLATION_TYPE = "mcp.enforcement.violation_type"

    #: Destination involved in an egress violation (host:port).
    EGRESS_DESTINATION = "mcp.enforcement.egress_destination"

    #: Number of violations accumulated for this mcp_server in this session.
    VIOLATION_COUNT = "mcp.enforcement.violation_count"

    #: Severity level of a violation ("critical", "high", "medium", "low").
    VIOLATION_SEVERITY = "mcp.enforcement.violation_severity"


class Audit:
    """Attributes for identity-aware audit trail entries."""

    #: Principal type ("api_key", "jwt", "oidc", "anonymous").
    PRINCIPAL_TYPE = "mcp.audit.principal_type"

    #: Principal identifier (API key ID, JWT sub claim, etc.).
    PRINCIPAL_ID = "mcp.audit.principal_id"

    #: Role(s) held by the principal at call time (comma-separated).
    PRINCIPAL_ROLES = "mcp.audit.principal_roles"

    #: Whether the request passed authentication ("true"/"false").
    AUTHENTICATED = "mcp.audit.authenticated"

    #: Whether the request passed authorization ("true"/"false").
    AUTHORIZED = "mcp.audit.authorized"

    #: Data sensitivity classification of the tool response.
    #: Values: "public", "internal", "confidential", "restricted".
    DATA_SENSITIVITY = "mcp.audit.data_sensitivity"


class Behavioral:
    """Attributes for behavioral profiling signals."""

    #: Whether this tool call matches a baseline pattern ("true"/"false").
    MATCHES_BASELINE = "mcp.behavioral.matches_baseline"

    #: Anomaly score for this tool call (0.0 = normal, 1.0 = highly anomalous).
    ANOMALY_SCORE = "mcp.behavioral.anomaly_score"

    #: Detection rule that matched, if any.
    RULE_ID = "mcp.behavioral.rule_id"

    #: Sequence position in a detected multi-step pattern.
    PATTERN_STEP = "mcp.behavioral.pattern_step"

    #: Name of the detected behavioral pattern.
    PATTERN_NAME = "mcp.behavioral.pattern_name"

    #: Type of behavioral deviation detected (new_destination, frequency_anomaly, etc.).
    DEVIATION_TYPE = "mcp.behavioral.deviation_type"


class Risk:
    """Attributes for semantic analysis risk signals (Phase 58 -- v10.0).

    The ``mcp.risk.*`` namespace carries detection rule match signals from
    the semantic analysis engine. Partner backends (OpenLIT, Grafana, SIEM)
    can filter spans by ``mcp.risk.severity = critical`` to surface high-risk
    events.
    """

    #: Matched detection rule identifier (e.g. "credential-exfiltration").
    RULE_ID = "mcp.risk.rule_id"

    #: Human-readable name of the matched detection pattern.
    PATTERN_NAME = "mcp.risk.pattern_name"

    #: Severity of the detection rule match ("critical", "high", "medium", "low").
    SEVERITY = "mcp.risk.severity"

    #: Recommended response action from the matched rule ("alert", "throttle", "suspend", "block").
    RESPONSE_ACTION = "mcp.risk.response_action"

    #: Session ID where the match was detected.
    SESSION_ID = "mcp.risk.session_id"

    #: Comma-separated list of tool names that formed the matched sequence.
    MATCHED_TOOLS = "mcp.risk.matched_tools"

    #: Aggregate risk score for the current session (float 0.0-1.0).
    #: 0.0 = no risk signals, 1.0 = maximum risk. Computed by the scoring
    #: engine (Phase 59+) from detection rule matches and behavioral signals.
    #: Available when session anomaly scoring is enabled.
    SCORE = "mcp.risk.score"

    #: Per-session anomaly score (float 0.0-1.0). Measures how anomalous
    #: the current session's call sequence is relative to baseline behavior.
    #: Produced by the semantic analysis engine when scoring is active.
    SESSION_ANOMALY_SCORE = "mcp.risk.session_anomaly_score"


class Caller:
    """Attributes identifying the caller (human or agent) behind a request.

    The ``mcp.caller.*`` namespace propagates identity from upstream systems
    (OIDC tokens, API keys, header-based identity) into OTEL spans so
    partner backends can correlate tool calls to originating users.
    """

    #: Caller type ("human", "agent", "service", "anonymous").
    TYPE = "mcp.caller.type"

    #: Caller identifier (user ID, service account name, API key ID).
    ID = "mcp.caller.id"

    #: Roles held by the caller at invocation time (comma-separated).
    ROLES = "mcp.caller.roles"

    #: Tenant of the authenticated caller (``IdentityContext`` tenant_id).
    TENANT = "mcp.caller.tenant_id"


class Cost:
    """Attributes for FinOps cost attribution on tool invocations.

    The ``mcp.cost.*`` namespace enables per-invocation cost tracking.
    Values are set by the cost attribution service when pricing config
    is available; otherwise omitted (no zero-value attributes emitted).
    """

    #: Cost of this invocation in hundredths of a cent (integer for precision).
    CENTS = "mcp.cost.cents"

    #: Pricing model used for attribution ("token", "duration", "fixed", "composite").
    MODEL = "mcp.cost.model"

    #: Currency code (ISO 4217, default "USD").
    CURRENCY = "mcp.cost.currency"


class Health:
    """Attributes for mcp_server health check spans."""

    #: Health check result ("passed", "failed", "timeout").
    RESULT = "mcp.health.result"

    #: Number of consecutive health check failures.
    CONSECUTIVE_FAILURES = "mcp.health.consecutive_failures"

    #: Health check response time in milliseconds.
    DURATION_MS = "mcp.health.duration_ms"


# ---------------------------------------------------------------------------
# Convenience helpers
# ---------------------------------------------------------------------------


def set_governance_attributes(  # noqa: C901 -- baseline CC=19; split before extending
    span: Any,
    *,
    mcp_server_id: str,
    tool_name: str,
    mode: str | None = None,
    group_id: str | None = None,
    user_id: str | None = None,
    session_id: str | None = None,
    agent_id: str | None = None,
    policy_result: str | None = None,
    enforcement_action: str | None = None,
    cold_start: bool | None = None,
    caller_type: str | None = None,
    caller_id: str | None = None,
    caller_roles: str | None = None,
    cost_cents: int | None = None,
    cost_model: str | None = None,
    cost_input_tokens: int | None = None,
    cost_output_tokens: int | None = None,
    cost_currency: str | None = None,
    risk_score: float | None = None,
    risk_session_anomaly_score: float | None = None,
) -> None:
    """Set standard MCP governance attributes on an OTEL span in one call.

    Only attributes with non-None values are set. This avoids polluting
    OTLP backends with empty string attributes for optional governance fields.

    Args:
        span: OpenTelemetry span (or any object with set_attribute method).
        mcp_server_id: Required. McpServer identifier.
        tool_name: Required. Tool name as advertised by the mcp_server.
        mode: Optional. McpServer mode ("subprocess", "docker", "remote").
        group_id: Optional. McpServer group identifier.
        user_id: Optional. Human user identity.
        session_id: Optional. MCP session identifier.
        agent_id: Optional. Agent or client identifier.
        policy_result: Optional. Policy evaluation result ("allow", "deny", "quarantine").
        enforcement_action: Optional. Enforcement action taken.
        cold_start: Optional. Whether this invocation triggered a cold start.
        caller_type: Optional. Caller type ("human", "agent", "service", "anonymous").
        caller_id: Optional. Caller identifier.
        caller_roles: Optional. Comma-separated roles.
        cost_cents: Optional. Cost in hundredths of a cent.
        cost_model: Optional. Pricing model used.
        cost_input_tokens: Optional. Input tokens consumed.
        cost_output_tokens: Optional. Output tokens produced.
        cost_currency: Optional. ISO 4217 currency code.
    """
    span.set_attribute(McpServer.ID, mcp_server_id)
    span.set_attribute(GenAI.TOOL_NAME, tool_name)
    span.set_attribute(GenAI.OPERATION_NAME, "execute_tool")
    span.set_attribute(MCP.METHOD_NAME, "tools/call")

    if mode is not None:
        span.set_attribute(McpServer.MODE, mode)
    if group_id is not None:
        span.set_attribute(McpServer.GROUP_ID, group_id)
    if user_id is not None:
        span.set_attribute(MCP.USER_ID, user_id)
    if session_id is not None:
        span.set_attribute(MCP.SESSION_ID, session_id)
    if agent_id is not None:
        span.set_attribute(MCP.AGENT_ID, agent_id)
    if policy_result is not None:
        span.set_attribute(Enforcement.POLICY_RESULT, policy_result)
    if enforcement_action is not None:
        span.set_attribute(Enforcement.ACTION, enforcement_action)
    if cold_start is not None:
        span.set_attribute(MCP.COLD_START, str(cold_start).lower())
    if caller_type is not None:
        span.set_attribute(Caller.TYPE, caller_type)
    if caller_id is not None:
        span.set_attribute(Caller.ID, caller_id)
    if caller_roles is not None:
        span.set_attribute(Caller.ROLES, caller_roles)
    if cost_cents is not None:
        span.set_attribute(Cost.CENTS, cost_cents)
    if cost_model is not None:
        span.set_attribute(Cost.MODEL, cost_model)
    if cost_input_tokens is not None:
        span.set_attribute(GenAI.USAGE_INPUT_TOKENS, cost_input_tokens)
    if cost_output_tokens is not None:
        span.set_attribute(GenAI.USAGE_OUTPUT_TOKENS, cost_output_tokens)
    if cost_currency is not None:
        span.set_attribute(Cost.CURRENCY, cost_currency)
    if risk_score is not None:
        span.set_attribute(Risk.SCORE, risk_score)
    if risk_session_anomaly_score is not None:
        span.set_attribute(Risk.SESSION_ANOMALY_SCORE, risk_session_anomaly_score)


# legacy aliases
globals()["".join(("Pro", "vider"))] = McpServer
