"""
AutoPIL OpenTelemetry integration.

Emits one span and metrics per context evaluation — visible in Datadog, Grafana,
Splunk, or any OTEL-compatible backend without building custom dashboards.

Spans:
  autopil.evaluate
    autopil.agent_role     string
    autopil.source_id      string
    autopil.decision       "ALLOW" | "DENY"
    autopil.policy_name    string
    autopil.tenant_id      string
    autopil.session_id     string
    autopil.sensitivity_level  string (optional)
    autopil.reason         string (on DENY)
    autopil.latency_ms     float

Metrics:
  autopil.evaluate.count          counter   — total evaluations
  autopil.evaluate.denied.count   counter   — denials only
  autopil.evaluate.duration       histogram — latency in ms

Configuration (standard OTEL env vars):
  OTEL_SERVICE_NAME              service name shown in your APM (default: autopil)
  OTEL_EXPORTER_OTLP_ENDPOINT   gRPC endpoint, e.g. http://localhost:4317
  OTEL_EXPORTER_OTLP_HEADERS    auth headers, e.g. "x-api-key=<key>" for Datadog
  OTEL_TRACES_EXPORTER           otlp | console | none
  OTEL_METRICS_EXPORTER          otlp | console | none

Install:
  pip install autopil[otel]          # gRPC OTLP (Datadog, Grafana Cloud, Splunk)
  pip install autopil[otel-http]     # HTTP OTLP alternative

Zero-overhead no-op when opentelemetry-sdk is not installed or no exporter is configured.
"""

import os
import time
from contextlib import contextmanager

# Module-level instruments — None until setup() initializes them
_tracer          = None
_eval_counter    = None
_deny_counter    = None
_duration_histo  = None
_initialized     = False


def setup(service_name: str | None = None) -> bool:
    """
    Initialize OTEL providers from env vars. Idempotent — safe to call repeatedly.

    Returns True if at least one exporter was configured, False otherwise.
    No-op (returns False) if opentelemetry-sdk is not installed.
    """
    global _tracer, _eval_counter, _deny_counter, _duration_histo, _initialized

    if _initialized:
        return _tracer is not None or _eval_counter is not None

    _initialized = True

    try:
        from opentelemetry import metrics as _om, trace as _ot
        from opentelemetry.sdk.metrics import MeterProvider
        from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
        from opentelemetry.sdk.resources import Resource
        from opentelemetry.sdk.trace import TracerProvider
    except ImportError:
        return False  # SDK not installed — remain no-op

    svc      = service_name or os.environ.get("OTEL_SERVICE_NAME", "autopil")
    resource = Resource.create({"service.name": svc})
    endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "")

    # default: use OTLP when endpoint is set, otherwise nothing
    traces_exp  = os.environ.get("OTEL_TRACES_EXPORTER",  "otlp" if endpoint else "").lower()
    metrics_exp = os.environ.get("OTEL_METRICS_EXPORTER", "otlp" if endpoint else "").lower()

    configured = False

    # ── traces ────────────────────────────────────────────────────────────────
    if traces_exp and traces_exp != "none":
        tracer_provider = TracerProvider(resource=resource)
        _attach_trace_exporter(tracer_provider, traces_exp, endpoint)
        _ot.set_tracer_provider(tracer_provider)
        _tracer = _ot.get_tracer("autopil", "0.1.0")
        configured = True

    # ── metrics ───────────────────────────────────────────────────────────────
    if metrics_exp and metrics_exp != "none":
        reader = _make_metric_reader(metrics_exp, endpoint)
        if reader is not None:
            meter_provider = MeterProvider(resource=resource, metric_readers=[reader])
            _om.set_meter_provider(meter_provider)
            meter = _om.get_meter("autopil", "0.1.0")

            _eval_counter = meter.create_counter(
                "autopil.evaluate.count",
                description="Total context evaluation requests",
            )
            _deny_counter = meter.create_counter(
                "autopil.evaluate.denied.count",
                description="Context evaluations that resulted in DENY",
            )
            _duration_histo = meter.create_histogram(
                "autopil.evaluate.duration",
                unit="ms",
                description="Context evaluation latency in milliseconds",
            )
            configured = True

    return configured


def reset() -> None:
    """Reset telemetry state. For testing only."""
    global _tracer, _eval_counter, _deny_counter, _duration_histo, _initialized
    _tracer = _eval_counter = _deny_counter = _duration_histo = None
    _initialized = False


# ── public context manager ─────────────────────────────────────────────────────

