"""
Unified state tracking for persuade operations.

This module provides a single source of truth for:
1. Sync tracking: What files persuade synced for .git/info/exclude entries
2. Session continuity (Issue #151): Claude's working context across sessions

State is stored at `.claude/state/persuade.json` in the target project.

Usage:
    >>> from persuade.state import PersuadeState
    >>> state = PersuadeState.load(Path("/path/to/project"))
    >>> state.sync.add_entry(".github/workflows/ci.yml", ".github/workflows/ci.yml")
    >>> state.session.update(current_task="Implementing feature X")
    >>> state.save(Path("/path/to/project"))
"""

from __future__ import annotations

import json
from dataclasses import dataclass, field
from datetime import UTC, datetime
from pathlib import Path
from typing import Any

from persuade.logging import get_logger

__all__ = [
    "PersuadeState",
    "SyncState",
    "SyncEntry",
    "SessionState",
    "MetaState",
    "STATE_FILE_PATH",
    "STATE_SCHEMA_VERSION",
    "SESSION_VALIDITY_HOURS",
]

logger = get_logger()

# State file location (relative to project root)
STATE_FILE_PATH = ".claude/state/persuade.json"

# Current schema version for migrations
STATE_SCHEMA_VERSION = 1

# Sessions older than this are considered stale
SESSION_VALIDITY_HOURS = 24


@dataclass
class SyncEntry:
    """Details about a single synced file.

    Attributes:
        source: Source path in the template repository
        destination: Destination path in the target project
        synced_at: ISO timestamp of when the file was synced
        checksum: Optional SHA256 checksum for change detection
    """

    source: str
    destination: str
    synced_at: str
    checksum: str = ""

    def to_dict(self) -> dict[str, str]:
        """Convert to dictionary for JSON serialization."""
        return {
            "source": self.source,
            "destination": self.destination,
            "synced_at": self.synced_at,
            "checksum": self.checksum,
        }

    @classmethod
    def from_dict(cls, data: dict[str, str]) -> SyncEntry:
        """Create from dictionary."""
        return cls(
            source=data.get("source", ""),
            destination=data.get("destination", ""),
            synced_at=data.get("synced_at", ""),
            checksum=data.get("checksum", ""),
        )


