"""Unified step tracing with manifest enrichment.

This module provides a single entry point for all step span creation,
replacing the fragmented tracer usage across step_decorator_factory.py
and step_context.py.
"""

import inspect
import logging
from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Optional, Tuple

from opentelemetry import trace
from opentelemetry.trace import Span, Status, StatusCode

logger = logging.getLogger(__name__)

# Single tracer name for ALL supervisor spans (InstrumentationScope, not a span attribute)
# Fixes tracer fragmentation where different modules used different tracer names
TRACER_NAME = "thoughtful.supervisor"

try:
    from importlib.metadata import version

    TRACER_VERSION = version("thoughtful")
except Exception:
    TRACER_VERSION = "0.0.0"


@dataclass
class StepDefinition:
    """Step metadata from manifest."""

    step_id: str
    title: str
    description: Optional[str] = None
    parent_id: Optional[str] = None


class StepTracer:
    """Unified step tracing for decorator and context manager patterns.

    Usage:
        # Configure once at startup (in MainContext.__enter__)
        StepTracer.configure(manifest=self.manifest)

        # Use in decorator or context manager
        span, span_cm = StepTracer.start_step_span("2.1", fn=my_function)
        try:
            result = my_function()
        finally:
            StepTracer.end_step_span(span, span_cm)
    """

    _tracer: Optional[trace.Tracer] = None
    _manifest: Any = None
    _step_lookup: Dict[str, StepDefinition] = {}

    @classmethod
    def configure(cls, manifest: Any = None) -> None:
        """Configure the tracer with manifest context.

        Args:
            manifest: Parsed manifest object with workflow definition
        """
        cls._tracer = trace.get_tracer(TRACER_NAME, TRACER_VERSION)
        cls._manifest = manifest
        cls._step_lookup = cls._build_step_lookup(manifest)
        logger.debug("StepTracer configured with %d steps", len(cls._step_lookup))

    @classmethod
    def _build_step_lookup(cls, manifest: Any) -> Dict[str, StepDefinition]:
        """Build step_id -> StepDefinition mapping from manifest."""
        if not manifest or not hasattr(manifest, "workflow") or not manifest.workflow:
            return {}

        lookup: Dict[str, StepDefinition] = {}
        cls._parse_steps_recursive(manifest.workflow, None, lookup)
        return lookup

    @classmethod
    def _parse_steps_recursive(
        cls,
        steps: List[Any],
        parent_id: Optional[str],
        lookup: Dict[str, StepDefinition],
    ) -> None:
        """Recursively parse manifest workflow to build step lookup."""
        for step in steps:
            step_id = str(step.step_id)
            lookup[step_id] = StepDefinition(
                step_id=step_id,
                title=step.title or "",
                description=getattr(step, "description", None),
                parent_id=parent_id,
            )
            if hasattr(step, "steps") and step.steps:
                cls._parse_steps_recursive(step.steps, step_id, lookup)

    @classmethod
    def start_step_span(
        cls, step_id: str, fn: Optional[Callable] = None
    ) -> Tuple[Optional[Span], Any]:
        """Create a step span with manifest enrichment.

        Args:
            step_id: Step identifier (e.g., "2.1.1")
            fn: Optional function being wrapped (for code location)

        Returns:
            Tuple of (span, context_manager) or (None, None) if tracing not active
        """
        # Lazy initialization if configure() wasn't called
        if cls._tracer is None:
            cls._tracer = trace.get_tracer(TRACER_NAME, TRACER_VERSION)

        # Check if tracing is active
        current_span = trace.get_current_span()
        if not current_span or not current_span.get_span_context().is_valid:
            return None, None

        # Build attributes
        attributes: Dict[str, Any] = {"step.id": step_id}

        # Enrich from manifest
        step_def = cls._step_lookup.get(step_id)
        if step_def:
            attributes["step.title"] = step_def.title
            if step_def.description:
                attributes["step.description"] = step_def.description
            if step_def.parent_id:
                attributes["step.parent_id"] = step_def.parent_id
        else:
            # Derive parent from step_id pattern (e.g., "2.1" -> "2")
            parent_id = cls._derive_parent_id(step_id)
            if parent_id:
                attributes["step.parent_id"] = parent_id

        # Add code location
        if fn:
            attributes["code.function"] = fn.__name__
            try:
                attributes["code.filepath"] = inspect.getfile(fn)
            except (TypeError, OSError):
                pass

        # Create span
        try:
            span_cm = cls._tracer.start_as_current_span(
                name=f"step.{step_id}",
                kind=trace.SpanKind.INTERNAL,
                attributes=attributes,
                end_on_exit=False,  # Manual lifecycle for error handling
            )
            span = span_cm.__enter__()
            return span, span_cm
        except Exception as e:
            logger.debug("Failed to create step span: %s", e)
            return None, None

    @classmethod
    def _derive_parent_id(cls, step_id: str) -> Optional[str]:
        """Derive parent step ID from step_id pattern.

        Examples:
            "2.1.1" -> "2.1"
            "2.1" -> "2"
            "2" -> None
        """
        parts = step_id.rsplit(".", 1)
        return parts[0] if len(parts) > 1 else None

    @classmethod
    def end_step_span(
        cls,
        span: Optional[Span],
        span_cm: Any,
        exception: Optional[BaseException] = None,
    ) -> None:
        """End a step span with appropriate status.

        Args:
            span: The span to end
            span_cm: The context manager to exit
            exception: Exception if step failed
        """
        if span is None:
            return

        try:
            if exception:
                span.set_status(Status(StatusCode.ERROR, str(exception)))
                span.record_exception(exception)
            else:
                span.set_status(Status(StatusCode.OK))

            span.end()
            span_cm.__exit__(
                type(exception) if exception else None,
                exception,
                exception.__traceback__ if exception else None,
            )
        except Exception as e:
            logger.debug("Error ending step span: %s", e)

    @classmethod
    def set_span_status(
        cls,
        span: Optional[Span],
        status: "Status",
        exception: Optional[BaseException] = None,
    ) -> None:
        """Update span status (for manual status overrides).

        Args:
            span: The span to update
            status: The supervisor Status enum value
            exception: Optional exception to record
        """
        if span is None or not span.is_recording():
            return

        # Import here to avoid circular imports
        from thoughtful.supervisor.reporting.status import Status as SupervisorStatus

        if status == SupervisorStatus.FAILED:
            span.set_status(Status(StatusCode.ERROR))
            if exception:
                span.record_exception(exception)
        else:
            span.set_status(Status(StatusCode.OK))

    @classmethod
    def add_record_to_span(
        cls,
        span: Optional[Span],
        record_id: str,
        status: str,
        message: Optional[str] = None,
        metadata: Optional[Dict[str, Any]] = None,
    ) -> None:
        """Update span with record status attributes.

        This captures record-level tracking within a step span.

        Args:
            span: Current span (or None)
            record_id: Unique record identifier
            status: Record status (e.g., "succeeded", "failed")
            message: Optional status message
            metadata: Optional additional metadata
        """
        if span is None or not span.is_recording():
            return

        span.set_attribute("record.id", record_id)
        span.set_attribute("record.status", status)
        if message:
            span.set_attribute("record.message", message)

        # Add metadata as span attributes with record.metadata prefix
        if metadata:
            for key, value in metadata.items():
                if isinstance(value, (str, int, float, bool)):
                    span.set_attribute(f"record.metadata.{key}", value)

        # Add span event for timeline visibility
        event_attrs: Dict[str, Any] = {
            "record.id": record_id,
            "record.status": status,
        }
        if message:
            event_attrs["record.message"] = message
        if metadata:
            for key, value in metadata.items():
                if isinstance(value, (str, int, float, bool)):
                    event_attrs[f"record.metadata.{key}"] = value

        span.add_event("record.status_change", attributes=event_attrs)

    @classmethod
    def get_step_info(cls, step_id: str) -> str:
        """Get formatted step info for logging.

        Args:
            step_id: Step identifier

        Returns:
            Formatted string like "Step 2.1 - Get CareLogic Batch"
        """
        step_def = cls._step_lookup.get(step_id)
        if step_def and step_def.title:
            return f"Step {step_id} - {step_def.title}"
        return f"Step {step_id}"

    @classmethod
    def reset(cls) -> None:
        """Reset state (for testing)."""
        cls._tracer = None
        cls._manifest = None
        cls._step_lookup = {}
