"""
History System - Undo/Redo action tracking

Provides action history management for the CLI:
- Track file changes, commands, and other actions
- Undo and redo support
- Persistent history (optional)
- Action grouping for compound operations

Examples:
    # Create history manager
    history = HistoryManager()

    # Record an action
    history.record(FileChangeAction(
        path="file.py",
        old_content="original",
        new_content="modified"
    ))

    # Undo the action
    history.undo()

    # Redo the action
    history.redo()
"""

import json
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import Any, Optional


class ActionType(Enum):
    """Types of actions that can be undone"""

    FILE_CREATE = "file_create"
    FILE_MODIFY = "file_modify"
    FILE_DELETE = "file_delete"
    FILE_RENAME = "file_rename"
    DIRECTORY_CREATE = "directory_create"
    DIRECTORY_DELETE = "directory_delete"
    COMMAND_EXECUTE = "command_execute"
    AGENT_SWITCH = "agent_switch"
    CONFIG_CHANGE = "config_change"
    SESSION_CHANGE = "session_change"


class Action(ABC):
    """Base class for undoable actions"""

    @property
    @abstractmethod
    def action_type(self) -> ActionType:
        """Type of action"""
        pass

    @property
    @abstractmethod
    def description(self) -> str:
        """Human-readable description"""
        pass

    @abstractmethod
    def undo(self) -> bool:
        """
        Undo the action

        Returns:
            True if undo was successful
        """
        pass

    @abstractmethod
    def redo(self) -> bool:
        """
        Redo the action

        Returns:
            True if redo was successful
        """
        pass

    @abstractmethod
    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for serialization"""
        pass

    @classmethod
    @abstractmethod
    def from_dict(cls, data: dict[str, Any]) -> "Action":
        """Create from dictionary"""
        pass


@dataclass
class FileChangeAction(Action):
    """Action for file modifications"""

    path: str
    old_content: Optional[str]
    new_content: Optional[str]
    created_at: datetime = field(default_factory=datetime.now)

    @property
    def action_type(self) -> ActionType:
        if self.old_content is None:
            return ActionType.FILE_CREATE
        elif self.new_content is None:
            return ActionType.FILE_DELETE
        else:
            return ActionType.FILE_MODIFY

    @property
    def description(self) -> str:
        if self.action_type == ActionType.FILE_CREATE:
            return f"Create file: {self.path}"
        elif self.action_type == ActionType.FILE_DELETE:
            return f"Delete file: {self.path}"
        else:
            return f"Modify file: {self.path}"

    def undo(self) -> bool:
        """Restore the previous state"""
        try:
            file_path = Path(self.path)

            if self.action_type == ActionType.FILE_CREATE:
                # Undo create = delete
                if file_path.exists():
                    file_path.unlink()
                    return True
            elif self.action_type == ActionType.FILE_DELETE:
                # Undo delete = recreate
                if self.old_content is not None:
                    file_path.parent.mkdir(parents=True, exist_ok=True)
                    file_path.write_text(self.old_content, encoding="utf-8")
                    return True
            else:
                # Undo modify = restore old content
                if self.old_content is not None:
                    file_path.write_text(self.old_content, encoding="utf-8")
                    return True

            return False
        except Exception:
            return False

    def redo(self) -> bool:
        """Reapply the action"""
        try:
            file_path = Path(self.path)

            if self.action_type == ActionType.FILE_CREATE:
                # Redo create = create
                if self.new_content is not None:
                    file_path.parent.mkdir(parents=True, exist_ok=True)
                    file_path.write_text(self.new_content, encoding="utf-8")
                    return True
            elif self.action_type == ActionType.FILE_DELETE:
                # Redo delete = delete
                if file_path.exists():
                    file_path.unlink()
                    return True
            else:
                # Redo modify = apply new content
                if self.new_content is not None:
                    file_path.write_text(self.new_content, encoding="utf-8")
                    return True

            return False
        except Exception:
            return False

    def to_dict(self) -> dict[str, Any]:
        return {
            "type": "file_change",
            "path": self.path,
            "old_content": self.old_content,
            "new_content": self.new_content,
            "created_at": self.created_at.isoformat(),
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "FileChangeAction":
        return cls(
            path=data["path"],
            old_content=data.get("old_content"),
            new_content=data.get("new_content"),
            created_at=datetime.fromisoformat(data["created_at"])
            if "created_at" in data
            else datetime.now(),
        )


@dataclass
class FileRenameAction(Action):
    """Action for file renaming"""

    old_path: str
    new_path: str
    created_at: datetime = field(default_factory=datetime.now)

    @property
    def action_type(self) -> ActionType:
        return ActionType.FILE_RENAME

    @property
    def description(self) -> str:
        return f"Rename: {self.old_path} -> {self.new_path}"

    def undo(self) -> bool:
        """Rename back to original"""
        try:
            new_file = Path(self.new_path)
            old_file = Path(self.old_path)
            if new_file.exists():
                new_file.rename(old_file)
                return True
            return False
        except Exception:
            return False

    def redo(self) -> bool:
        """Rename again"""
        try:
            old_file = Path(self.old_path)
            new_file = Path(self.new_path)
            if old_file.exists():
                old_file.rename(new_file)
                return True
            return False
        except Exception:
            return False

    def to_dict(self) -> dict[str, Any]:
        return {
            "type": "file_rename",
            "old_path": self.old_path,
            "new_path": self.new_path,
            "created_at": self.created_at.isoformat(),
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "FileRenameAction":
        return cls(
            old_path=data["old_path"],
            new_path=data["new_path"],
            created_at=datetime.fromisoformat(data["created_at"])
            if "created_at" in data
            else datetime.now(),
        )


@dataclass
class DirectoryAction(Action):
    """Action for directory operations"""

    path: str
    is_create: bool
    contents: Optional[dict[str, str]] = None  # file_path -> content for restore
    created_at: datetime = field(default_factory=datetime.now)

    @property
    def action_type(self) -> ActionType:
        return ActionType.DIRECTORY_CREATE if self.is_create else ActionType.DIRECTORY_DELETE

    @property
    def description(self) -> str:
        action = "Create" if self.is_create else "Delete"
        return f"{action} directory: {self.path}"

    def undo(self) -> bool:
        """Undo directory operation"""
        try:
            dir_path = Path(self.path)

            if self.is_create:
                # Undo create = delete
                if dir_path.exists() and dir_path.is_dir():
                    import shutil

                    shutil.rmtree(dir_path)
                    return True
            else:
                # Undo delete = recreate with contents
                dir_path.mkdir(parents=True, exist_ok=True)
                if self.contents:
                    for file_path, content in self.contents.items():
                        full_path = Path(file_path)
                        full_path.parent.mkdir(parents=True, exist_ok=True)
                        full_path.write_text(content, encoding="utf-8")
                return True

            return False
        except Exception:
            return False

    def redo(self) -> bool:
        """Redo directory operation"""
        try:
            dir_path = Path(self.path)

            if self.is_create:
                # Redo create = create
                dir_path.mkdir(parents=True, exist_ok=True)
                return True
            else:
                # Redo delete = delete
                if dir_path.exists() and dir_path.is_dir():
                    import shutil

                    shutil.rmtree(dir_path)
                    return True

            return False
        except Exception:
            return False

    def to_dict(self) -> dict[str, Any]:
        return {
            "type": "directory",
            "path": self.path,
            "is_create": self.is_create,
            "contents": self.contents,
            "created_at": self.created_at.isoformat(),
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "DirectoryAction":
        return cls(
            path=data["path"],
            is_create=data["is_create"],
            contents=data.get("contents"),
            created_at=datetime.fromisoformat(data["created_at"])
            if "created_at" in data
            else datetime.now(),
        )


@dataclass
class ActionGroup(Action):
    """Group of actions that should be undone/redone together"""

    actions: list[Action]
    name: str
    created_at: datetime = field(default_factory=datetime.now)

    @property
    def action_type(self) -> ActionType:
        # Return the type of the first action
        if self.actions:
            return self.actions[0].action_type
        return ActionType.FILE_MODIFY

    @property
    def description(self) -> str:
        return f"{self.name} ({len(self.actions)} actions)"

    def undo(self) -> bool:
        """Undo all actions in reverse order"""
        success = True
        for action in reversed(self.actions):
            if not action.undo():
                success = False
        return success

    def redo(self) -> bool:
        """Redo all actions in order"""
        success = True
        for action in self.actions:
            if not action.redo():
                success = False
        return success

    def to_dict(self) -> dict[str, Any]:
        return {
            "type": "group",
            "name": self.name,
            "actions": [a.to_dict() for a in self.actions],
            "created_at": self.created_at.isoformat(),
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "ActionGroup":
        actions = []
        for action_data in data.get("actions", []):
            action = _action_from_dict(action_data)
            if action:
                actions.append(action)

        return cls(
            name=data["name"],
            actions=actions,
            created_at=datetime.fromisoformat(data["created_at"])
            if "created_at" in data
            else datetime.now(),
        )


def _action_from_dict(data: dict[str, Any]) -> Optional[Action]:
    """Create action from dictionary based on type"""
    action_type = data.get("type")

    if action_type == "file_change":
        return FileChangeAction.from_dict(data)
    elif action_type == "file_rename":
        return FileRenameAction.from_dict(data)
    elif action_type == "directory":
        return DirectoryAction.from_dict(data)
    elif action_type == "group":
        return ActionGroup.from_dict(data)

    return None


@dataclass
class HistoryState:
    """State of the history manager"""

    actions: list[Action] = field(default_factory=list)
    current_index: int = -1  # Points to the last executed action
    max_size: int = 100


class HistoryManager:
    """
    Manages action history for undo/redo

    Provides a stack-based history system where actions can be
    undone and redone. Supports action grouping for compound operations.

    Example:
        history = HistoryManager()

        # Record actions
        history.record(FileChangeAction(path="file.py", old_content="a", new_content="b"))

        # Check state
        print(history.can_undo())  # True
        print(history.can_redo())  # False

        # Undo
        history.undo()

        # Now can redo
        print(history.can_redo())  # True
    """

    def __init__(
        self,
        max_size: int = 100,
        storage_path: Optional[Path] = None,
    ):
        """
        Initialize history manager

        Args:
            max_size: Maximum number of actions to keep
            storage_path: Optional path for persistent storage
        """
        self._state = HistoryState(max_size=max_size)
        self._storage_path = storage_path
        self._group_stack: list[list[Action]] = []

        # Load persisted history if available
        if storage_path and storage_path.exists():
            self._load()

    def record(self, action: Action) -> None:
        """
        Record a new action

        If we're in the middle of the history (after undo), this
        discards any actions that were after the current position.

        Args:
            action: The action to record
        """
        # If we're grouping actions, add to the current group
        if self._group_stack:
            self._group_stack[-1].append(action)
            return

        # Discard any redoable actions
        if self._state.current_index < len(self._state.actions) - 1:
            self._state.actions = self._state.actions[: self._state.current_index + 1]

        # Add new action
        self._state.actions.append(action)
        self._state.current_index = len(self._state.actions) - 1

        # Trim if over max size
        if len(self._state.actions) > self._state.max_size:
            excess = len(self._state.actions) - self._state.max_size
            self._state.actions = self._state.actions[excess:]
            self._state.current_index -= excess

        # Save if persistent
        if self._storage_path:
            self._save()

    def begin_group(self, name: str = "Group") -> None:
        """
        Begin a group of actions

        All actions recorded until end_group() will be grouped together
        and undone/redone as a single unit.

        Args:
            name: Name for the action group
        """
        self._group_stack.append([])

    def end_group(self, name: str = "Group") -> None:
        """
        End the current action group

        Args:
            name: Name for the action group
        """
        if self._group_stack:
            actions = self._group_stack.pop()
            if actions:
                group = ActionGroup(actions=actions, name=name)
                self.record(group)

    def undo(self) -> Optional[Action]:
        """
        Undo the last action

        Returns:
            The undone action, or None if nothing to undo
        """
        if not self.can_undo():
            return None

        action = self._state.actions[self._state.current_index]
        if action.undo():
            self._state.current_index -= 1

            if self._storage_path:
                self._save()

            return action

        return None

    def redo(self) -> Optional[Action]:
        """
        Redo the next action

        Returns:
            The redone action, or None if nothing to redo
        """
        if not self.can_redo():
            return None

        self._state.current_index += 1
        action = self._state.actions[self._state.current_index]

        if action.redo():
            if self._storage_path:
                self._save()
            return action

        # Redo failed, restore index
        self._state.current_index -= 1
        return None

    def can_undo(self) -> bool:
        """Check if undo is possible"""
        return self._state.current_index >= 0

    def can_redo(self) -> bool:
        """Check if redo is possible"""
        return self._state.current_index < len(self._state.actions) - 1

    def get_undo_description(self) -> Optional[str]:
        """Get description of action that would be undone"""
        if self.can_undo():
            return self._state.actions[self._state.current_index].description
        return None

    def get_redo_description(self) -> Optional[str]:
        """Get description of action that would be redone"""
        if self.can_redo():
            return self._state.actions[self._state.current_index + 1].description
        return None

    def get_history(self, limit: int = 10) -> list[tuple[int, Action, bool]]:
        """
        Get recent history

        Args:
            limit: Maximum number of items to return

        Returns:
            List of (index, action, is_current) tuples
        """
        result = []
        start = max(0, len(self._state.actions) - limit)

        for i in range(start, len(self._state.actions)):
            is_current = i == self._state.current_index
            result.append((i, self._state.actions[i], is_current))

        return result

    def clear(self) -> None:
        """Clear all history"""
        self._state.actions.clear()
        self._state.current_index = -1

        if self._storage_path:
            self._save()

    def _load(self) -> None:
        """Load history from storage"""
        if not self._storage_path or not self._storage_path.exists():
            return

        try:
            data = json.loads(self._storage_path.read_text(encoding="utf-8"))
            actions = []
            for action_data in data.get("actions", []):
                action = _action_from_dict(action_data)
                if action:
                    actions.append(action)

            self._state.actions = actions
            self._state.current_index = data.get("current_index", len(actions) - 1)
        except Exception:
            # If load fails, start fresh
            pass

    def _save(self) -> None:
        """Save history to storage"""
        if not self._storage_path:
            return

        try:
            self._storage_path.parent.mkdir(parents=True, exist_ok=True)
            data = {
                "actions": [a.to_dict() for a in self._state.actions],
                "current_index": self._state.current_index,
            }
            self._storage_path.write_text(json.dumps(data, indent=2), encoding="utf-8")
        except Exception:
            # If save fails, continue without persistence
            pass


# Convenience functions


def create_file_change_action(
    path: str,
    old_content: Optional[str],
    new_content: Optional[str],
) -> FileChangeAction:
    """Create a file change action"""
    return FileChangeAction(path=path, old_content=old_content, new_content=new_content)


def create_file_rename_action(old_path: str, new_path: str) -> FileRenameAction:
    """Create a file rename action"""
    return FileRenameAction(old_path=old_path, new_path=new_path)