@dataclass
class SyncState:
    """Track what files persuade synced for .git/info/exclude entries.

    Attributes:
        synced_files: Dict mapping destination path to SyncEntry details
        last_sync: ISO timestamp of the last sync operation
        persuade_version: Version of persuade that performed the sync
    """

    synced_files: dict[str, SyncEntry] = field(default_factory=dict)
    last_sync: str = ""
    persuade_version: str = ""

    def add_entry(self, source: str, destination: str, checksum: str = "") -> None:
        """Add or update a synced file entry.

        Args:
            source: Source path in template repository
            destination: Destination path in target project
            checksum: Optional file checksum
        """
        self.synced_files[destination] = SyncEntry(
            source=source,
            destination=destination,
            synced_at=datetime.now(UTC).isoformat(),
            checksum=checksum,
        )
        self.last_sync = datetime.now(UTC).isoformat()

    def remove_entry(self, destination: str) -> bool:
        """Remove a synced file entry.

        Args:
            destination: Destination path to remove

        Returns:
            True if entry was removed, False if not found
        """
        if destination in self.synced_files:
            del self.synced_files[destination]
            return True
        return False

    def get_destinations(self) -> list[str]:
        """Get all synced destination paths.

        Returns:
            List of destination paths
        """
        return list(self.synced_files.keys())

    def get_external_destinations(self) -> list[str]:
        """Get destinations outside .claude/ directory.

        These are listed as individual entries in .git/info/exclude.

        Returns:
            List of external destination paths
        """
        return [dest for dest in self.synced_files if not dest.startswith(".claude/")]

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for JSON serialization."""
        return {
            "synced_files": {dest: entry.to_dict() for dest, entry in self.synced_files.items()},
            "last_sync": self.last_sync,
            "persuade_version": self.persuade_version,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> SyncState:
        """Create from dictionary."""
        synced_files = {}
        raw_files = data.get("synced_files", {})
        for dest, entry_data in raw_files.items():
            if isinstance(entry_data, dict):
                synced_files[dest] = SyncEntry.from_dict(entry_data)

        return cls(
            synced_files=synced_files,
            last_sync=data.get("last_sync", ""),
            persuade_version=data.get("persuade_version", ""),
        )


@dataclass
class SessionState:
    """Track Claude's working context across sessions.

    This enables Issue #151's session continuity features.

    Attributes:
        last_updated: ISO timestamp of last session update
        working_directory: Last working directory
        git_branch: Last git branch being worked on
        current_task: Description of current task
        learned_patterns: Patterns learned during session (e.g., "prefers pytest")
    """

    last_updated: str = ""
    working_directory: str = ""
    git_branch: str = ""
    current_task: str = ""
    learned_patterns: list[str] = field(default_factory=list)

    def is_valid(self) -> bool:
        """Check if session is still valid (not stale).

        Sessions older than SESSION_VALIDITY_HOURS are considered stale.

        Returns:
            True if session is valid
        """
        if not self.last_updated:
            return False

        try:
            updated_at = datetime.fromisoformat(self.last_updated)
            now = datetime.now(UTC)
            hours_old = (now - updated_at).total_seconds() / 3600
            return hours_old < SESSION_VALIDITY_HOURS
        except ValueError:
            return False

    def update(
        self,
        working_directory: str | None = None,
        git_branch: str | None = None,
        current_task: str | None = None,
        learned_patterns: list[str] | None = None,
    ) -> None:
        """Update session state with new values.

        Only updates fields that are explicitly provided.

        Args:
            working_directory: New working directory
            git_branch: New git branch
            current_task: New current task
            learned_patterns: New patterns to add (appended to existing)
        """
        self.last_updated = datetime.now(UTC).isoformat()

        if working_directory is not None:
            self.working_directory = working_directory
        if git_branch is not None:
            self.git_branch = git_branch
        if current_task is not None:
            self.current_task = current_task
        if learned_patterns is not None:
            # Append new patterns, avoiding duplicates
            for pattern in learned_patterns:
                if pattern not in self.learned_patterns:
                    self.learned_patterns.append(pattern)

    def add_pattern(self, pattern: str) -> None:
        """Add a learned pattern if not already present.

        Args:
            pattern: Pattern to add (e.g., "prefers pytest over unittest")
        """
        if pattern and pattern not in self.learned_patterns:
            self.learned_patterns.append(pattern)
            self.last_updated = datetime.now(UTC).isoformat()

    def clear(self) -> None:
        """Clear session state."""
        self.last_updated = ""
        self.working_directory = ""
        self.git_branch = ""
        self.current_task = ""
        self.learned_patterns = []

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for JSON serialization."""
        return {
            "last_updated": self.last_updated,
            "working_directory": self.working_directory,
            "git_branch": self.git_branch,
            "current_task": self.current_task,
            "learned_patterns": self.learned_patterns,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> SessionState:
        """Create from dictionary."""
        return cls(
            last_updated=data.get("last_updated", ""),
            working_directory=data.get("working_directory", ""),
            git_branch=data.get("git_branch", ""),
            current_task=data.get("current_task", ""),
            learned_patterns=data.get("learned_patterns", []),
        )


@dataclass
class MetaState:
    """Metadata about the state file itself.

    Attributes:
        created_at: ISO timestamp of state file creation
        persuade_version: Version of persuade that created the file
        schema_version: Schema version for migrations
    """

    created_at: str = ""
    persuade_version: str = ""
    schema_version: int = STATE_SCHEMA_VERSION

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for JSON serialization."""
        return {
            "created_at": self.created_at,
            "persuade_version": self.persuade_version,
            "schema_version": self.schema_version,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> MetaState:
        """Create from dictionary."""
        return cls(
            created_at=data.get("created_at", ""),
            persuade_version=data.get("persuade_version", ""),
            schema_version=data.get("schema_version", STATE_SCHEMA_VERSION),
        )


@dataclass
class PersuadeState:
    """Unified state tracking for persuade operations.

    This class provides a single source of truth for all persuade state,
    supporting both sync tracking and session continuity use cases.

    Attributes:
        version: Schema version for the state file
        sync: Sync state tracking synced files
        session: Session state tracking working context
        meta: Metadata about the state file
    """

    version: int = STATE_SCHEMA_VERSION
    sync: SyncState = field(default_factory=SyncState)
    session: SessionState = field(default_factory=SessionState)
    meta: MetaState = field(default_factory=MetaState)

    @classmethod
    def load(cls, project_path: Path) -> PersuadeState:
        """Load state from the project's state file.

        Creates a new empty state if the file doesn't exist.

        Args:
            project_path: Path to the project root

        Returns:
            PersuadeState instance
        """
        state_file = project_path / STATE_FILE_PATH

        if not state_file.exists():
            logger.debug("state_file_not_found", path=str(state_file))
            return cls._create_new()

        try:
            with open(state_file, encoding="utf-8") as f:
                data = json.load(f)

            state = cls.from_dict(data)
            logger.debug("state_loaded", path=str(state_file))
            return state

        except json.JSONDecodeError as e:
            logger.warning(
                "state_file_invalid_json",
                path=str(state_file),
                error=str(e),
            )
            return cls._create_new()
        except OSError as e:
            logger.warning(
                "state_file_read_error",
                path=str(state_file),
                error=str(e),
            )
            return cls._create_new()

    @classmethod
    def _create_new(cls) -> PersuadeState:
        """Create a new empty state with metadata."""
        try:
            from persuade import __version__

            version = __version__
        except ImportError:
            version = "unknown"

        return cls(
            version=STATE_SCHEMA_VERSION,
            sync=SyncState(persuade_version=version),
            session=SessionState(),
            meta=MetaState(
                created_at=datetime.now(UTC).isoformat(),
                persuade_version=version,
                schema_version=STATE_SCHEMA_VERSION,
            ),
        )

    def save(self, project_path: Path) -> None:
        """Save state to the project's state file.

        Creates the state directory if it doesn't exist.

        Args:
            project_path: Path to the project root
        """
        state_file = project_path / STATE_FILE_PATH

        # Ensure directory exists
        state_file.parent.mkdir(parents=True, exist_ok=True)

        try:
            with open(state_file, "w", encoding="utf-8") as f:
                json.dump(self.to_dict(), f, indent=2)
                f.write("\n")

            logger.debug("state_saved", path=str(state_file))
        except OSError as e:
            logger.error(
                "state_file_write_error",
                path=str(state_file),
                error=str(e),
            )
            raise

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for JSON serialization."""
        return {
            "version": self.version,
            "sync": self.sync.to_dict(),
            "session": self.session.to_dict(),
            "meta": self.meta.to_dict(),
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> PersuadeState:
        """Create from dictionary."""
        return cls(
            version=data.get("version", STATE_SCHEMA_VERSION),
            sync=SyncState.from_dict(data.get("sync", {})),
            session=SessionState.from_dict(data.get("session", {})),
            meta=MetaState.from_dict(data.get("meta", {})),
        )

    def clear_session(self) -> None:
        """Clear session state only, preserving sync state."""
        self.session.clear()

    def clear_all(self) -> None:
        """Clear all state (sync and session)."""
        self.sync = SyncState()
        self.session = SessionState()
