"""Unified CLI for exploring Claude Code and Codex conversation history."""

import re
from datetime import datetime
from pathlib import Path

import click

from claude_history.cli import (
    _find_final_assistant_message,
    _select_messages,
    echo,
    format_timestamp,
    print_table,
    render_message,
    summarize_tools,
    truncate,
)

from .backends import (
    ClaudeBackend,
    CodexBackend,
    UnifiedConversationSummary,
    UnifiedProject,
    resolve_conversation,
)

# Source tag for display
_SOURCE_TAG = {"claude": "[c]", "codex": "[x]"}


def _resolve_here() -> str:
    """Resolve the current working directory to a project identifier."""
    return str(Path.cwd())


def _get_backends(source: str) -> tuple[ClaudeBackend | None, CodexBackend | None]:
    """Return backend instances based on source filter."""
    claude = ClaudeBackend() if source in ("all", "claude") else None
    codex = CodexBackend() if source in ("all", "codex") else None
    return claude, codex


@click.group()
def cli():
    """Explore Claude Code and Codex conversation history.

    Search, list, and view past conversations across both Claude Code
    and Codex. Session IDs can be prefixed with c: or x: to force a
    specific source (e.g., 'c:a1b2c3d4' for Claude, 'x:e5f6g7h8' for Codex).
    """
    pass


@cli.command()
@click.option(
    "--sort",
    type=click.Choice(["modified", "name", "conversations"]),
    default="modified",
    help="Sort projects by: modified (default), name, or conversations",
)
@click.option(
    "--source",
    type=click.Choice(["all", "claude", "codex"]),
    default="all",
    help="Filter by source",
)
def projects(sort: str, source: str):
    """List all projects with conversation history."""
    claude, codex = _get_backends(source)

    # Collect from both backends, merge by project path
    merged: dict[str, UnifiedProject] = {}

    for backend in (claude, codex):
        if backend is None:
            continue
        for p in backend.list_projects():
            if p.name in merged:
                existing = merged[p.name]
                existing.conversations += p.conversations
                existing.sources |= p.sources
                if p.latest_mtime > existing.latest_mtime:
                    existing.latest_mtime = p.latest_mtime
                    existing.latest_dt = p.latest_dt
            else:
                merged[p.name] = p

    project_list = list(merged.values())

    if sort == "modified":
        project_list.sort(key=lambda p: p.latest_mtime, reverse=True)
    elif sort == "name":
        project_list.sort(key=lambda p: p.name)
    elif sort == "conversations":
        project_list.sort(key=lambda p: p.conversations, reverse=True)

    if not project_list:
        echo("No projects found.")
        return

    rows = [
        [
            p.name,
            str(p.conversations),
            format_timestamp(p.latest_dt),
            ", ".join(sorted(p.sources)),
        ]
        for p in project_list
    ]
    print_table(["Project", "Conversations", "Last Modified", "Source"], rows)


@cli.command("list")
@click.argument("project", required=False)
@click.option("-n", "--limit", default=20, help="Number of conversations to show")
@click.option("--here", is_flag=True, help="Scope to the current directory's project")
@click.option(
    "--source",
    type=click.Choice(["all", "claude", "codex"]),
    default="all",
    help="Filter by source",
)
def list_conversations(
    project: str | None, limit: int, here: bool, source: str
):
    """List conversations, optionally filtered by project.

    PROJECT can be a filesystem path or project name suffix.
    """
    if here:
        if project:
            raise click.UsageError("Cannot combine PROJECT argument with --here")
        project = _resolve_here()

    claude, codex = _get_backends(source)

    # Collect from both backends
    all_convos: list[UnifiedConversationSummary] = []
    for backend in (claude, codex):
        if backend is None:
            continue
        all_convos.extend(backend.list_conversations(project, limit))

    # Sort by mtime descending, take top limit
    all_convos.sort(key=lambda c: c.mtime, reverse=True)
    all_convos = all_convos[:limit]

    if not all_convos:
        echo("No conversations found.")
        return

    rows = []
    for i, c in enumerate(all_convos):
        tag = _SOURCE_TAG[c.source]
        msg_count = f"{c.user_message_count}u/{c.assistant_message_count}a"
        date = format_timestamp(c.start_time)
        rows.append(
            [str(i + 1), tag, c.session_id[:8], truncate(c.title, 50), msg_count, date]
        )

    echo(f"Conversations (showing {len(all_convos)}):")
    print_table(["#", "Src", "Session ID", "Title", "Messages", "Date"], rows)
    echo()
    echo("Tip: Use 'agent-history view <session-id>' to view a conversation")
    echo("     Prefix with c: or x: to disambiguate (e.g., c:a1b2c3d4)")


