from __future__ import annotations

import atexit
import json
import os
import re
import sys
import time
from collections.abc import Mapping
from contextvars import ContextVar
from datetime import datetime, timezone
from threading import Lock, Thread
from typing import Any

from opentelemetry.context import Context
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator
from opentelemetry.trace import Status, StatusCode

_TRACER_PROVIDER: Any | None = None
_TRACER: Any | None = None
_TRACER_ENDPOINT = ""
_TRACER_LOCK = Lock()
_CURRENT_TRACE_ID: ContextVar[int | None] = ContextVar("structured_otlp_trace_id", default=None)

_HEX_RE = re.compile(r"[^0-9a-fA-F]+")
_ISO_EVENT_TIMESTAMP_RE = re.compile(
    r"^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d{1,9}))?Z$"
)
_LEGACY_EVENT_TIMESTAMP_RE = re.compile(r"^(\d{2}/\d{2}/\d{4} \d{2}:\d{2}:\d{2}),(\d{1,9})$")
_EXCLUDED_ATTRIBUTE_KEYS = {"original_message_id"}
_OMIT_EMPTY_ATTRIBUTE_KEYS = {"latencia_TFFB_STT_TTS", "duracao_audio", "interrupcao"}


class _SessionTraceIdGenerator(IdGenerator):
    def __init__(self) -> None:
        self._fallback = RandomIdGenerator()

    def generate_span_id(self) -> int:
        return self._fallback.generate_span_id()

    def generate_trace_id(self) -> int:
        trace_id = _CURRENT_TRACE_ID.get()
        if trace_id is not None:
            return trace_id
        return self._fallback.generate_trace_id()


_ID_GENERATOR = _SessionTraceIdGenerator()


def trace_id_from_session_id(session_id: Any) -> str:
    trace_id = _HEX_RE.sub("", str(session_id or "")).lower()
    if len(trace_id) != 32 or trace_id == ("0" * 32):
        return ""
    return trace_id


def _configured_endpoint() -> str:
    return (os.getenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "") or "").strip()


def _service_name() -> str:
    return (
        os.getenv("OTEL_SERVICE_NAME", "")
        or os.getenv("APP_LOGGER_NAME", "")
        or "agent_internal_stt"
    ).strip()


def shutdown_structured_otlp() -> None:
    """Drena e encerra o provider corrente no shutdown do processo."""
    global _TRACER_PROVIDER, _TRACER, _TRACER_ENDPOINT

    with _TRACER_LOCK:
        provider = _TRACER_PROVIDER
        _TRACER_PROVIDER = None
        _TRACER = None
        _TRACER_ENDPOINT = ""

    if provider is not None:
        try:
            provider.shutdown()
        except Exception as exc:
            print(
                f"STRUCTURED_EVENT_OTLP_SHUTDOWN_FAIL | error={type(exc).__name__}: {exc}",
                file=sys.stderr,
            )


def force_flush_structured_otlp(timeout_millis: int = 5_000) -> bool:
    """Força o envio pendente fora do caminho de áudio, útil em teardown/testes."""
    with _TRACER_LOCK:
        provider = _TRACER_PROVIDER
    if provider is None:
        return True
    try:
        return bool(provider.force_flush(timeout_millis=max(0, int(timeout_millis))))
    except Exception as exc:
        print(
            f"STRUCTURED_EVENT_OTLP_FLUSH_FAIL | error={type(exc).__name__}: {exc}",
            file=sys.stderr,
        )
        return False


def _shutdown_replaced_provider(provider: Any) -> None:
    try:
        provider.shutdown()
    except Exception:
        pass


