"""Top-level scripting API.

Provides the ``Session`` and ``Template`` classes that serve as the
primary entry points for both scripting and the web UI.

Usage::

    from impakt import Session

    test = Session.open("tests/mme_data/3239")
    ch = test.channel("11HEAD0000H3ACXP")

    # Fluent chaining — each transform returns a ChannelHandle
    filtered = ch.transform.cfc(1000).transform.y_align()

    # Compute all injury criteria (auto-detects channels)
    criteria = test.compute_criteria()

    # Score against a protocol
    result = test.evaluate("euro_ncap")
    result.to_pdf("report.pdf")
"""

from __future__ import annotations

import logging
from pathlib import Path
from typing import Any

import numpy as np

from impakt.channel.model import Channel, ChannelGroup, TestData, TestMetadata
from impakt.criteria.base import CriterionResult
from impakt.io.mme import MMEReader
from impakt.io.reader import get_registry, register_reader
from impakt.plot.engine import PlotEngine
from impakt.plot.spec import ChannelRef, PlotSpec, PlotStyle
from impakt.protocol.base import ProtocolResult
from impakt.template.library import TemplateLibrary
from impakt.template.model import TemplateSpec
from impakt.template.session import SessionManager
from impakt.transform.cfc import CFCFilter

logger = logging.getLogger(__name__)

# Register the built-in MME reader
register_reader(MMEReader())


