"""Backend abstraction for unified agent history access."""

from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Literal

Source = Literal["claude", "codex"]


@dataclass
class UnifiedSearchMatch:
    source: Source
    session_id: str
    title: str
    timestamp: datetime | None
    snippet: str


@dataclass
class UnifiedConversationSummary:
    source: Source
    session_id: str
    title: str
    start_time: datetime | None
    user_message_count: int
    assistant_message_count: int
    cwd: str | None
    file_path: Path | None = None
    mtime: float = 0.0


@dataclass
class UnifiedProject:
    name: str
    conversations: int
    latest_mtime: float
    latest_dt: datetime
    sources: set[Source]


def parse_session_ref(ref: str) -> tuple[Source | None, str]:
    """Parse a session reference with optional source prefix.

    'c:abc123' -> ('claude', 'abc123')
    'x:abc123' -> ('codex', 'abc123')
    'abc123'   -> (None, 'abc123')
    """
    if ref.startswith("c:"):
        return "claude", ref[2:]
    if ref.startswith("x:"):
        return "codex", ref[2:]
    return None, ref


def _greedy_path_reconstruct(parts: list[str]) -> str | None:
    """Reconstruct a filesystem path from dash-separated segments.

    Greedily tries to join segments with dashes when a directory with
    that name exists, falling back to treating segments as path separators.
    E.g. ["Users", "bob", "lm", "deluge"] with /Users/bob/lm-deluge on disk
    will produce "/Users/bob/lm-deluge".
    """
    if not parts:
        return None

    def _recurse(parent: str, idx: int) -> str | None:
        if idx >= len(parts):
            return parent if Path(parent).exists() else None

        # Try building a segment by joining consecutive parts with dashes.
        # Start with the longest possible segment (greedy) so we prefer
        # "lm-deluge" over "lm"/"deluge" when both exist.
        for end in range(len(parts), idx, -1):
            segment = "-".join(parts[idx:end])
            candidate = parent + "/" + segment
            if Path(candidate).exists():
                result = _recurse(candidate, end)
                if result is not None:
                    return result

        return None

    return _recurse("", 0)


def _decode_claude_project_name(dirname: str) -> str:
    """Decode a Claude projects directory name to a filesystem path.

    Claude encodes paths by replacing / with -, e.g.
    /Users/bob/my-project -> -Users-bob-my-project

    Naive replacement breaks paths containing actual dashes. We reconstruct
    the real path by greedily matching directory segments against the filesystem.
    """
    if not dirname.startswith("-"):
        return dirname

    # Try greedy filesystem-based reconstruction
    parts = dirname[1:].split("-")  # strip leading dash, split on all dashes
    reconstructed = _greedy_path_reconstruct(parts)
    if reconstructed:
        return reconstructed

    # Fallback: naive replacement
    return dirname.replace("-", "/")


