from __future__ import annotations

import logging
import os
import warnings
from dataclasses import dataclass
from importlib import metadata
from typing import Any, Literal

from collections.abc import Mapping, Sequence

from opentelemetry import _events, trace
from opentelemetry._logs import get_logger_provider, set_logger_provider
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter as HTTPLogExporter
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter as HTTPSpanExporter
from opentelemetry.sdk._events import EventLoggerProvider
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, LogExportResult, LogExporter, SimpleLogRecordProcessor
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import ReadableSpan, TracerProvider
from opentelemetry.sdk.trace.export import (
    BatchSpanProcessor,
    SimpleSpanProcessor,
    SpanExportResult,
    SpanExporter,
)

logger = logging.getLogger("impact.sdk.otel")

InitMode = Literal["auto", "bootstrap", "attach"]


def _success_logging_enabled() -> bool:
    return os.getenv("IMPACT_EXPORTER_DEBUG", "").lower() in ("1", "true", "yes", "on")


class _LoggingSpanExporter(SpanExporter):
    """Wrap a SpanExporter to emit debug logging on successful exports."""

    def __init__(self, delegate: SpanExporter, *, target: str | None = None) -> None:
        self._delegate = delegate
        self._target = target or getattr(delegate, "endpoint", None)

    def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
        span_count = len(spans)
        target_suffix = f" to {self._target}" if self._target else ""
        try:
            result = self._delegate.export(spans)
        except Exception:
            logger.exception("Failed to export %d spans%s", span_count, target_suffix)
            return SpanExportResult.FAILURE

        if result == SpanExportResult.SUCCESS:
            if _success_logging_enabled():
                logger.debug("Exported %d spans%s", span_count, target_suffix)
        else:
            logger.warning("Span exporter returned %s while sending %d spans%s", result.name, span_count, target_suffix)
        return result

    def shutdown(self) -> None:
        self._delegate.shutdown()

    def force_flush(self, timeout_millis: int = 30000) -> bool:
        return self._delegate.force_flush(timeout_millis=timeout_millis)


class _LoggingLogExporter(LogExporter):
    """Wrap a LogExporter to emit debug logging on successful exports."""

    def __init__(self, delegate: LogExporter, *, target: str | None = None) -> None:
        self._delegate = delegate
        self._target = target or getattr(delegate, "endpoint", None)

    def export(self, batch) -> LogExportResult:
        log_count = len(batch)
        target_suffix = f" to {self._target}" if self._target else ""
        try:
            result = self._delegate.export(batch)
        except Exception:
            logger.exception("Failed to export %d logs%s", log_count, target_suffix)
            return LogExportResult.FAILURE

        if result == LogExportResult.SUCCESS:
            if _success_logging_enabled():
                logger.debug("Exported %d logs%s", log_count, target_suffix)
        else:
            logger.warning("Log exporter returned %s while sending %d logs%s", result.name, log_count, target_suffix)
        return result

    def shutdown(self) -> None:
        self._delegate.shutdown()

    def force_flush(self, timeout_millis: int = 30000) -> bool:
        return self._delegate.force_flush(timeout_millis=timeout_millis)

def _create_trace_exporter(api_endpoint: str, headers: Mapping[str, str]) -> SpanExporter:
    """Create OTLP trace exporter. Appends /v1/traces to the endpoint."""
    endpoint = f"{api_endpoint.rstrip('/')}/v1/traces"
    exporter = HTTPSpanExporter(endpoint=endpoint, headers=dict(headers))
    return _LoggingSpanExporter(exporter, target=endpoint)

def _create_log_exporter(api_endpoint: str, headers: Mapping[str, str]):
    """Create OTLP log exporter. Appends /v1/logs to the endpoint."""
    endpoint = f"{api_endpoint.rstrip('/')}/v1/logs"
    exporter = HTTPLogExporter(endpoint=endpoint, headers=dict(headers))
    return _LoggingLogExporter(exporter, target=endpoint)


def _is_real_tracer_provider(provider: Any) -> bool:
    if provider is None:
        return False
    name = type(provider).__name__
    if name in ("ProxyTracerProvider", "_DefaultTracerProvider"):
        return False
    return hasattr(provider, "add_span_processor")


def _is_real_logger_provider(provider: Any) -> bool:
    if provider is None:
        return False
    name = type(provider).__name__
    if name in ("ProxyLoggerProvider", "_DefaultLoggerProvider"):
        return False
    return hasattr(provider, "add_log_record_processor")


def _is_proxy_event_logger_provider(provider: Any) -> bool:
    if provider is None:
        return True
    name = type(provider).__name__
    return name in ("ProxyEventLoggerProvider", "_DefaultEventLoggerProvider")


@dataclass
class TracingRuntime:
    """Tracks what Impact installed so shutdown is safe in BYO/attach mode."""

    mode: InitMode
    attached: bool

    tracer_provider: Any
    created_tracer_provider: bool
    span_processor: Any | None

    logger_provider: Any | None
    created_logger_provider: bool
    log_processor: Any | None

    def shutdown(self) -> None:
        # Flush first (best-effort)
        try:
            if hasattr(self.tracer_provider, "force_flush"):
                self.tracer_provider.force_flush()
        except Exception:
            pass
        try:
            if self.logger_provider is not None and hasattr(self.logger_provider, "force_flush"):
                self.logger_provider.force_flush()
        except Exception:
            pass

        # BOOTSTRAP: we own providers -> shutdown providers
        if self.created_tracer_provider and hasattr(self.tracer_provider, "shutdown"):
            try:
                self.tracer_provider.shutdown()
            except Exception:
                pass
        elif self.span_processor is not None:
            # ATTACH: do not shutdown user's provider
            try:
                self.span_processor.shutdown()
            except Exception:
                pass

        if self.created_logger_provider and self.logger_provider is not None and hasattr(self.logger_provider, "shutdown"):
            try:
                self.logger_provider.shutdown()
            except Exception:
                pass
        elif self.log_processor is not None:
            try:
                self.log_processor.shutdown()
            except Exception:
                pass

