"""OpenTelemetry flakiness signals.

Exports flakiness as first-class observability signals:
- Metrics: Gauge and counter metrics for flake rates
- Traces: Spans for flaky test executions
- Events: Log events for flakiness detection

These signals can be exported to any OTLP-compatible backend
(Grafana, Datadog, Honeycomb, New Relic, etc.)
"""

from __future__ import annotations

from collections.abc import Generator
from contextlib import contextmanager
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any, Dict, Optional

# Check for OpenTelemetry availability
_otel_available = False
_otel_metrics_available = False

try:
    from opentelemetry import trace
    from opentelemetry.sdk.trace import TracerProvider
    from opentelemetry.trace import SpanKind, Status, StatusCode
    _otel_available = True
except ImportError:
    trace = None
    Status = None
    StatusCode = None
    SpanKind = None
    TracerProvider = None

try:
    from opentelemetry import metrics
    from opentelemetry.sdk.metrics import MeterProvider
    _otel_metrics_available = True
except ImportError:
    metrics = None
    MeterProvider = None


@dataclass
class FlakinessEvent:
    """A flakiness event for export."""

    event_type: str  # "detected", "fixed", "regression"
    test_name: str
    test_file: Optional[str]
    project_id: Optional[str]
    run_id: Optional[str]
    timestamp: datetime
    attributes: Dict[str, Any]


