"""Pydantic Logfire observability configuration for Hikaflow."""

from __future__ import annotations

import os
from typing import Optional

# Logfire is optional - only import if installed
_logfire_available = False
try:
    import logfire
    _logfire_available = True
except ImportError:
    logfire = None


def _get_version() -> str:
    """Get application version."""
    try:
        from importlib.metadata import version
        return version("autodebug")
    except Exception:
        return "0.0.0"


def configure_observability(
    service_name: str = "hikaflow",
    environment: Optional[str] = None,
    send_to_logfire: Optional[bool] = None,
) -> bool:
    """Configure Pydantic Logfire for observability.

    Args:
        service_name: Service name for traces.
        environment: Deployment environment (auto-detected if not provided).
        send_to_logfire: Whether to send to Logfire cloud. If None, uses
            LOGFIRE_SEND_TO_LOGFIRE env var or defaults to True.

    Returns:
        True if Logfire was configured, False if not available or configuration failed.
    """
    if not _logfire_available:
        return False

    # Check if Logfire token is available
    logfire_token = os.getenv("LOGFIRE_TOKEN")
    if not logfire_token and os.getenv("ENVIRONMENT", "development") != "production":
        # Skip Logfire in development/test without token
        return False

    environment = environment or os.getenv(
        "ENVIRONMENT",
        os.getenv("RAILWAY_ENVIRONMENT", "development")
    )

    # Determine if we should send to Logfire cloud
    if send_to_logfire is None:
        send_to_logfire_env = os.getenv("LOGFIRE_SEND_TO_LOGFIRE", "true")
        send_to_logfire = send_to_logfire_env.lower() in ("true", "1", "yes")

    try:
        logfire.configure(
            service_name=service_name,
            service_version=_get_version(),
            environment=environment,
            send_to_logfire=send_to_logfire,
        )
        return True
    except Exception:
        # Configuration failed (e.g., not authenticated)
        # Fail silently - observability is optional
        return False


def instrument_fastapi(app) -> bool:
    """Instrument FastAPI application with Logfire.

    Args:
        app: FastAPI application instance.

    Returns:
        True if instrumented, False if Logfire not available or instrumentation failed.
    """
    if not _logfire_available:
        return False

    try:
        logfire.instrument_fastapi(app)
        return True
    except RuntimeError:
        # opentelemetry-instrumentation-fastapi not installed
        return False
    except Exception:
        # Any other error, fail silently
        return False


def instrument_pydantic_ai() -> bool:
    """Instrument Pydantic AI agents with Logfire.

    Returns:
        True if instrumented, False if Logfire not available.
    """
    if not _logfire_available:
        return False

    try:
        logfire.instrument_pydantic_ai()
        return True
    except AttributeError:
        # Older version of logfire without pydantic_ai support
        return False


def instrument_httpx() -> bool:
    """Instrument httpx HTTP client with Logfire.

    Returns:
        True if instrumented, False if Logfire not available or instrumentation failed.
    """
    if not _logfire_available:
        return False

    try:
        logfire.instrument_httpx()
        return True
    except RuntimeError:
        # opentelemetry-instrumentation-httpx not installed
        return False
    except Exception:
        # Any other error, fail silently
        return False


def instrument_logging() -> bool:
    """Instrument Python logging with Logfire.

    Returns:
        True if instrumented, False if Logfire not available or instrumentation failed.
    """
    if not _logfire_available:
        return False

    try:
        logfire.instrument_logging()
        return True
    except RuntimeError:
        # Required package not installed
        return False
    except Exception:
        # Any other error, fail silently
        return False


def span(name: str, **attributes):
    """Create a Logfire span context manager.

    Args:
        name: Span name.
        **attributes: Span attributes.

    Returns:
        Span context manager (or no-op if Logfire not available).
    """
    if not _logfire_available:
        from contextlib import nullcontext
        return nullcontext()

    return logfire.span(name, **attributes)


def info(message: str, **kwargs):
    """Log info message to Logfire.

    Args:
        message: Log message.
        **kwargs: Additional log attributes.
    """
    if _logfire_available:
        logfire.info(message, **kwargs)


def warning(message: str, **kwargs):
    """Log warning message to Logfire.

    Args:
        message: Log message.
        **kwargs: Additional log attributes.
    """
    if _logfire_available:
        logfire.warning(message, **kwargs)