class ClaudeBackend:
    """Wraps claude_history for unified access."""

    source: Source = "claude"

    def get_base_dir(self) -> Path:
        from claude_history.cli import get_projects_dir
        return get_projects_dir()

    def find_conversation_file(self, session_id: str) -> Path | None:
        from claude_history.cli import find_conversation_file
        base = self.get_base_dir()
        if not base.exists():
            return None
        return find_conversation_file(base, session_id)

    def parse_conversation(self, file_path: Path):
        from claude_history.parser import parse_conversation
        return parse_conversation(file_path)

    def search(
        self, query: str, limit: int, project: str | None = None
    ) -> list[UnifiedSearchMatch]:
        from claude_history.cli import get_projects_dir, resolve_project_path
        from claude_history.parser import search_conversations, search_project

        projects_dir = get_projects_dir()
        if not projects_dir.exists():
            return []

        if project:
            project_path = resolve_project_path(projects_dir, project)
            if not project_path:
                return []
            results = search_project(project_path, query, limit)
        else:
            results = search_conversations(projects_dir, query, limit)

        return [
            UnifiedSearchMatch(
                source="claude",
                session_id=r.session_id,
                title=r.title,
                timestamp=r.timestamp,
                snippet=r.snippet,
            )
            for r in results
        ]

    def list_conversations(
        self, project: str | None = None, limit: int = 20
    ) -> list[UnifiedConversationSummary]:
        from claude_history.cli import get_projects_dir, resolve_project_path
        from claude_history.parser import (
            find_conversations,
            get_file_size_human,
            summarize_conversation,
        )

        projects_dir = get_projects_dir()
        if not projects_dir.exists():
            return []

        if project:
            project_path = resolve_project_path(projects_dir, project)
            if not project_path:
                return []
            convos = find_conversations(project_path)
        else:
            convos = []
            for subdir in projects_dir.iterdir():
                if subdir.is_dir():
                    convos.extend(find_conversations(subdir))
            convos.sort(key=lambda p: p.stat().st_mtime, reverse=True)

        results = []
        for conv_path in convos[:limit]:
            try:
                summary = summarize_conversation(conv_path)
                # Derive cwd from directory name
                parent = conv_path.parent
                cwd = _decode_claude_project_name(parent.name)
                results.append(
                    UnifiedConversationSummary(
                        source="claude",
                        session_id=summary.session_id,
                        title=summary.title,
                        start_time=summary.start_time,
                        user_message_count=summary.user_message_count,
                        assistant_message_count=summary.assistant_message_count,
                        cwd=cwd,
                        file_path=conv_path,
                        mtime=conv_path.stat().st_mtime,
                    )
                )
            except Exception:
                continue
        return results

    def list_projects(self) -> list[UnifiedProject]:
        from claude_history.parser import find_conversations

        projects_dir = self.get_base_dir()
        if not projects_dir.exists():
            return []

        results = []
        for project_path in projects_dir.iterdir():
            if not project_path.is_dir():
                continue
            convos = find_conversations(project_path)
            if not convos:
                continue
            latest = max(c.stat().st_mtime for c in convos)
            name = _decode_claude_project_name(project_path.name)
            results.append(
                UnifiedProject(
                    name=name,
                    conversations=len(convos),
                    latest_mtime=latest,
                    latest_dt=datetime.fromtimestamp(latest),
                    sources={"claude"},
                )
            )
        return results