def setup_tracing(
    *,
    api_endpoint: str,
    headers: Mapping[str, str],
    mode: InitMode = "auto",
    disable_batch: bool = False,
    resource_attributes: Mapping[str, str] | None = None,
    disable_logs: bool = False,
) -> TracingRuntime:
    """Setup OTLP OpenTelemetry export to Impact.

    Supports three modes:
      - auto: attach if a real TracerProvider exists, else bootstrap
      - bootstrap: always create and set new providers (clobbers global providers)
      - attach: require an existing TracerProvider; do not replace it
    """
    # Resource is only applied when we create a provider.
    try:
        res_attrs: dict[str, str] = {
            "service.name": os.getenv("OTEL_SERVICE_NAME") or "impact-app",
            "impact.sdk.name": "impact",
            "impact.sdk.version": metadata.version("impact"),
        }
    except Exception:
        res_attrs = {}
    if resource_attributes:
        res_attrs |= dict(resource_attributes)
    resource = Resource.create(res_attrs)

    existing_tp = trace.get_tracer_provider()
    can_attach_tp = _is_real_tracer_provider(existing_tp)
    logger.info(
        "TracerProvider check: existing=%s, can_attach=%s, mode=%s",
        type(existing_tp).__name__,
        can_attach_tp,
        mode,
    )

    if mode == "attach" and not can_attach_tp:
        raise ValueError("mode='attach' but no existing TracerProvider found")

    should_attach_tp = (mode == "attach") or (mode == "auto" and can_attach_tp)

    tracer_provider: Any
    created_tracer_provider = False

    if should_attach_tp:
        tracer_provider = existing_tp
    else:
        tracer_provider = TracerProvider(resource=resource)
        created_tracer_provider = True

    # Always add an Impact OTLP exporter processor (even in attach mode).
    trace_exporter = _create_trace_exporter(api_endpoint, headers)
    span_processor = SimpleSpanProcessor(trace_exporter) if disable_batch else BatchSpanProcessor(trace_exporter)

    if should_attach_tp:
        try:
            tracer_provider.add_span_processor(span_processor)  # type: ignore[attr-defined]
            logger.debug(
                "Attached Impact span processor to existing TracerProvider %s",
                type(tracer_provider).__name__,
            )
        except Exception as e:
            logger.warning(
                "Failed to attach to existing TracerProvider (%s): %s. Falling back to bootstrap.",
                type(tracer_provider).__name__,
                e,
                exc_info=True,
            )
            if mode == "attach":
                raise
            # auto fallback: bootstrap
            tracer_provider = TracerProvider(resource=resource)
            tracer_provider.add_span_processor(span_processor)
            trace.set_tracer_provider(tracer_provider)
            created_tracer_provider = True
            should_attach_tp = False
    else:
        tracer_provider.add_span_processor(span_processor)
        trace.set_tracer_provider(tracer_provider)

    # Logs setup
    logger_provider: Any | None = None
    created_logger_provider = False
    log_processor: Any | None = None

    if not disable_logs:
        log_exporter = _create_log_exporter(api_endpoint, headers)
        existing_lp = get_logger_provider()
        can_attach_lp = _is_real_logger_provider(existing_lp)

        if mode == "bootstrap" or not can_attach_lp:
            # If no real logger provider exists, we can safely set ours (even if tracer is attached).
            logger_provider = LoggerProvider(resource=resource)
            log_processor = (
                SimpleLogRecordProcessor(log_exporter) if disable_batch else BatchLogRecordProcessor(log_exporter)
            )
            logger_provider.add_log_record_processor(log_processor)
            set_logger_provider(logger_provider)
            created_logger_provider = True
        else:
            # Attach to existing logger provider
            logger_provider = existing_lp
            log_processor = (
                SimpleLogRecordProcessor(log_exporter) if disable_batch else BatchLogRecordProcessor(log_exporter)
            )
            try:
                logger_provider.add_log_record_processor(log_processor)  # type: ignore[attr-defined]
            except Exception:
                # Best-effort: don't fail init due to logs
                log_processor = None

        # Ensure events API has a provider, but avoid clobbering an existing one.
        try:
            with warnings.catch_warnings():
                warnings.simplefilter("ignore", DeprecationWarning)
                current_event_provider = _events.get_event_logger_provider()
            if _is_proxy_event_logger_provider(current_event_provider) and logger_provider is not None:
                event_logger_provider = EventLoggerProvider(logger_provider=logger_provider)
                _events.set_event_logger_provider(event_logger_provider)
        except Exception:
            # best-effort
            pass

    return TracingRuntime(
        mode=mode,
        attached=should_attach_tp and not created_tracer_provider,
        tracer_provider=tracer_provider,
        created_tracer_provider=created_tracer_provider,
        span_processor=span_processor if should_attach_tp and not created_tracer_provider else None,
        logger_provider=logger_provider,
        created_logger_provider=created_logger_provider,
        log_processor=log_processor if not created_logger_provider else None,
    )
