"""Application state management.

AppState is the central data store for the web UI. It holds Session
objects (from the scripting API) and delegates to them for channel
access, transforms, criteria, and protocol scoring.

AppState is NOT a Dash Store — it lives server-side in Python memory.
Dash stores hold lightweight references (test IDs, selected channels)
that callbacks use to look up data from AppState.
"""

from __future__ import annotations

import logging
from pathlib import Path
from typing import Any

from impakt.channel.model import Channel, TestData
from impakt.script.api import Session
from impakt.template.library import TemplateLibrary
from impakt.template.model import PlotDefinition, TemplateSpec
from impakt.transform.align import YAlign
from impakt.transform.cfc import CFCFilter

logger = logging.getLogger(__name__)


class AppState:
    """Server-side application state.

    Manages multiple loaded test sessions and provides channel resolution
    for the UI callbacks. All test data access goes through Session objects.
    """

    def __init__(self) -> None:
        self._sessions: dict[str, Session] = {}
        self._session_order: list[str] = []
        self._template_library = TemplateLibrary()
        self._active_template: TemplateSpec | None = None
        # Per-channel transform overrides: {channel_key: {"cfc": "600"}}
        self.channel_overrides: dict[str, dict[str, Any]] = {}
        # Active corridors
        self.corridors: list[dict[str, Any]] = []

    def load_test(self, path: str | Path) -> Session:
        """Load a test from a path and add it to the state.

        Returns the Session object.
        """
        session = Session.open(path)
        test_id = session.test_id

        self._sessions[test_id] = session
        if test_id not in self._session_order:
            self._session_order.append(test_id)

        logger.info("Loaded test '%s' from %s (%d channels)", test_id, path, len(session))
        return session

    def add_session(self, session: Session) -> None:
        """Add an already-created Session to the state."""
        test_id = session.test_id
        self._sessions[test_id] = session
        if test_id not in self._session_order:
            self._session_order.append(test_id)

    def remove_test(self, test_id: str) -> None:
        """Remove a loaded test."""
        self._sessions.pop(test_id, None)
        if test_id in self._session_order:
            self._session_order.remove(test_id)

    @property
    def sessions(self) -> list[Session]:
        """All loaded sessions in load order."""
        return [self._sessions[tid] for tid in self._session_order if tid in self._sessions]

    @property
    def tests(self) -> list[Session]:
        """Alias for sessions (backward compat for components)."""
        return self.sessions

    @property
    def test_ids(self) -> list[str]:
        return list(self._session_order)

    @property
    def primary_test(self) -> Session | None:
        """The first loaded session (primary for criteria, etc.)."""
        if self._session_order:
            return self._sessions.get(self._session_order[0])
        return None

    def get_test(self, test_id: str) -> Session | None:
        return self._sessions.get(test_id)

    def get_channel(self, test_id: str, channel_name: str) -> Channel | None:
        """Resolve a channel from a test ID and channel name."""
        session = self._sessions.get(test_id)
        if session is None:
            return None
        try:
            return session.data.get(channel_name)
        except KeyError:
            return None

    # ----- Template & Session -----

    @property
    def template_library(self) -> TemplateLibrary:
        return self._template_library

    @property
    def active_template(self) -> TemplateSpec | None:
        return self._active_template

    @property
    def template_names(self) -> list[str]:
        return self._template_library.list()

    def apply_template(self, name: str) -> tuple[list[str], dict[str, str]]:
        """Apply a template by name.

        Resolves channel patterns against the primary test.
        Returns (selected_channel_keys, transform_settings).
        """
        template = self._template_library.get(name)
        self._active_template = template

        primary = self.primary_test
        if primary:
            primary.apply_template(template)

        # Resolve channel patterns
        resolved_keys: list[str] = []
        if primary:
            for plot_def in template.plots:
                for pattern in plot_def.channel_patterns:
                    matches = primary.find(pattern)
                    for ch in matches:
                        key = f"{primary.test_id}::{ch.name}"
                        if key not in resolved_keys:
                            resolved_keys.append(key)

        transforms: dict[str, str] = {}
        if template.default_cfc is not None:
            transforms["cfc"] = str(template.default_cfc)
        else:
            transforms["cfc"] = "none"

        logger.info("Applied template '%s' — %d channels", name, len(resolved_keys))
        return resolved_keys, transforms

    def save_as_template(
        self,
        name: str,
        description: str,
        selected_keys: list[str],
        cfc_value: str,
        x1: float | None,
        x2: float | None,
        protocol: str = "",
    ) -> TemplateSpec:
        """Capture current UI state as a new template."""
        patterns: list[str] = []
        for key in selected_keys:
            ch_name = key.split("::", 1)[-1] if "::" in key else key
            if ch_name not in patterns:
                patterns.append(ch_name)

        plot = PlotDefinition(
            title=name,
            channel_patterns=patterns,
            x_cursors=(x1, x2) if x1 is not None and x2 is not None else None,
        )

        transforms: list[dict[str, Any]] = []
        if cfc_value and cfc_value != "none":
            transforms.append({"type": "cfc_filter", "cfc_class": int(cfc_value)})
        if transforms:
            plot.transforms = transforms

        template = TemplateSpec(
            name=name,
            description=description,
            plots=[plot],
            default_cfc=int(cfc_value) if cfc_value and cfc_value != "none" else None,
            protocol=protocol,
        )

        self._template_library.save(template)
        self._active_template = template
        logger.info("Saved template '%s' with %d patterns", name, len(patterns))
        return template

    def save_session(self, selected_keys: list[str], cfc_value: str, **overrides: Any) -> None:
        """Auto-save current state to the session for the primary test."""
        primary = self.primary_test
        if not primary or not primary.path:
            return

        from impakt.template.session import SessionManager

        mgr = SessionManager(primary.path)
        mgr.state.overrides = {
            "selected_channels": selected_keys,
            "cfc": cfc_value,
            **overrides,
        }
        mgr.save()

    def load_session_state(self) -> dict[str, Any] | None:
        """Load saved session state for the primary test."""
        primary = self.primary_test
        if not primary or not primary.path:
            return None

        from impakt.template.session import SessionManager

        mgr = SessionManager(primary.path)
        if not mgr.has_session:
            return None

        return {
            "selected_channels": mgr.state.overrides.get("selected_channels", []),
            "cfc": mgr.state.overrides.get("cfc", "none"),
            "template": mgr.state.template_name,
        }

    # ----- Channel tree / grid helpers -----

    def build_channel_tree(self) -> dict[str, Any]:
        """Build hierarchical channel tree across all loaded tests."""
        result: dict[str, Any] = {}
        for session in self.sessions:
            tree = session.channel_tree()
            test_tree: dict[str, Any] = {}
            for obj, locations in sorted(tree.items()):
                obj_tree: dict[str, Any] = {}
                for loc, measurements in sorted(locations.items()):
                    loc_tree: dict[str, Any] = {}
                    for meas, channels in sorted(measurements.items()):
                        ch_list = []
                        for ch in channels:
                            label = ch.code.short_label if ch.code.is_valid else ch.name
                            ch_list.append(
                                {
                                    "name": ch.name,
                                    "label": label,
                                    "key": f"{session.test_id}::{ch.name}",
                                    "unit": ch.unit,
                                    "peak": f"{ch.peak:.2f}",
                                    "samples": str(ch.n_samples),
                                    "rate": f"{ch.sample_rate:.0f}",
                                }
                            )
                        loc_tree[meas] = ch_list
                    obj_tree[loc] = loc_tree
                test_tree[obj] = obj_tree
            result[session.test_id] = test_tree
        return result

    @property
    def is_empty(self) -> bool:
        return len(self._sessions) == 0

    @property
    def total_channels(self) -> int:
        return sum(len(s) for s in self.sessions)

    def __repr__(self) -> str:
        test_info = ", ".join(f"{s.test_id}({len(s)}ch)" for s in self.sessions)
        tmpl = f", template={self._active_template.name}" if self._active_template else ""
        return f"AppState([{test_info}]{tmpl})"