class CodexBackend:
    """Wraps codex_history for unified access."""

    source: Source = "codex"

    def get_base_dir(self) -> Path:
        from codex_history.cli import get_sessions_dir
        return get_sessions_dir()

    def find_conversation_file(self, session_id: str) -> Path | None:
        from codex_history.cli import find_conversation_file, get_sessions_dir
        base = get_sessions_dir()
        if not base.exists():
            return None
        return find_conversation_file(base, session_id)

    def parse_conversation(self, file_path: Path):
        from codex_history.parser import parse_conversation
        return parse_conversation(file_path)

    def search(
        self, query: str, limit: int, project: str | None = None
    ) -> list[UnifiedSearchMatch]:
        from codex_history.cli import (
            _build_catalog,
            _resolve_project_cwd,
            get_sessions_dir,
        )
        from codex_history.parser import search_conversations, search_project

        sessions_dir = get_sessions_dir()
        if not sessions_dir.exists():
            return []

        if project:
            catalog = _build_catalog(sessions_dir)
            project_cwd = _resolve_project_cwd(catalog, project)
            if not project_cwd:
                return []
            project_files = [
                item["path"] for item in catalog if item["cwd"] == project_cwd
            ]
            results = search_project(project_files, query, limit)
        else:
            results = search_conversations(sessions_dir, query, limit)

        return [
            UnifiedSearchMatch(
                source="codex",
                session_id=r.session_id,
                title=r.title,
                timestamp=r.timestamp,
                snippet=r.snippet,
            )
            for r in results
        ]

    def list_conversations(
        self, project: str | None = None, limit: int = 20
    ) -> list[UnifiedConversationSummary]:
        from codex_history.cli import (
            _build_catalog,
            _resolve_project_cwd,
            get_sessions_dir,
        )
        from codex_history.parser import find_conversations, summarize_conversation

        sessions_dir = get_sessions_dir()
        if not sessions_dir.exists():
            return []

        if project:
            catalog = _build_catalog(sessions_dir)
            project_cwd = _resolve_project_cwd(catalog, project)
            if not project_cwd:
                return []
            convos = []
            for item in catalog:
                if item["cwd"] == project_cwd:
                    convos.append(item)
            convos.sort(key=lambda x: x["mtime"], reverse=True)

            results = []
            for item in convos[:limit]:
                s = item["summary"]
                results.append(
                    UnifiedConversationSummary(
                        source="codex",
                        session_id=s.session_id,
                        title=s.title,
                        start_time=s.start_time,
                        user_message_count=s.user_message_count,
                        assistant_message_count=s.assistant_message_count,
                        cwd=s.cwd,
                        file_path=item["path"],
                        mtime=item["mtime"],
                    )
                )
            return results
        else:
            convos = find_conversations(sessions_dir)
            results = []
            for conv_path in convos[:limit]:
                try:
                    summary = summarize_conversation(conv_path)
                    results.append(
                        UnifiedConversationSummary(
                            source="codex",
                            session_id=summary.session_id,
                            title=summary.title,
                            start_time=summary.start_time,
                            user_message_count=summary.user_message_count,
                            assistant_message_count=summary.assistant_message_count,
                            cwd=summary.cwd,
                            file_path=conv_path,
                            mtime=conv_path.stat().st_mtime,
                        )
                    )
                except Exception:
                    continue
            return results

    def list_projects(self) -> list[UnifiedProject]:
        from codex_history.cli import _build_catalog, get_sessions_dir

        sessions_dir = get_sessions_dir()
        if not sessions_dir.exists():
            return []

        catalog = _build_catalog(sessions_dir)
        project_data: dict[str, UnifiedProject] = {}
        for item in catalog:
            cwd = item["cwd"] or "unknown"
            if cwd not in project_data:
                project_data[cwd] = UnifiedProject(
                    name=cwd,
                    conversations=0,
                    latest_mtime=item["mtime"],
                    latest_dt=datetime.fromtimestamp(item["mtime"]),
                    sources={"codex"},
                )
            p = project_data[cwd]
            p.conversations += 1
            if item["mtime"] > p.latest_mtime:
                p.latest_mtime = item["mtime"]
                p.latest_dt = datetime.fromtimestamp(item["mtime"])

        return list(project_data.values())


def resolve_conversation(
    session_ref: str,
) -> tuple[Source, Path]:
    """Resolve a session reference to a (source, file_path) pair.

    Raises click.UsageError on not-found or ambiguous matches.
    """
    import click

    forced_source, session_id = parse_session_ref(session_ref)

    claude = ClaudeBackend()
    codex = CodexBackend()

    claude_path = None
    codex_path = None

    if forced_source is None or forced_source == "claude":
        claude_path = claude.find_conversation_file(session_id)
    if forced_source is None or forced_source == "codex":
        codex_path = codex.find_conversation_file(session_id)

    if forced_source == "claude":
        if not claude_path:
            raise click.UsageError(f"Claude conversation not found: {session_id}")
        return "claude", claude_path

    if forced_source == "codex":
        if not codex_path:
            raise click.UsageError(f"Codex conversation not found: {session_id}")
        return "codex", codex_path

    # Auto-detect
    if claude_path and codex_path:
        raise click.UsageError(
            f"Ambiguous session ID '{session_id}' found in both Claude and Codex.\n"
            f"Use c:{session_id} or x:{session_id} to disambiguate."
        )
    if claude_path:
        return "claude", claude_path
    if codex_path:
        return "codex", codex_path

    raise click.UsageError(f"Conversation not found: {session_id}")
