"""Action session runner for headless/CLI execution."""
from __future__ import annotations

import copy
import inspect
import json
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional

from takefits.core.actions import ActionRegistry
from takefits.core.app_state import AppState

_USE_CURRENT_STATE = object()


@dataclass
class ActionRecord:
    action: str
    params: Dict[str, Any]
    timestamp: str
    tag: Optional[str] = None

    def to_dict(self) -> Dict[str, Any]:
        payload: Dict[str, Any] = {
            "action": self.action,
            "params": dict(self.params),
            "timestamp": self.timestamp,
        }
        if self.tag is not None:
            payload["tag"] = self.tag
        return payload

    @classmethod
    def from_dict(cls, payload: Dict[str, Any]) -> "ActionRecord":
        if not isinstance(payload, dict):
            raise TypeError("ActionRecord payload must be a mapping")
        action = payload.get("action") or payload.get("name")
        if not action:
            raise ValueError("ActionRecord payload is missing 'action'/'name'")
        params = payload.get("params") or {}
        if not isinstance(params, dict):
            raise ValueError("ActionRecord 'params' must be an object")
        timestamp = str(payload.get("timestamp") or _utc_timestamp())
        tag = payload.get("tag")
        return cls(
            action=str(action),
            params=dict(params),
            timestamp=timestamp,
            tag=str(tag) if tag is not None else None,
        )