def _tracer() -> Any | None:
    global _TRACER_PROVIDER, _TRACER, _TRACER_ENDPOINT

    endpoint = _configured_endpoint()
    if not endpoint:
        return None

    with _TRACER_LOCK:
        if _TRACER is not None and _TRACER_ENDPOINT == endpoint:
            return _TRACER

        try:
            resource = Resource.create(attributes={SERVICE_NAME: _service_name()})
            provider = TracerProvider(resource=resource, id_generator=_ID_GENERATOR)
            provider.add_span_processor(
                BatchSpanProcessor(OTLPSpanExporter(endpoint=endpoint))
            )

            previous_provider = _TRACER_PROVIDER
            _TRACER_PROVIDER = provider
            _TRACER = provider.get_tracer("app.utils.structured_otlp")
            _TRACER_ENDPOINT = endpoint
            tracer = _TRACER
        except Exception as exc:
            print(
                f"STRUCTURED_EVENT_OTLP_DISABLED | error={type(exc).__name__}: {exc}",
                file=sys.stderr,
            )
            return None

    if previous_provider is not None:
        Thread(
            target=_shutdown_replaced_provider,
            args=(previous_provider,),
            name="structured-otlp-old-provider-shutdown",
            daemon=True,
        ).start()
    return tracer


atexit.register(shutdown_structured_otlp)


def _parse_event_timestamp(value: Any) -> int | None:
    raw = str(value or "").strip()
    iso_match = _ISO_EVENT_TIMESTAMP_RE.match(raw)
    if iso_match is not None:
        dt = datetime.strptime(iso_match.group(1), "%Y-%m-%dT%H:%M:%S").replace(tzinfo=timezone.utc)
        fractional_ns = int((iso_match.group(2) or "").ljust(9, "0")[:9])
        return int(dt.timestamp()) * 1_000_000_000 + fractional_ns

    legacy_match = _LEGACY_EVENT_TIMESTAMP_RE.match(raw)
    if legacy_match is None:
        return None

    dt = datetime.strptime(legacy_match.group(1), "%d/%m/%Y %H:%M:%S")
    fractional_ns = int(legacy_match.group(2).ljust(9, "0")[:9])
    return int(dt.timestamp()) * 1_000_000_000 + fractional_ns


def _event_times(event: Mapping[str, Any]) -> tuple[int, int]:
    start_ns = _parse_event_timestamp(event.get("dat_hora_inicio")) or time.time_ns()
    end_ns = _parse_event_timestamp(event.get("dat_hora_fim")) or start_ns
    if end_ns < start_ns:
        return start_ns, start_ns
    return start_ns, end_ns


def _attribute_value(value: Any) -> str | bool | int | float | None:
    if value is None:
        return None
    if isinstance(value, str | bool | int | float):
        return value
    try:
        return json.dumps(value, ensure_ascii=False)
    except Exception:
        return str(value)


def _attributes(event: Mapping[str, Any]) -> dict[str, str | bool | int | float]:
    attrs: dict[str, str | bool | int | float] = {}
    for key, value in event.items():
        if str(key) in _EXCLUDED_ATTRIBUTE_KEYS:
            continue
        if str(key) in _OMIT_EMPTY_ATTRIBUTE_KEYS and value == "":
            continue
        attr_value = _attribute_value(value)
        if attr_value is not None:
            attrs[str(key)] = attr_value
    return attrs


def publish_structured_span(event: Mapping[str, Any]) -> None:
    trace_id = trace_id_from_session_id(event.get("session_id"))
    if not trace_id:
        return

    tracer = _tracer()
    if tracer is None:
        return

    try:
        start_ns, end_ns = _event_times(event)
        tipo_evento = str(event.get("tipo_evento") or "unknown").strip() or "unknown"

        token = _CURRENT_TRACE_ID.set(int(trace_id, 16))
        try:
            span = tracer.start_span(
                f"structured_log.{tipo_evento}",
                context=Context(),
                attributes=_attributes(event),
                start_time=start_ns,
            )
        finally:
            _CURRENT_TRACE_ID.reset(token)

        try:
            erro_msg = str(event.get("erro_msg") or "").strip()
            if erro_msg:
                span.set_status(Status(StatusCode.ERROR, erro_msg))
        finally:
            span.end(end_time=end_ns)
    except Exception as exc:
        print(f"STRUCTURED_EVENT_OTLP_PUBLISH_FAIL | error={type(exc).__name__}: {exc}", file=sys.stderr)