class FlakinessSignals:
    """OpenTelemetry signal exporter for flakiness.

    Exports flakiness as OTLP signals:
    - hikaflow.flake_rate (gauge): Current flake rate percentage
    - hikaflow.flake_count (counter): Total flakes detected
    - hikaflow.fix_count (counter): Total flakes fixed
    - hikaflow.mttf (gauge): Mean time to fix in seconds
    - hikaflow.stability_score (gauge): CI stability score (0-100)

    Also creates spans for flaky test runs with attributes:
    - test.name, test.file, project.id
    - flaky.category, flaky.root_cause
    - divergence.type, divergence.location
    """

    METER_NAME = "hikaflow.flakiness"
    TRACER_NAME = "hikaflow.flakiness"

    def __init__(
        self,
        service_name: str = "hikaflow",
        project_id: Optional[str] = None,
    ):
        """Initialize flakiness signals.

        Args:
            service_name: Service name for OTLP resource
            project_id: Default project ID for metrics
        """
        self.service_name = service_name
        self.project_id = project_id
        self._tracer = None
        self._meter = None
        self._metrics_initialized = False

        # Metric instruments
        self._flake_rate_gauge = None
        self._flake_counter = None
        self._fix_counter = None
        self._mttf_gauge = None
        self._stability_gauge = None

        self._init_otel()

    def _init_otel(self) -> None:
        """Initialize OpenTelemetry instruments."""
        if _otel_available:
            # Get tracer from global provider
            try:
                self._tracer = trace.get_tracer(self.TRACER_NAME)
            except Exception:
                pass

        if _otel_metrics_available:
            try:
                # Get meter from global provider
                meter = metrics.get_meter(self.METER_NAME)

                # Create metric instruments
                self._flake_rate_gauge = meter.create_gauge(
                    "hikaflow.flake_rate",
                    unit="%",
                    description="Current flake rate as percentage",
                )

                self._flake_counter = meter.create_counter(
                    "hikaflow.flake_count",
                    unit="1",
                    description="Total number of flaky test runs detected",
                )

                self._fix_counter = meter.create_counter(
                    "hikaflow.fix_count",
                    unit="1",
                    description="Total number of flakes fixed",
                )

                self._mttf_gauge = meter.create_gauge(
                    "hikaflow.mttf_seconds",
                    unit="s",
                    description="Mean time to fix in seconds",
                )

                self._stability_gauge = meter.create_gauge(
                    "hikaflow.stability_score",
                    unit="1",
                    description="CI stability score (0-100)",
                )

                self._metrics_initialized = True
            except Exception:
                pass

    def record_flaky_run(
        self,
        test_name: str,
        test_file: Optional[str] = None,
        project_id: Optional[str] = None,
        run_id: Optional[str] = None,
        category: Optional[str] = None,
        root_cause: Optional[str] = None,
        divergence_type: Optional[str] = None,
        divergence_location: Optional[str] = None,
        attributes: Optional[Dict[str, Any]] = None,
    ) -> None:
        """Record a flaky test run.

        Creates a span and increments the flake counter.

        Args:
            test_name: Name of the flaky test
            test_file: File containing the test
            project_id: Project identifier
            run_id: Run identifier
            category: Flakiness category (timing, ordering, etc.)
            root_cause: Root cause description
            divergence_type: Type of divergence detected
            divergence_location: Location of divergence
            attributes: Additional span attributes
        """
        project_id = project_id or self.project_id

        # Increment counter
        if self._flake_counter:
            labels = {
                "project_id": project_id or "unknown",
                "category": category or "unknown",
            }
            self._flake_counter.add(1, labels)

        # Create span for the flaky run
        if self._tracer:
            span_attributes = {
                "test.name": test_name,
                "test.flaky": True,
                "hikaflow.signal_type": "flaky_run",
            }

            if test_file:
                span_attributes["test.file"] = test_file
            if project_id:
                span_attributes["project.id"] = project_id
            if run_id:
                span_attributes["hikaflow.run_id"] = run_id
            if category:
                span_attributes["flaky.category"] = category
            if root_cause:
                span_attributes["flaky.root_cause"] = root_cause
            if divergence_type:
                span_attributes["divergence.type"] = divergence_type
            if divergence_location:
                span_attributes["divergence.location"] = divergence_location

            if attributes:
                span_attributes.update(attributes)

            with self._tracer.start_as_current_span(
                f"flaky_test:{test_name}",
                kind=SpanKind.INTERNAL,
                attributes=span_attributes,
            ) as span:
                span.set_status(Status(StatusCode.ERROR, "Test is flaky"))
                span.add_event(
                    "flakiness_detected",
                    attributes={
                        "category": category or "unknown",
                        "timestamp": datetime.now(timezone.utc).isoformat(),
                    },
                )

    def record_flake_fixed(
        self,
        test_name: str,
        project_id: Optional[str] = None,
        fix_type: str = "manual",
        time_to_fix_seconds: Optional[float] = None,
    ) -> None:
        """Record a flake being fixed.

        Args:
            test_name: Name of the fixed test
            project_id: Project identifier
            fix_type: Type of fix (manual, auto, verified)
            time_to_fix_seconds: Time taken to fix in seconds
        """
        project_id = project_id or self.project_id

        if self._fix_counter:
            labels = {
                "project_id": project_id or "unknown",
                "fix_type": fix_type,
            }
            self._fix_counter.add(1, labels)

        if self._tracer:
            with self._tracer.start_as_current_span(
                f"flake_fixed:{test_name}",
                kind=SpanKind.INTERNAL,
                attributes={
                    "test.name": test_name,
                    "hikaflow.signal_type": "flake_fixed",
                    "fix.type": fix_type,
                },
            ) as span:
                if time_to_fix_seconds:
                    span.set_attribute("fix.time_seconds", time_to_fix_seconds)
                span.set_status(Status(StatusCode.OK, "Flake fixed"))

    def update_metrics(
        self,
        flake_rate: Optional[float] = None,
        mttf_seconds: Optional[float] = None,
        stability_score: Optional[float] = None,
        project_id: Optional[str] = None,
    ) -> None:
        """Update gauge metrics.

        Args:
            flake_rate: Current flake rate percentage (0-100)
            mttf_seconds: Mean time to fix in seconds
            stability_score: CI stability score (0-100)
            project_id: Project identifier
        """
        project_id = project_id or self.project_id
        labels = {"project_id": project_id or "unknown"}

        if flake_rate is not None and self._flake_rate_gauge:
            self._flake_rate_gauge.set(flake_rate, labels)

        if mttf_seconds is not None and self._mttf_gauge:
            self._mttf_gauge.set(mttf_seconds, labels)

        if stability_score is not None and self._stability_gauge:
            self._stability_gauge.set(stability_score, labels)

    @contextmanager
    def flaky_test_span(
        self,
        test_name: str,
        test_file: Optional[str] = None,
        project_id: Optional[str] = None,
    ) -> Generator[Any, None, None]:
        """Context manager for a potentially flaky test span.

        Usage:
            with signals.flaky_test_span("test_my_function") as span:
                # Run test
                if is_flaky:
                    span.set_attribute("test.flaky", True)
                    span.set_status(StatusCode.ERROR, "Test is flaky")

        Args:
            test_name: Name of the test
            test_file: File containing the test
            project_id: Project identifier

        Yields:
            The span object (or None if OTel not available)
        """
        if not self._tracer:
            yield None
            return

        project_id = project_id or self.project_id
        attributes = {
            "test.name": test_name,
            "hikaflow.signal_type": "test_run",
        }
        if test_file:
            attributes["test.file"] = test_file
        if project_id:
            attributes["project.id"] = project_id

        with self._tracer.start_as_current_span(
            f"test:{test_name}",
            kind=SpanKind.INTERNAL,
            attributes=attributes,
        ) as span:
            try:
                yield span
            except Exception as e:
                span.set_status(Status(StatusCode.ERROR, str(e)))
                span.record_exception(e)
                raise

    def is_available(self) -> bool:
        """Check if OTel signals are available."""
        return _otel_available or _otel_metrics_available


