"""CLI for exploring Claude Code conversation history."""

import importlib.resources
import os
import re
import shutil
from pathlib import Path

import click

from .parser import (
    Message,
    find_conversations,
    get_file_size_human,
    parse_conversation,
    search_conversations,
    summarize_conversation,
)

# Default Claude projects directory
DEFAULT_CLAUDE_DIR = Path.home() / ".claude" / "projects"


def get_projects_dir() -> Path:
    """Get the Claude projects directory."""
    env_dir = os.environ.get("CLAUDE_HISTORY_DIR")
    if env_dir:
        return Path(env_dir)
    return DEFAULT_CLAUDE_DIR


def echo(text: str = "") -> None:
    """Print a line."""
    click.echo(text)


def print_table(headers: list[str], rows: list[list[str]]) -> None:
    """Print a simple tab-separated table."""
    for row in rows:
        for i, cell in enumerate(row):
            if cell is None:
                row[i] = ""
            else:
                row[i] = str(cell).replace("\t", " ").replace("\n", " ")
    click.echo("\t".join(headers))
    for row in rows:
        click.echo("\t".join(row))


def resolve_project_path(projects_dir: Path, project: str) -> Path | None:
    """Resolve a project identifier to its actual Claude projects storage path.

    Handles multiple input formats:
    - Direct path within ~/.claude/projects/: -Users-bob-myproject
    - Original filesystem path: /Users/bob/myproject (converted to storage format)
    - Display format with slashes: /Users/bob/myproject (as shown by 'projects' command)
    - Just the project name: myproject (searches for matching suffix)
    """
    # 1. Convert filesystem path to storage format (slashes -> dashes)
    # e.g., /Users/bob/myproject -> -Users-bob-myproject
    # This is the most common case - user passes their actual project path
    storage_name = project.replace("/", "-")
    if not storage_name.startswith("-") and project.startswith("/"):
        storage_name = "-" + storage_name.lstrip("-")
    storage_path = projects_dir / storage_name
    if storage_path.exists():
        return storage_path

    # 2. Check if it exists as a subdirectory name in projects_dir (already in storage format)
    direct_child = projects_dir / project
    if direct_child.exists():
        return direct_child

    # 3. Check if it's a direct path that exists (e.g., full path to projects dir)
    project_path = Path(project)
    if project_path.exists() and project_path.is_dir():
        # Only use if it looks like a Claude projects path (contains .jsonl files)
        if list(project_path.glob("*.jsonl")):
            return project_path

    # 4. Try partial matching - find projects ending with the given name
    # This handles cases like "lci-project" matching "-Users-benjamin-Desktop-repos-lci-project"
    search_suffix = project.replace("/", "-")
    for child in projects_dir.iterdir():
        if child.is_dir():
            # Match "-suffix" or exact suffix at end of name
            if child.name.endswith("-" + search_suffix) or child.name == search_suffix:
                return child

    return None


def format_timestamp(dt) -> str:
    """Format a datetime for display."""
    if not dt:
        return "unknown"
    return dt.strftime("%Y-%m-%d %H:%M")


def truncate(text: str, max_len: int) -> str:
    """Truncate text with ellipsis."""
    if len(text) <= max_len:
        return text
    return text[: max_len - 3] + "..."


def find_conversation_file(projects_dir: Path, session_id: str) -> Path | None:
    """Find a conversation file by session ID (full or partial)."""
    # Check if it's a direct file path
    if Path(session_id).exists():
        return Path(session_id)

    # Search in all project directories
    for project_path in projects_dir.iterdir():
        if not project_path.is_dir():
            continue

        for conv_path in project_path.glob("*.jsonl"):
            if conv_path.stem == session_id or conv_path.stem.startswith(session_id):
                return conv_path

    return None