@cli.command()
@click.argument("query")
@click.option(
    "-p", "--project", default=None, help="Limit search to a specific project"
)
@click.option("-n", "--limit", default=10, help="Number of results to show")
@click.option("--here", is_flag=True, help="Scope to the current directory's project")
@click.option(
    "--source",
    type=click.Choice(["all", "claude", "codex"]),
    default="all",
    help="Filter by source",
)
def search(query: str, project: str | None, limit: int, here: bool, source: str):
    """Search conversations for a query string."""
    if here:
        if project:
            raise click.UsageError("Cannot combine --project with --here")
        project = _resolve_here()

    claude, codex = _get_backends(source)

    all_results = []
    for backend in (claude, codex):
        if backend is None:
            continue
        all_results.extend(backend.search(query, limit, project))

    # Sort by timestamp (most recent first)
    all_results.sort(
        key=lambda r: r.timestamp or datetime.min, reverse=True
    )
    all_results = all_results[:limit]

    if not all_results:
        echo(f"No results found for: {query}")
        return

    echo(f"Found {len(all_results)} matches:")
    echo()

    for match in all_results:
        tag = _SOURCE_TAG[match.source]
        echo(f"{tag} {match.session_id[:8]} - {truncate(match.title, 40)}")
        echo(f"  {format_timestamp(match.timestamp)}")
        echo(f"  {match.snippet}")
        echo()


@cli.command()
@click.argument("session_id")
@click.option("-f", "--full", is_flag=True, help="Show full message content")
@click.option("--no-tools", is_flag=True, help="Hide tool use details")
@click.option("-n", "--limit", type=int, help="Limit number of messages shown")
@click.option("-o", "--offset", type=int, default=0, help="Skip first N messages")
@click.option(
    "--tail",
    is_flag=True,
    help="Show the last N messages instead of starting from the beginning",
)
def view(
    session_id: str,
    full: bool,
    no_tools: bool,
    limit: int | None,
    offset: int,
    tail: bool,
):
    """View a conversation by session ID.

    SESSION_ID can be prefixed with c: or x: to force a source.
    """
    source, conv_path = resolve_conversation(session_id)
    tag = _SOURCE_TAG[source]

    if source == "claude":
        backend = ClaudeBackend()
    else:
        backend = CodexBackend()

    conv = backend.parse_conversation(conv_path)

    echo(f"Conversation Info {tag}")
    echo(f"Title: {conv.title}")
    echo(f"Session: {conv.session_id}")
    echo(f"Directory: {conv.cwd or 'unknown'}")
    echo(f"Branch: {conv.git_branch or 'unknown'}")
    echo(f"Version: {conv.version or 'unknown'}")
    echo(
        f"Time: {format_timestamp(conv.start_time)} - {format_timestamp(conv.end_time)}"
    )
    echo(
        f"Messages: {conv.user_message_count} user, {conv.assistant_message_count} assistant"
    )
    echo(f"Tool uses: {conv.tool_use_count}")

    if conv.summaries:
        echo()
        echo("Summaries:")
        for summary in conv.summaries:
            indented = summary.replace("\n", "\n  ")
            echo(f"- {indented}")

    if tail and offset:
        raise click.UsageError("Cannot combine --tail with --offset")

    messages = _select_messages(
        conv.messages,
        offset=offset,
        limit=limit,
        tail=tail,
        include_tools=not no_tools,
    )
    visible_total = len(
        _select_messages(
            conv.messages,
            offset=0,
            limit=None,
            tail=False,
            include_tools=not no_tools,
        )
    )

    echo()
    echo(f"Messages (showing {len(messages)} of {visible_total}):")
    echo()

    for msg in messages:
        render_message(msg, full=full, show_tools=not no_tools)