def error(message: str, **kwargs):
    """Log error message to Logfire.

    Args:
        message: Log message.
        **kwargs: Additional log attributes.
    """
    if _logfire_available:
        logfire.error(message, **kwargs)


def is_available() -> bool:
    """Check if Logfire is available.

    Returns:
        True if logfire package is installed.
    """
    return _logfire_available


# ============================================================================
# OpenTelemetry Integration (Plan 51 - Gap 5: Observability)
# ============================================================================

_otel_available = False
try:
    from opentelemetry import trace
    from opentelemetry.sdk.resources import Resource
    from opentelemetry.sdk.trace import TracerProvider
    from opentelemetry.sdk.trace.export import BatchSpanProcessor
    _otel_available = True
except ImportError:
    trace = None
    TracerProvider = None
    BatchSpanProcessor = None
    Resource = None


def configure_otel(
    run_id: str,
    service_name: str = "hikaflow-test",
    export_to_user_collector: bool = True,
) -> bool:
    """Configure OpenTelemetry with Hikaflow context.

    Sets up OpenTelemetry with:
    - Hikaflow run_id as a resource attribute
    - Export to user's existing collector if configured (via OTEL_EXPORTER_OTLP_ENDPOINT)
    - Export to Hikaflow transcript format

    Args:
        run_id: The Hikaflow run ID for this capture session
        service_name: Service name for OpenTelemetry resource
        export_to_user_collector: If True, also export to OTEL_EXPORTER_OTLP_ENDPOINT

    Returns:
        True if configured, False if OpenTelemetry not available
    """
    if not _otel_available:
        return False

    from opentelemetry.sdk.resources import SERVICE_NAME, SERVICE_VERSION

    # Create resource with Hikaflow context
    resource = Resource.create({
        SERVICE_NAME: service_name,
        SERVICE_VERSION: _get_version(),
        "hikaflow.run_id": run_id,
    })

    provider = TracerProvider(resource=resource)

    # Export to user's existing OTLP collector if configured
    if export_to_user_collector and os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT"):
        try:
            from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
            provider.add_span_processor(
                BatchSpanProcessor(OTLPSpanExporter())
            )
        except ImportError:
            pass  # OTLP exporter not installed

    # Export to Hikaflow transcript format
    try:
        from autodebug_probe.otel_integration import HikaflowSpanExporter
        provider.add_span_processor(
            BatchSpanProcessor(HikaflowSpanExporter(run_id))
        )
    except ImportError:
        pass  # Probe not installed

    trace.set_tracer_provider(provider)
    return True


def get_trace_context() -> dict:
    """Get current W3C trace context for propagation.

    Returns:
        Dict with trace_id, span_id, and parent_span_id (if available)
    """
    if not _otel_available:
        return {}

    span = trace.get_current_span()
    if not span or not span.is_recording():
        return {}

    ctx = span.get_span_context()
    return {
        "trace_id": format(ctx.trace_id, "032x") if ctx.trace_id else None,
        "span_id": format(ctx.span_id, "016x") if ctx.span_id else None,
    }


def inject_trace_context(headers: dict) -> dict:
    """Inject W3C trace context into HTTP headers.

    Args:
        headers: Headers dict to inject into

    Returns:
        Headers dict with trace context added
    """
    if not _otel_available:
        return headers

    try:
        from opentelemetry.propagate import inject
        from opentelemetry.propagators.textmap import DefaultSetter

        inject(headers, setter=DefaultSetter())
    except ImportError:
        pass

    return headers


def extract_trace_context(headers: dict):
    """Extract W3C trace context from HTTP headers.

    Args:
        headers: Headers dict to extract from

    Returns:
        OpenTelemetry context object
    """
    if not _otel_available:
        return None

    try:
        from opentelemetry.propagate import extract
        return extract(headers)
    except ImportError:
        return None


# ============================================================================
# Sentry Integration (Plan 51 - Gap 5: Observability)
# ============================================================================

_sentry_available = False
try:
    import sentry_sdk
    _sentry_available = True
except ImportError:
    sentry_sdk = None


