"""Top-level scripting API.

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

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 ReaderRegistry, get_registry, register_reader
from impakt.plot.engine import PlotEngine, cursor_values
from impakt.plot.spec import ChannelRef, CursorValues, PlotSpec, PlotStyle
from impakt.protocol.base import ProtocolResult
from impakt.template.library import TemplateLibrary
from impakt.template.model import SessionState, 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)
    """

    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 open(cls, path: str | Path) -> Session:
        """Open a crash test from a path.

        Auto-detects the format using the reader registry.
        """
        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 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."""
        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()

    # ----- 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.

    Example:
        ch = session.channel("11HEAD0000ACXA")
        filtered = ch.transform.cfc(1000)
        aligned = ch.transform.x_align(method="threshold", threshold_value=5.0)
    """

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

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

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

    @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

    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 Channel (non-destructive).
    """

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

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

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

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

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

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

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

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

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

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

        return 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,
        )