class Session:
    """A loaded crash test session.

    Wraps TestData with session state, template binding, and
    convenience methods for transforms, criteria, and plotting.

    Usage::

        test = Session.open("/path/to/test_001")
        ch = test.channel("11HEAD0000ACXA")
        filtered = ch.transform.cfc(1000)  # returns ChannelHandle, chainable

        criteria = test.compute_criteria()  # auto-detect channels
        result = test.evaluate("euro_ncap")  # score against protocol
        result.to_pdf("report.pdf")
    """

    _plugins_discovered = False

    def __init__(self, test_data: TestData, session_mgr: SessionManager | None = None) -> None:
        self._data = test_data
        self._session_mgr = session_mgr or (
            SessionManager(test_data.path) if test_data.path else None
        )
        self._template: TemplateSpec | None = None

    @classmethod
    def _discover_plugins(cls) -> None:
        """Discover and register plugins. Called once on first Session.open()."""
        if cls._plugins_discovered:
            return
        cls._plugins_discovered = True
        try:
            from impakt.plugin.registry import discover_all

            discover_all()
        except Exception as e:
            logger.debug("Plugin discovery failed (non-fatal): %s", e)

    @classmethod
    def open(cls, path: str | Path) -> Session:
        """Open a crash test from a path.

        Auto-detects the format using the reader registry.
        Discovers plugins on first call.
        """
        # Discover plugins (idempotent — safe to call multiple times)
        cls._discover_plugins()

        path = Path(path).resolve()
        registry = get_registry()
        test_data = registry.read(path)

        session_mgr = SessionManager(path)
        session = cls(test_data, session_mgr)

        # Load existing session state if available
        if session_mgr.has_session:
            state = session_mgr.state
            if state.template_name:
                try:
                    lib = TemplateLibrary()
                    session._template = lib.get(state.template_name)
                except FileNotFoundError:
                    pass

        return session

    @property
    def test_id(self) -> str:
        return self._data.test_id

    @property
    def metadata(self) -> TestMetadata:
        return self._data.metadata

    @property
    def data(self) -> TestData:
        """Underlying TestData object."""
        return self._data

    @property
    def path(self) -> Path | None:
        """Path to the test data directory."""
        return self._data.path

    @property
    def channel_names(self) -> list[str]:
        return self._data.channel_names

    @property
    def template(self) -> TemplateSpec | None:
        return self._template

    # ----- Channel access -----

    def channel(self, name: str) -> ChannelHandle:
        """Get a channel by name, wrapped with transform convenience methods.

        Returns a ChannelHandle with a fluent ``.transform`` interface.
        """
        ch = self._data.get(name)
        return ChannelHandle(ch)

    def find(self, pattern: str) -> list[Channel]:
        """Find channels matching a pattern."""
        return self._data.find(pattern)

    def group(self, pattern: str) -> ChannelGroup:
        """Find a channel group (X/Y/Z family)."""
        return self._data.group(pattern)

    def groups(self) -> dict[str, ChannelGroup]:
        """All auto-detected channel groups."""
        return self._data.groups()

    def channel_tree(self) -> dict[str, dict[str, dict[str, list[Channel]]]]:
        """Hierarchical channel tree for UI display."""
        return self._data.channel_tree()

    # ----- Criteria & Protocol -----

    def compute_criteria(self) -> dict[str, CriterionResult]:
        """Auto-detect channels and compute all applicable injury criteria.

        Uses ISO channel naming to find the right channels for each criterion.
        Returns a dict of criterion name -> CriterionResult.
        """
        from impakt.web.components.criteria import auto_compute_criteria

        return auto_compute_criteria(self._data)

    def evaluate(self, protocol: str = "euro_ncap", version: str = "") -> ProtocolResult:
        """Compute criteria and score against a rating protocol.

        Args:
            protocol: Protocol name ('euro_ncap', 'us_ncap', 'iihs').
            version: Protocol version (optional, uses latest if empty).

        Returns:
            ProtocolResult with stars/rating and per-region scores.
        """
        criteria = self.compute_criteria()

        if protocol == "euro_ncap":
            from impakt.protocol.euro_ncap import evaluate

            return evaluate(criteria, version=version or "2024")
        elif protocol == "us_ncap":
            from impakt.protocol.us_ncap import evaluate

            return evaluate(criteria, version=version or "2023")
        elif protocol == "iihs":
            from impakt.protocol.iihs import evaluate

            return evaluate(criteria, version=version or "2024")
        else:
            raise ValueError(
                f"Unknown protocol: {protocol}. Use 'euro_ncap', 'us_ncap', or 'iihs'."
            )

    # ----- Template -----

    def apply_template(self, name_or_spec: str | TemplateSpec) -> None:
        """Apply a template to this session."""
        if isinstance(name_or_spec, str):
            lib = TemplateLibrary()
            spec = lib.get(name_or_spec)
        else:
            spec = name_or_spec

        self._template = spec
        if self._session_mgr:
            self._session_mgr.apply_template(spec)

    # ----- Quick plotting -----

    def plot(
        self,
        *channel_names: str,
        title: str = "",
        cfc: int | None = None,
    ) -> Any:
        """Quick plot of one or more channels.

        Returns a Plotly figure.
        """
        refs: list[ChannelRef] = []
        for name in channel_names:
            ch = self._data.get(name)
            if cfc is not None:
                ch = CFCFilter(cfc_class=cfc).apply(ch)
            refs.append(
                ChannelRef(
                    test_id=self.test_id,
                    channel_name=name,
                    channel=ch,
                    style=PlotStyle(label=ch.code.short_label if ch.code.is_valid else name),
                )
            )

        spec = PlotSpec(
            channels=refs,
            title=title or ", ".join(channel_names),
            y_label=refs[0].channel.unit if refs and refs[0].channel else "",
        )

        engine = PlotEngine()
        return engine.render(spec)

    # ----- Session state -----

    def save(self) -> None:
        """Save current session state."""
        if self._session_mgr:
            self._session_mgr.save()

    def __repr__(self) -> str:
        tmpl = f", template={self._template.name}" if self._template else ""
        return f"Session({self.test_id}, {len(self._data)} channels{tmpl})"

    def __len__(self) -> int:
        return len(self._data)

    def __contains__(self, name: str) -> bool:
        return name in self._data