# Module-level singleton
_signals: Optional[FlakinessSignals] = None


def get_flakiness_signals(
    service_name: str = "hikaflow",
    project_id: Optional[str] = None,
) -> FlakinessSignals:
    """Get or create the global flakiness signals instance.

    Args:
        service_name: Service name for OTLP
        project_id: Default project ID

    Returns:
        FlakinessSignals singleton
    """
    global _signals
    if _signals is None:
        _signals = FlakinessSignals(service_name, project_id)
    return _signals


# Convenience functions


def record_flaky_run(
    test_name: str,
    test_file: Optional[str] = None,
    project_id: Optional[str] = None,
    run_id: Optional[str] = None,
    category: Optional[str] = None,
    root_cause: Optional[str] = None,
    **kwargs,
) -> None:
    """Record a flaky test run (convenience function)."""
    signals = get_flakiness_signals()
    signals.record_flaky_run(
        test_name=test_name,
        test_file=test_file,
        project_id=project_id,
        run_id=run_id,
        category=category,
        root_cause=root_cause,
        attributes=kwargs,
    )


def record_flake_detected(
    test_name: str,
    category: str,
    project_id: Optional[str] = None,
) -> None:
    """Record flakiness detection (alias for record_flaky_run)."""
    record_flaky_run(
        test_name=test_name,
        category=category,
        project_id=project_id,
    )


def record_flake_fixed(
    test_name: str,
    project_id: Optional[str] = None,
    fix_type: str = "manual",
    time_to_fix_seconds: Optional[float] = None,
) -> None:
    """Record a flake being fixed (convenience function)."""
    signals = get_flakiness_signals()
    signals.record_flake_fixed(
        test_name=test_name,
        project_id=project_id,
        fix_type=fix_type,
        time_to_fix_seconds=time_to_fix_seconds,
    )


def get_flakiness_metrics() -> Dict[str, Any]:
    """Get current flakiness metrics as a dict.

    Returns:
        Dict with metric names and values
    """
    # This is a placeholder - in practice you'd query the metrics backend
    return {
        "flake_rate": 0.0,
        "flake_count": 0,
        "fix_count": 0,
        "mttf_seconds": 0.0,
        "stability_score": 100.0,
    }


@contextmanager
def start_flaky_test_span(
    test_name: str,
    test_file: Optional[str] = None,
    project_id: Optional[str] = None,
) -> Generator[Any, None, None]:
    """Start a span for a potentially flaky test (convenience function)."""
    signals = get_flakiness_signals()
    with signals.flaky_test_span(test_name, test_file, project_id) as span:
        yield span