@cli.command()
@click.argument("session_id")
def summary(session_id: str):
    """Show a concise summary of a conversation."""
    source, conv_path = resolve_conversation(session_id)
    tag = _SOURCE_TAG[source]

    if source == "claude":
        backend = ClaudeBackend()
    else:
        backend = CodexBackend()

    conv = backend.parse_conversation(conv_path)

    echo(f"Session: {conv.session_id} {tag}")
    echo(f"Title: {conv.title}")
    echo(f"Directory: {conv.cwd or 'unknown'}")
    echo(f"Time: {format_timestamp(conv.start_time)}")
    echo(
        f"Stats: {conv.user_message_count} user msgs, {conv.assistant_message_count} assistant msgs, {conv.tool_use_count} tool uses"
    )

    if conv.summaries:
        echo()
        echo("Stored Summaries:")
        for s in conv.summaries:
            indented = s.replace("\n", "\n  ")
            echo(f"- {indented}")

    echo()
    echo("Conversation Flow:")

    turn = 0
    pending_tools: list[str] = []

    for i, msg in enumerate(conv.messages):
        if msg.is_meta or msg.role == "tool":
            continue

        turn += 1
        if msg.role == "user":
            text = msg.text.strip()
            if text.startswith("<"):
                continue
            echo()
            echo(f"{turn}. User: {truncate(text, 100)}")

        elif msg.role == "assistant":
            if msg.tool_names:
                pending_tools.extend(msg.tool_names)

            text = msg.text.strip()
            if text:
                if pending_tools:
                    tool_summary = summarize_tools(pending_tools)
                    echo(f"  Tools: {tool_summary}")
                    pending_tools = []
                echo(f"  Assistant: {truncate(text, 100)}")

            next_msg = conv.messages[i + 1] if i + 1 < len(conv.messages) else None
            if pending_tools and (not next_msg or next_msg.role == "user"):
                tool_summary = summarize_tools(pending_tools)
                echo(f"  Tools: {tool_summary}")
                pending_tools = []


@cli.command()
@click.argument("session_id")
@click.option(
    "-f",
    "--format",
    "output_format",
    type=click.Choice(["text", "json"]),
    default="text",
)
def export(session_id: str, output_format: str):
    """Export a conversation to a simpler format."""
    source, conv_path = resolve_conversation(session_id)

    if source == "claude":
        backend = ClaudeBackend()
    else:
        backend = CodexBackend()

    conv = backend.parse_conversation(conv_path)

    if output_format == "json":
        import json

        messages: list[dict] = []
        output = {
            "source": source,
            "session_id": conv.session_id,
            "title": conv.title,
            "cwd": conv.cwd,
            "git_branch": conv.git_branch,
            "start_time": conv.start_time.isoformat() if conv.start_time else None,
            "messages": messages,
        }

        for msg in conv.messages:
            if msg.is_meta:
                continue
            messages.append(
                {
                    "role": msg.role,
                    "text": msg.text,
                    "tools": msg.tool_names,
                    "timestamp": msg.timestamp.isoformat() if msg.timestamp else None,
                }
            )

        click.echo(json.dumps(output, indent=2))

    else:
        tag = _SOURCE_TAG[source]
        click.echo(f"# {conv.title} {tag}")
        click.echo(f"Session: {conv.session_id}")
        click.echo(f"Date: {format_timestamp(conv.start_time)}")
        click.echo(f"Directory: {conv.cwd}")
        click.echo()

        for msg in conv.messages:
            if msg.is_meta:
                continue

            role = "USER" if msg.role == "user" else "ASSISTANT"
            click.echo(f"## {role}")

            if msg.text:
                click.echo(msg.text)

            if msg.tool_names:
                click.echo(f"\n[Tools: {', '.join(msg.tool_names)}]")

            click.echo()