class ChannelHandle:
    """Wrapper around a Channel providing fluent transform access.

    Each transform method on ``.transform`` returns a new ``ChannelHandle``,
    enabling chaining::

        ch = session.channel("11HEAD0000ACXA")
        result = ch.transform.cfc(1000).transform.y_align().transform.trim(t_end=0.1)
    """

    def __init__(self, channel: Channel) -> None:
        self._channel = channel
        self.transform = TransformProxy(self)

    @property
    def raw(self) -> Channel:
        """The underlying Channel object."""
        return self._channel

    @property
    def name(self) -> str:
        return self._channel.name

    @property
    def code(self):
        return self._channel.code

    @property
    def data(self) -> np.ndarray:
        return self._channel.data

    @property
    def time(self) -> np.ndarray:
        return self._channel.time

    @property
    def unit(self) -> str:
        return self._channel.unit

    @property
    def peak(self) -> float:
        return self._channel.peak

    @property
    def sample_rate(self) -> float:
        return self._channel.sample_rate

    def value_at(self, t: float) -> float:
        return self._channel.value_at(t)

    def plot(self, title: str = "") -> Any:
        """Quick plot of this channel."""
        spec = PlotSpec(
            channels=[ChannelRef(channel=self._channel)],
            title=title or self._channel.code.description,
            y_label=self._channel.unit,
        )
        return PlotEngine().render(spec)

    def __repr__(self) -> str:
        return repr(self._channel)


class TransformProxy:
    """Fluent transform interface for a channel.

    Each method returns a new ``ChannelHandle`` wrapping the transformed
    channel, enabling chaining.
    """

    def __init__(self, handle: ChannelHandle) -> None:
        self._handle = handle

    @property
    def _channel(self) -> Channel:
        return self._handle._channel

    def cfc(self, cfc_class: int) -> ChannelHandle:
        """Apply CFC filter."""
        from impakt.transform.cfc import CFCFilter

        return ChannelHandle(CFCFilter(cfc_class=cfc_class).apply(self._channel))

    def x_align(
        self, method: str = "manual", reference_time: float = 0.0, **kwargs: Any
    ) -> ChannelHandle:
        """Apply time-zero alignment."""
        from impakt.transform.align import XAlign

        return ChannelHandle(
            XAlign(method=method, reference_time=reference_time, **kwargs).apply(self._channel)
        )

    def y_align(self, window: tuple[float, float] | None = None) -> ChannelHandle:
        """Apply Y-axis zero correction."""
        from impakt.transform.align import YAlign

        start, end = window if window else (None, None)
        return ChannelHandle(YAlign(window_start=start, window_end=end).apply(self._channel))

    def trim(self, t_start: float | None = None, t_end: float | None = None) -> ChannelHandle:
        """Trim to a time range."""
        from impakt.transform.resample import Trim

        return ChannelHandle(Trim(t_start=t_start, t_end=t_end).apply(self._channel))

    def resample(self, target_rate: float) -> ChannelHandle:
        """Resample to a new rate."""
        from impakt.transform.resample import Resample

        return ChannelHandle(Resample(target_rate=target_rate).apply(self._channel))


class Template:
    """Template management interface."""

    @staticmethod
    def load(name: str) -> TemplateSpec:
        """Load a template from the global library by name."""
        lib = TemplateLibrary()
        return lib.get(name)

    @staticmethod
    def list() -> list[str]:
        """List available templates in the global library."""
        lib = TemplateLibrary()
        return lib.list()

    @staticmethod
    def save(spec: TemplateSpec) -> Path:
        """Save a template to the global library."""
        lib = TemplateLibrary()
        return lib.save(spec)

    @staticmethod
    def create(
        name: str,
        plots: list[dict[str, Any]] | None = None,
        criteria: list[str] | None = None,
        protocol: str = "",
        **kwargs: Any,
    ) -> TemplateSpec:
        """Create a new template spec."""
        from impakt.template.model import PlotDefinition

        plot_defs = []
        for p in plots or []:
            plot_defs.append(
                PlotDefinition(
                    title=p.get("title", ""),
                    channel_patterns=p.get("channels", []),
                    transforms=p.get("transforms", []),
                )
            )

        return TemplateSpec(
            name=name,
            plots=plot_defs,
            criteria=criteria or [],
            protocol=protocol,
            **kwargs,
        )