def configure_sentry_integration(run_id: str, replay_url: Optional[str] = None) -> bool:
    """Add Hikaflow context to Sentry events.

    Enriches Sentry error events with:
    - hikaflow.run_id tag
    - Hikaflow context with run_id and replay URL
    - OpenTelemetry trace correlation (if available)

    Args:
        run_id: The Hikaflow run ID
        replay_url: Optional URL to the Hikaflow replay for this run

    Returns:
        True if Sentry is configured, False if not available
    """
    if not _sentry_available:
        return False

    # Check if Sentry is initialized
    if not sentry_sdk.Hub.current.client:
        return False

    # Add Hikaflow context to all events
    with sentry_sdk.configure_scope() as scope:
        scope.set_tag("hikaflow.run_id", run_id)

        context = {
            "run_id": run_id,
        }
        if replay_url:
            context["replay_url"] = replay_url
        else:
            context["replay_url"] = f"https://app.hikaflow.com/runs/{run_id}"

        scope.set_context("hikaflow", context)

    return True


def enrich_sentry_with_trace_context() -> bool:
    """Link Sentry errors to OpenTelemetry traces.

    Returns:
        True if enrichment was applied, False if not available
    """
    if not _sentry_available or not _otel_available:
        return False

    trace_ctx = get_trace_context()
    if not trace_ctx.get("trace_id"):
        return False

    with sentry_sdk.configure_scope() as scope:
        scope.set_context("otel", {
            "trace_id": trace_ctx.get("trace_id"),
            "span_id": trace_ctx.get("span_id"),
        })

    return True


# ============================================================================
# Profiling with py-spy (Plan 51 - Gap 5: Observability)
# ============================================================================

class ProfilerSession:
    """Manages py-spy profiling for a test run.

    py-spy is a sampling profiler that attaches to running Python
    processes. It outputs Speedscope-compatible JSON for visualization.
    """

    def __init__(self, run_id: str, output_dir: Optional[str] = None):
        """Initialize profiler session.

        Args:
            run_id: The run ID for this session
            output_dir: Output directory (defaults to .hikaflow/runs/{run_id})
        """
        import subprocess

        self.run_id = run_id
        self.output_dir = output_dir or f".hikaflow/runs/{run_id}"
        self.output_path = f"{self.output_dir}/profile.speedscope.json"
        self._process: Optional[subprocess.Popen] = None
        self._target_pid: Optional[int] = None

    def start(self, target_pid: Optional[int] = None) -> bool:
        """Start py-spy profiler.

        Args:
            target_pid: PID to profile (defaults to current process)

        Returns:
            True if profiler started, False if py-spy not available
        """
        import shutil
        import subprocess

        # Check if py-spy is available
        if not shutil.which("py-spy"):
            return False

        # Ensure output directory exists
        os.makedirs(self.output_dir, exist_ok=True)

        # Use current process if no PID specified
        if target_pid is None:
            target_pid = os.getpid()
        self._target_pid = target_pid

        try:
            self._process = subprocess.Popen(
                [
                    "py-spy", "record",
                    "--pid", str(target_pid),
                    "--output", self.output_path,
                    "--format", "speedscope",
                    "--rate", "100",  # 100 samples/sec
                ],
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
            )
            return True
        except Exception:
            return False

    def stop(self) -> Optional[str]:
        """Stop py-spy profiler.

        Returns:
            Path to profile file, or None if not running
        """
        if self._process:
            self._process.terminate()
            try:
                self._process.wait(timeout=5)
            except Exception:
                self._process.kill()
            self._process = None

            if os.path.exists(self.output_path):
                return self.output_path

        return None

    @property
    def speedscope_url(self) -> Optional[str]:
        """Get URL to view profile in Speedscope.

        Returns:
            Speedscope.app URL with profile, or None if not available
        """
        if not os.path.exists(self.output_path):
            return None

        # For local files, can't use speedscope.app directly
        # Return None, frontend should embed the viewer
        return None

    def __enter__(self):
        self.start()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.stop()
        return False


def start_profiler(run_id: str, output_dir: Optional[str] = None) -> ProfilerSession:
    """Start a profiler session for the current process.

    Args:
        run_id: The run ID for this session
        output_dir: Optional output directory

    Returns:
        ProfilerSession instance (call .stop() when done)
    """
    session = ProfilerSession(run_id, output_dir)
    session.start()
    return session


def is_otel_available() -> bool:
    """Check if OpenTelemetry is available.

    Returns:
        True if opentelemetry-sdk is installed
    """
    return _otel_available


def is_sentry_available() -> bool:
    """Check if Sentry is available.

    Returns:
        True if sentry-sdk is installed
    """
    return _sentry_available