@cli.command()
@click.argument("session_id")
@click.option("-c", "--max-chars", default=8000, help="Maximum characters in output")
@click.option("--include-tools", is_flag=True, help="Include tool names in output")
@click.option(
    "--prioritize-end/--from-start",
    default=True,
    help="Prioritize recent messages near the end (default: prioritize-end)",
)
def catchup(
    session_id: str,
    max_chars: int,
    include_tools: bool,
    prioritize_end: bool,
):
    """Generate a context summary for catching up in a new session.

    This outputs a compact summary suitable for pasting into a new Claude
    conversation to restore context from a previous session.
    """
    source, conv_path = resolve_conversation(session_id)
    tag = _SOURCE_TAG[source]

    if source == "claude":
        backend = ClaudeBackend()
    else:
        backend = CodexBackend()

    conv = backend.parse_conversation(conv_path)

    # Build message summaries
    entries: list[str] = []
    turn = 0
    pending_tools: list[str] = []

    for i, msg in enumerate(conv.messages):
        if msg.is_meta or msg.role == "tool":
            continue

        if msg.role == "user":
            text = msg.text.strip()
            if text.startswith("<"):
                continue
            turn += 1
            entries.append(f"**User {turn}:** {truncate(text, 200)}")
            entries.append("")
            continue

        if msg.role == "assistant":
            if msg.tool_names:
                pending_tools.extend(msg.tool_names)

            text = msg.text.strip()
            if text:
                if include_tools and pending_tools:
                    tool_summary = summarize_tools(pending_tools)
                    entries.append(f"  *Tools: {tool_summary}*")
                    entries.append("")
                    pending_tools = []

                entries.append(f"  Assistant: {truncate(text, 300)}")
                entries.append("")

            # Flush tools at end of turn
            next_msg = conv.messages[i + 1] if i + 1 < len(conv.messages) else None
            if (
                include_tools
                and pending_tools
                and (not next_msg or next_msg.role == "user")
            ):
                tool_summary = summarize_tools(pending_tools)
                entries.append(f"  *Tools: {tool_summary}*")
                entries.append("")
                pending_tools = []

    # Build output with final assistant message preserved
    final_assistant = _find_final_assistant_message(conv)
    lines = [
        f"# Previous Session Context {tag}",
        "",
        f"**Source:** {source}",
        f"**Session:** {conv.session_id}",
        f"**Project:** {conv.cwd or 'unknown'}",
        f"**Date:** {format_timestamp(conv.start_time)}",
        f"**Messages:** {conv.user_message_count} user, {conv.assistant_message_count} assistant",
        "",
    ]

    if conv.summaries:
        lines.append("## Stored Summaries")
        lines.extend(conv.summaries)
        lines.append("")

    if final_assistant:
        lines.append("## Final Assistant Message (Full)")
        lines.append("")
        lines.append(final_assistant.text.strip())
        lines.append("")

    lines.append("## Conversation Flow")
    lines.append("")

    if not entries:
        lines.append("(No message content found)")
    else:
        current_length = len("\n".join(lines))
        if prioritize_end:
            selected_reversed: list[str] = []
            for entry in reversed(entries):
                if current_length + len(entry) + 1 > max_chars:
                    continue
                selected_reversed.append(entry)
                current_length += len(entry) + 1

            selected = list(reversed(selected_reversed))
            if len(selected) < len(entries):
                lines.append(
                    "... (earlier messages omitted to prioritize recent context)"
                )
                lines.append("")
            lines.extend(selected)
        else:
            for entry in entries:
                if current_length + len(entry) + 1 > max_chars:
                    lines.append("... (truncated due to length limit)")
                    break
                lines.append(entry)
                current_length += len(entry) + 1

    output = "\n".join(lines)
    output = re.sub(r"\n{3,}", "\n\n", output)
    click.echo(output)
    click.echo(f"\n({len(output)} characters)", err=True)