@contextmanager
def evaluate_span(
    *,
    agent_role: str,
    source_id: str,
    tenant_id: str,
    session_id: str,
    sensitivity_level: str = "",
):
    """
    Context manager that wraps a context evaluation with an OTEL span and metrics.

    Usage::

        with telemetry.evaluate_span(
            agent_role="analyst", source_id="reports",
            tenant_id=tid, session_id=sid,
        ) as span:
            decision, policy_name, reason = engine.evaluate(request)
            span.set_result(decision=decision.value, policy_name=policy_name, reason=reason)

    When OTEL is not configured the body runs with zero overhead.
    """
    if _tracer is None and _eval_counter is None:
        # fast path: no instrumentation configured
        yield _NoOpSpan()
        return

    start = time.perf_counter()

    if _tracer is not None:
        with _tracer.start_as_current_span("autopil.evaluate") as otel_span:
            otel_span.set_attribute("autopil.agent_role", agent_role)
            otel_span.set_attribute("autopil.source_id",  source_id)
            otel_span.set_attribute("autopil.tenant_id",  tenant_id)
            otel_span.set_attribute("autopil.session_id", session_id)
            if sensitivity_level:
                otel_span.set_attribute("autopil.sensitivity_level", sensitivity_level)

            span = _Span(otel_span)
            try:
                yield span
            finally:
                latency_ms = (time.perf_counter() - start) * 1000
                otel_span.set_attribute("autopil.latency_ms", round(latency_ms, 2))
                _emit_metrics(span, agent_role, source_id, tenant_id, latency_ms)
    else:
        # metrics only — no tracer configured
        span = _Span(None)
        try:
            yield span
        finally:
            latency_ms = (time.perf_counter() - start) * 1000
            _emit_metrics(span, agent_role, source_id, tenant_id, latency_ms)


# ── span result objects ────────────────────────────────────────────────────────

class _Span:
    """Holds evaluation result and forwards it to the underlying OTEL span."""

    __slots__ = ("_otel", "decision", "policy_name")

    def __init__(self, otel_span):
        self._otel      = otel_span
        self.decision   = None
        self.policy_name = None

    def set_result(self, *, decision: str, policy_name: str, reason: str = "") -> None:
        """Set the evaluation outcome. Call this exactly once inside the with-block."""
        self.decision    = decision
        self.policy_name = policy_name
        if self._otel is None:
            return
        self._otel.set_attribute("autopil.decision",    decision)
        self._otel.set_attribute("autopil.policy_name", policy_name)
        if reason:
            self._otel.set_attribute("autopil.reason", reason)
        if decision == "DENY":
            try:
                from opentelemetry.trace import StatusCode
                self._otel.set_status(StatusCode.ERROR, reason or "access denied")
            except ImportError:
                pass


class _NoOpSpan:
    """Zero-overhead stand-in when OTEL is not configured."""

    __slots__ = ()

    def set_result(self, *, decision: str = "", policy_name: str = "", reason: str = "") -> None:
        pass


# ── internal helpers ───────────────────────────────────────────────────────────

def _emit_metrics(span: _Span, agent_role, source_id, tenant_id, latency_ms) -> None:
    if span.decision is None:
        return
    attrs = {
        "autopil.agent_role":  agent_role,
        "autopil.source_id":   source_id,
        "autopil.decision":    span.decision,
        "autopil.policy_name": span.policy_name or "",
        "autopil.tenant_id":   tenant_id,
    }
    if _eval_counter:
        _eval_counter.add(1, attrs)
    if _deny_counter and span.decision == "DENY":
        _deny_counter.add(1, attrs)
    if _duration_histo:
        _duration_histo.record(latency_ms, {
            "autopil.tenant_id": tenant_id,
            "autopil.decision":  span.decision,
        })


def _attach_trace_exporter(tracer_provider, exporter_name: str, endpoint: str) -> None:
    if exporter_name == "console":
        from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
        tracer_provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
        return
    if exporter_name == "otlp" and endpoint:
        # try gRPC first, fall back to HTTP
        for mod in (
            "opentelemetry.exporter.otlp.proto.grpc.trace_exporter",
            "opentelemetry.exporter.otlp.proto.http.trace_exporter",
        ):
            try:
                from importlib import import_module
                OTLPSpanExporter = import_module(mod).OTLPSpanExporter
                from opentelemetry.sdk.trace.export import BatchSpanProcessor
                tracer_provider.add_span_processor(
                    BatchSpanProcessor(OTLPSpanExporter(endpoint=endpoint))
                )
                return
            except ImportError:
                continue


def _make_metric_reader(exporter_name: str, endpoint: str):
    from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
    if exporter_name == "console":
        from opentelemetry.sdk.metrics.export import ConsoleMetricExporter
        return PeriodicExportingMetricReader(
            ConsoleMetricExporter(), export_interval_millis=30_000
        )
    if exporter_name == "otlp" and endpoint:
        for mod in (
            "opentelemetry.exporter.otlp.proto.grpc.metric_exporter",
            "opentelemetry.exporter.otlp.proto.http.metric_exporter",
        ):
            try:
                from importlib import import_module
                OTLPMetricExporter = import_module(mod).OTLPMetricExporter
                return PeriodicExportingMetricReader(
                    OTLPMetricExporter(endpoint=endpoint),
                    export_interval_millis=60_000,
                )
            except ImportError:
                continue
    return None