def summarize_tools(tools: list[str]) -> str:
    """Summarize a list of tool names into a compact string."""
    from collections import Counter

    counts = Counter(tools)
    parts = []
    for tool, count in counts.most_common():
        if count > 1:
            parts.append(f"{tool}×{count}")
        else:
            parts.append(tool)
    return ", ".join(parts)


def render_message(msg: Message, full: bool = False, show_tools: bool = True):
    """Render a message without rich formatting for LLM-friendly output."""
    if msg.role == "tool" and not show_tools:
        return

    header_parts = [msg.role.upper()]
    if msg.timestamp:
        header_parts.append(format_timestamp(msg.timestamp))
    if msg.model:
        header_parts.append(msg.model)
    if msg.is_sidechain:
        header_parts.append("sidechain")
    echo(" | ".join(header_parts))

    def echo_block(text: str) -> None:
        for line in text.splitlines() or [""]:
            echo(f"  {line}")

    for block in msg.blocks:
        if block.type == "text":
            text = block.text or ""
            if text.strip().startswith("<") and not full:
                if any(
                    tag in text
                    for tag in [
                        "<local-command",
                        "<system-reminder",
                        "<command-name>",
                    ]
                ):
                    echo_block("(system/command content)")
                    continue

            if not full and len(text) > 500:
                text = text[:500] + "\n... (truncated, use --full to see all)"
            echo_block(text)

        elif block.type == "tool_use" and show_tools:
            echo_block(f"TOOL: {block.tool_name}")
            if block.tool_input and full:
                import json

                input_str = json.dumps(block.tool_input, indent=2)
                if len(input_str) > 200:
                    input_str = input_str[:200] + "..."
                for line in input_str.splitlines():
                    echo_block(line)

        elif block.type == "tool_result" and show_tools:
            result_text = block.text or ""
            if not full and len(result_text) > 200:
                result_text = result_text[:200] + "..."
            echo_block(f"RESULT: {result_text}")

        elif block.type == "thinking":
            if full:
                text = block.text or ""
                if len(text) > 300:
                    text = text[:300] + "..."
                echo_block(f"THINKING: {text}")
            else:
                echo_block("THINKING: (hidden)")

    echo()


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

    Use this tool to list, search, and view past Claude Code conversations.
    """
    pass


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

    projects_dir = get_projects_dir()

    if not projects_dir.exists():
        echo(f"Directory not found: {projects_dir}")
        return

    # Collect project data
    project_data = []
    for project_path in projects_dir.iterdir():
        if not project_path.is_dir():
            continue

        convos = find_conversations(project_path)
        if not convos:
            continue

        # Get most recent modification time
        latest = max(c.stat().st_mtime for c in convos)
        latest_dt = datetime.fromtimestamp(latest)

        # Decode project name (replace dashes with slashes to show path)
        name = project_path.name
        if name.startswith("-"):
            name = name.replace("-", "/")

        project_data.append(
            {
                "name": name,
                "conversations": len(convos),
                "latest_dt": latest_dt,
                "latest_mtime": latest,
            }
        )

    # Sort based on option
    if sort == "modified":
        project_data.sort(key=lambda p: p["latest_mtime"], reverse=True)
    elif sort == "name":
        project_data.sort(key=lambda p: p["name"])
    elif sort == "conversations":
        project_data.sort(key=lambda p: p["conversations"], reverse=True)

    rows = [
        [p["name"], str(p["conversations"]), format_timestamp(p["latest_dt"])]
        for p in project_data
    ]
    print_table(["Project", "Conversations", "Last Modified"], rows)


@cli.command("list")
@click.argument("project", required=False)
@click.option("-n", "--limit", default=20, help="Number of conversations to show")
def list_conversations(project: str | None, limit: int):
    """List conversations in a project.

    PROJECT can be a full filesystem path (e.g., /Users/bob/myproject),
    the display path shown by 'projects' command, or just a project name suffix.
    """
    projects_dir = get_projects_dir()

    if project:
        project_path = resolve_project_path(projects_dir, project)
        if not project_path:
            echo(f"Project not found: {project}")
            echo("Tip: Use 'claude-history projects' to see available projects")
            return
    else:
        # List all conversations across all projects
        project_path = projects_dir

    # Find conversations
    if project_path.is_file() and project_path.suffix == ".jsonl":
        convos = [project_path]
    elif project_path.is_dir():
        if project:
            convos = find_conversations(project_path)
        else:
            # Gather from all subdirectories
            convos = []
            for subdir in project_path.iterdir():
                if subdir.is_dir():
                    convos.extend(find_conversations(subdir))
            convos.sort(key=lambda p: p.stat().st_mtime, reverse=True)
    else:
        echo(f"Invalid path: {project_path}")
        return

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

    rows = []
    for i, conv_path in enumerate(convos[:limit]):
        try:
            summary = summarize_conversation(conv_path)
            title = truncate(summary.title, 50)
            msg_count = (
                f"{summary.user_message_count}u/{summary.assistant_message_count}a"
            )
            date = format_timestamp(summary.start_time)
            size = get_file_size_human(conv_path)
            rows.append(
                [str(i + 1), summary.session_id[:8], title, msg_count, size, date]
            )
        except Exception as e:
            rows.append([str(i + 1), conv_path.stem[:8], f"Error: {e}", "-", "-", "-"])

    echo(f"Conversations ({len(convos)} total, showing {min(limit, len(convos))})")
    print_table(["#", "Session ID", "Title", "Messages", "Size", "Date"], rows)
    echo("Tip: Use 'claude-history view <session-id>' to view a conversation")


@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")
def view(session_id: str, full: bool, no_tools: bool, limit: int | None, offset: int):
    """View a conversation by session ID.

    SESSION_ID can be a full ID, partial ID, or a file path.
    """
    projects_dir = get_projects_dir()

    # Find the conversation file
    conv_path = find_conversation_file(projects_dir, session_id)
    if not conv_path:
        echo(f"Conversation not found: {session_id}")
        return

    conv = parse_conversation(conv_path)

    # Print header
    echo("Conversation Info")
    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}")

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

    # Print messages
    messages = conv.messages[offset:]
    if limit:
        messages = messages[:limit]

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

    for msg in messages:
        if msg.is_meta:
            continue

        render_message(msg, full=full, show_tools=not no_tools)


@cli.command()
@click.argument("query")
@click.option("-n", "--limit", default=10, help="Number of results to show")
def search(query: str, limit: int):
    """Search conversations for a query string."""
    projects_dir = get_projects_dir()

    results = search_conversations(projects_dir, query, limit)

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

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

    for match in results:
        echo(f"{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")
def summary(session_id: str):
    """Show a concise summary of a conversation."""
    projects_dir = get_projects_dir()

    conv_path = find_conversation_file(projects_dir, session_id)
    if not conv_path:
        echo(f"Conversation not found: {session_id}")
        return

    conv = parse_conversation(conv_path)

    # Print basic info
    echo(f"Session: {conv.session_id}")
    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"
    )

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

    # Generate a quick summary from messages
    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()
            # Skip system messages
            if text.startswith("<"):
                continue
            echo()
            echo(f"{turn}. User: {truncate(text, 100)}")

        elif msg.role == "assistant":
            # Collect tools used
            if msg.tool_names:
                pending_tools.extend(msg.tool_names)

            # Show text summary
            text = msg.text.strip()
            if text:
                # If we have pending tools, show them first
                if pending_tools:
                    tool_summary = summarize_tools(pending_tools)
                    echo(f"  Tools: {tool_summary}")
                    pending_tools = []
                echo(f"  Assistant: {truncate(text, 100)}")

            # Check if next message is a user message or end - if so, flush tools
            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."""
    projects_dir = get_projects_dir()

    conv_path = find_conversation_file(projects_dir, session_id)
    if not conv_path:
        echo(f"Conversation not found: {session_id}")
        return

    conv = parse_conversation(conv_path)

    if output_format == "json":
        import json

        output = {
            "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": [],
        }

        for msg in conv.messages:
            if msg.is_meta:
                continue
            output["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:  # text format
        click.echo(f"# {conv.title}")
        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")
def catchup(session_id: str, max_chars: int, include_tools: 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.
    """
    projects_dir = get_projects_dir()

    conv_path = find_conversation_file(projects_dir, session_id)
    if not conv_path:
        echo(f"Conversation not found: {session_id}")
        return

    conv = parse_conversation(conv_path)

    # Build the catchup summary
    lines = []
    lines.append("# Previous Session Context")
    lines.append("")
    lines.append(f"**Session:** {conv.session_id}")
    lines.append(f"**Project:** {conv.cwd or 'unknown'}")
    lines.append(f"**Date:** {format_timestamp(conv.start_time)}")
    lines.append(
        f"**Messages:** {conv.user_message_count} user, {conv.assistant_message_count} assistant"
    )
    lines.append("")

    # Include stored summaries if available
    if conv.summaries:
        lines.append("## Stored Summaries")
        for s in conv.summaries:
            lines.append(s)
            lines.append("")

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

    # Build message summaries
    current_length = len("\n".join(lines))
    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
            entry = f"**User {turn}:** {truncate(text, 200)}"
            if current_length + len(entry) + 10 > max_chars:
                lines.append("... (truncated due to length limit)")
                break
            lines.append(entry)
            current_length += len(entry) + 1

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

            text = msg.text.strip()
            if text:
                # Flush pending tools
                if include_tools and pending_tools:
                    tool_summary = summarize_tools(pending_tools)
                    lines.append(f"  *Tools: {tool_summary}*")
                    pending_tools = []

                entry = f"  Assistant: {truncate(text, 300)}"
                if current_length + len(entry) + 10 > max_chars:
                    lines.append("... (truncated due to length limit)")
                    break
                lines.append(entry)
                current_length += len(entry) + 1

            # 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)
                lines.append(f"  *Tools: {tool_summary}*")
                pending_tools = []

        lines.append("")

    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")
@click.option("-n", "--limit", default=5, help="Number of recent conversations")
@click.option("-c", "--max-chars", default=6000, help="Maximum characters in output")
def project_summary(project: str, limit: int, max_chars: int):
    """Generate a summary of recent conversations in a project.

    Useful for getting context on what's been happening in a project
    across multiple sessions.
    """
    projects_dir = get_projects_dir()

    # Find project directory
    project_path = resolve_project_path(projects_dir, project)
    if not project_path:
        echo(f"Project not found: {project}")
        echo("Tip: Use 'claude-history projects' to see available projects")
        return

    convos = find_conversations(project_path)[:limit]
    if not convos:
        echo("No conversations found.")
        return

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

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

    for conv_path in convos:
        try:
            conv = parse_conversation(conv_path)

            entry_lines = []
            entry_lines.append(
                f"## {format_timestamp(conv.start_time)} - {conv.session_id[:8]}"
            )
            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"
            )

            # Get first user message as context
            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

            # Get last substantive assistant message
            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"## {conv_path.stem[:8]} - 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 skill for claude-history to ~/.claude/skills/.

    This makes the claude-history skill available to Claude Code (or other coding agents).
    """
    # Determine destination
    if dest:
        dest_dir = Path(dest)
    else:
        dest_dir = Path.home() / ".claude" / "skills"

    skill_dest = dest_dir / "claude-history"

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

    # Get the bundled skill directory
    try:
        skill_source = importlib.resources.files("claude_history") / "skill"
    except (TypeError, AttributeError):
        # Fallback for older Python
        import claude_history

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

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

    # Create destination directory
    dest_dir.mkdir(parents=True, exist_ok=True)

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

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


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


if __name__ == "__main__":
    main()