@cli.command("project-summary")
@click.argument("project", required=False)
@click.option("-n", "--limit", default=5, help="Number of recent conversations")
@click.option("-c", "--max-chars", default=6000, help="Maximum characters in output")
@click.option("--here", is_flag=True, help="Scope to the current directory's project")
@click.option(
    "--source",
    type=click.Choice(["all", "claude", "codex"]),
    default="all",
    help="Filter by source",
)
def project_summary(
    project: str | None, limit: int, max_chars: int, here: bool, source: str
):
    """Generate a summary of recent conversations in a project."""
    if here:
        if project:
            raise click.UsageError("Cannot combine PROJECT argument with --here")
        project = _resolve_here()
    elif not project:
        raise click.UsageError("Either provide a PROJECT argument or use --here")

    claude, codex = _get_backends(source)

    # Collect conversations from both backends
    all_convos: list[UnifiedConversationSummary] = []
    for backend in (claude, codex):
        if backend is None:
            continue
        all_convos.extend(backend.list_conversations(project, limit))

    all_convos.sort(key=lambda c: c.mtime, reverse=True)
    all_convos = all_convos[:limit]

    if not all_convos:
        echo(f"No conversations found for project: {project}")
        return

    lines = []
    lines.append(f"# Project Summary: {project}")
    lines.append("")
    lines.append(f"Recent conversations ({len(all_convos)} shown):")
    lines.append("")

    current_length = len("\n".join(lines))

    for c in all_convos:
        tag = _SOURCE_TAG[c.source]

        if c.source == "claude":
            backend_inst = ClaudeBackend()
        else:
            backend_inst = CodexBackend()

        try:
            conv = backend_inst.parse_conversation(c.file_path)

            entry_lines = []
            entry_lines.append(
                f"## {format_timestamp(conv.start_time)} - {conv.session_id[:8]} {tag}"
            )
            entry_lines.append(f"**Title:** {conv.title}")
            entry_lines.append(
                f"**Stats:** {conv.user_message_count}u/{conv.assistant_message_count}a msgs, {conv.tool_use_count} tools"
            )

            for msg in conv.messages:
                if msg.role == "user" and not msg.is_meta:
                    text = msg.text.strip()
                    if not text.startswith("<"):
                        entry_lines.append(
                            f"**Started with:** {truncate(text, 150)}"
                        )
                        break

            for msg in reversed(conv.messages):
                if msg.role == "assistant" and msg.text.strip():
                    entry_lines.append(
                        f"**Last response:** {truncate(msg.text.strip(), 150)}"
                    )
                    break

            entry_lines.append("")

            entry = "\n".join(entry_lines)
            if current_length + len(entry) > max_chars:
                lines.append("... (more conversations truncated)")
                break
            lines.extend(entry_lines)
            current_length += len(entry)

        except Exception as e:
            lines.append(f"## {c.session_id[:8]} {tag} - Error: {e}")
            lines.append("")

    output = "\n".join(lines)
    click.echo(output)
    click.echo(f"\n({len(output)} characters)", err=True)


@cli.command("install-skill")
@click.option(
    "-d",
    "--dest",
    type=click.Path(),
    default=None,
    help="Destination directory (default: ~/.claude/skills)",
)
@click.option("-f", "--force", is_flag=True, help="Overwrite existing skill")
def install_skill(dest: str | None, force: bool):
    """Install the agent-history skill to ~/.claude/skills/.

    This makes the agent-history skill available to Claude Code (or other coding agents).
    """
    import shutil

    if dest:
        dest_dir = Path(dest)
    else:
        dest_dir = Path.home() / ".claude" / "skills"

    skill_dest = dest_dir / "agent-history"

    if skill_dest.exists() and not force:
        echo(f"Skill already exists at {skill_dest}")
        echo("Use --force to overwrite")
        return

    import agent_history

    skill_source = Path(agent_history.__file__).parent / "skill"

    if not skill_source.is_dir():
        echo(f"Error: Bundled skill not found at {skill_source}")
        return

    dest_dir.mkdir(parents=True, exist_ok=True)

    if skill_dest.exists():
        shutil.rmtree(skill_dest)
        echo(f"Removed existing skill at {skill_dest}")

    shutil.copytree(skill_source, skill_dest)
    echo(f"Installed skill to {skill_dest}")


@cli.command()
def skill():
    """Print the bundled SKILL.md contents."""
    from importlib import resources

    try:
        skill_text = (
            resources.files("agent_history")
            .joinpath("skill")
            .joinpath("SKILL.md")
            .read_text(encoding="utf-8")
        )
    except Exception as e:
        echo(f"Error reading bundled skill: {e}")
        return

    click.echo(skill_text, nl=False)


def main():
    """Entry point for the CLI."""
    cli()


if __name__ == "__main__":
    main()