@dataclass
class ActionSession:
    registry: ActionRegistry
    state: Optional[AppState] = None
    history: List[ActionRecord] = field(default_factory=list)
    last_result: Any = None
    _initial_state_seed: Optional[AppState] = field(default=None, init=False, repr=False)
    _initial_seed_set: bool = field(default=False, init=False, repr=False)
    _cursor: int = field(default=0, init=False, repr=False)

    def __post_init__(self) -> None:
        self.set_initial_state_seed()
        self._cursor = len(self.history)

    def execute(
        self,
        name: str,
        *,
        record_only: bool = False,
        replace_tag: Optional[str] = None,
        **params: Any,
    ) -> Any:
        result: Any = None
        if not record_only:
            result = self._execute_handler(name=name, params=params)

        self._append_record(name=name, params=params, tag=replace_tag)
        return result

    def record(
        self,
        name: str,
        params: Optional[Dict[str, Any]] = None,
        *,
        replace_tag: Optional[str] = None,
    ) -> None:
        """Record an action without executing its handler."""
        self.execute(
            name,
            record_only=True,
            replace_tag=replace_tag,
            **(params or {}),
        )

    def remove_record_by_tag(self, tag: str) -> bool:
        """Remove the most recent history record with the provided tag."""
        for idx in range(len(self.history) - 1, -1, -1):
            if self.history[idx].tag == tag:
                del self.history[idx]
                if idx < self._cursor:
                    self._cursor -= 1
                return True
        return False

    def run_actions(self, actions: List[Dict[str, Any]]) -> List[Any]:
        results = []
        for entry in actions:
            name = entry.get("action") or entry.get("name")
            if not name:
                raise ValueError("Action entry missing 'action' or 'name' field.")
            params = entry.get("params", {})
            if params is None:
                params = {}
            results.append(self.execute(name, **params))
        return results

    def export_history(self) -> List[Dict[str, Any]]:
        return [record.to_dict() for record in self.history]

    def save_history(self, path: str) -> str:
        payload = {
            "version": 1,
            "saved_at": _utc_timestamp(),
            "history": self.export_history(),
        }
        output_path = Path(path)
        output_path.parent.mkdir(parents=True, exist_ok=True)
        output_path.write_text(
            json.dumps(payload, ensure_ascii=False, indent=2),
            encoding="utf-8",
        )
        return str(output_path)

    def load_history(
        self,
        path: str,
        *,
        replay: bool = False,
        replace: bool = True,
    ) -> List[ActionRecord]:
        loaded = json.loads(Path(path).read_text(encoding="utf-8"))
        if isinstance(loaded, dict):
            entries = loaded.get("history", [])
        elif isinstance(loaded, list):
            entries = loaded
        else:
            raise ValueError("History file must be a list or an object with 'history'")

        records = [ActionRecord.from_dict(entry) for entry in entries]

        if not replay:
            if replace:
                self.history = list(records)
            else:
                if self._cursor < len(self.history):
                    del self.history[self._cursor :]
                self.history.extend(records)
            self._cursor = len(self.history)
            return records

        if replace:
            self.history = []
            self._cursor = 0
            self._restore_initial_state()
        elif self._cursor < len(self.history):
            del self.history[self._cursor :]

        for record in records:
            self.execute(
                record.action,
                replace_tag=record.tag,
                **dict(record.params),
            )
            if self.history:
                self.history[-1].timestamp = record.timestamp

        self._cursor = len(self.history)
        return list(self.history)

    def set_initial_state_seed(self, state: Any = _USE_CURRENT_STATE) -> None:
        source = self.state if state is _USE_CURRENT_STATE else state
        self._initial_state_seed = _clone_state(source) if source is not None else None
        self._initial_seed_set = True

    def reset_to_initial(self, *, keep_history: bool = True) -> Optional[AppState]:
        self._restore_initial_state()
        self._cursor = 0
        if not keep_history:
            self.history = []
        return self.state

    @property
    def cursor(self) -> int:
        return self._cursor

    def can_undo(self) -> bool:
        return self._cursor > 0

    def can_redo(self) -> bool:
        return self._cursor < len(self.history)

    def undo(self, steps: int = 1) -> Optional[AppState]:
        steps_int = int(steps)
        if steps_int < 1:
            raise ValueError("undo steps must be >= 1")
        target_cursor = max(0, self._cursor - steps_int)
        return self._replay_to_cursor(target_cursor)

    def redo(self, steps: int = 1) -> Optional[AppState]:
        steps_int = int(steps)
        if steps_int < 1:
            raise ValueError("redo steps must be >= 1")
        target_cursor = min(len(self.history), self._cursor + steps_int)
        return self._replay_to_cursor(target_cursor)

    def _restore_initial_state(self) -> None:
        if not self._initial_seed_set:
            raise ValueError("Initial state seed is not set.")
        if self._initial_state_seed is None:
            self.state = None
            self.last_result = None
            return
        self.state = _clone_state(self._initial_state_seed)
        self.last_result = self.state

    def _replay_to_cursor(self, target_cursor: int) -> Optional[AppState]:
        if target_cursor < 0 or target_cursor > len(self.history):
            raise ValueError("Replay target is out of range.")
        self._restore_initial_state()
        for record in self.history[:target_cursor]:
            self._execute_handler(name=record.action, params=dict(record.params))
        self._cursor = target_cursor
        return self.state

    def _append_record(self, name: str, params: Dict[str, Any], tag: Optional[str]) -> None:
        if self._cursor < len(self.history):
            del self.history[self._cursor :]
        if tag:
            self.remove_record_by_tag(tag)
        self.history.append(
            ActionRecord(
                action=name,
                params=dict(params),
                timestamp=_utc_timestamp(),
                tag=tag,
            )
        )
        self._cursor = len(self.history)

    def _execute_handler(self, name: str, params: Dict[str, Any]) -> Any:
        action = self.registry.get_action(name)
        if not action:
            raise ValueError(f"Action '{name}' not found.")

        handler = action.handler
        accepts_state = _handler_accepts_state(handler)
        call_params = _filter_handler_kwargs(handler, params)

        if accepts_state:
            if self.state is None:
                raise ValueError(f"Action '{name}' requires state, but no state is loaded.")
            result = handler(state=self.state, **call_params)
        else:
            result = handler(**call_params)

        if isinstance(result, AppState):
            self.state = result
        elif result is None and accepts_state:
            # In-place mutation pattern
            result = self.state

        self.last_result = result
        return result


def _handler_accepts_state(handler: Any) -> bool:
    try:
        sig = inspect.signature(handler)
    except (TypeError, ValueError):
        return False
    return "state" in sig.parameters


def _filter_handler_kwargs(handler: Any, params: Dict[str, Any]) -> Dict[str, Any]:
    """
    Keep only keyword arguments accepted by the handler.

    This lets history records carry UI-only metadata without breaking replay.
    """
    if not isinstance(params, dict):
        return {}
    try:
        sig = inspect.signature(handler)
    except (TypeError, ValueError):
        return dict(params)

    values = list(sig.parameters.values())
    if any(param.kind == inspect.Parameter.VAR_KEYWORD for param in values):
        return dict(params)

    allowed: set[str] = set()
    for param in values:
        if param.name == "state":
            continue
        if param.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY):
            allowed.add(param.name)
    return {key: value for key, value in params.items() if key in allowed}


def _utc_timestamp() -> str:
    return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def _clone_state(state: AppState) -> AppState:
    return copy.deepcopy(state)
