"""App management commands for scry-run."""

import json
import re
import shutil
import subprocess
from datetime import datetime
from pathlib import Path

import click
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.prompt import Confirm

from scry_run.home import get_apps_dir, get_app_dir
from scry_run.packages import ensure_scry_run_installed, get_venv_python, get_installed_packages

console = Console()


def get_all_apps() -> list[str]:
    """Get list of all app names."""
    apps_dir = get_apps_dir()
    if not apps_dir.exists():
        return []
    return sorted([
        d.name for d in apps_dir.iterdir()
        if d.is_dir() and (d / "app.py").exists()
    ])


def extract_description_from_app(app_dir: Path) -> str | None:
    """Extract the description from an app's app.py file.

    The description is stored in the module docstring and class docstring.
    Returns the full description text (may be multi-line).
    """
    app_file = app_dir / "app.py"
    if not app_file.exists():
        return None

    content = app_file.read_text()

    # Try to extract from module docstring (first triple-quoted string)
    # Format: """
    # {description}
    #
    # Generated by scry-run...
    # """
    match = re.search(r'^"""[\s\n]*(.*?)(?:\n\nGenerated by scry-run|""")', content, re.MULTILINE | re.DOTALL)
    if match:
        return match.group(1).strip()

    return None


@click.command("list")
def list_apps() -> None:
    """List all scry-run apps."""
    apps = get_all_apps()

    if not apps:
        console.print("[dim]No apps found.[/dim]")
        console.print(f"[dim]Create one with:[/dim] [cyan]scry-run init[/cyan]")
        return

    table = Table(show_header=True, header_style="bold")
    table.add_column("Name")
    table.add_column("Description")
    table.add_column("Path")

    for app_name in apps:
        app_dir = get_app_dir(app_name)
        description = extract_description_from_app(app_dir) or "[dim]—[/dim]"
        table.add_row(app_name, description, str(app_dir))

    console.print(table)


@click.command("which")
@click.argument("app_name")
def which_app(app_name: str) -> None:
    """Print command to run app in its environment.

    Example:
        scry-run which myapp
        $(scry-run which myapp) --help
    """
    app_dir = get_app_dir(app_name)
    app_file = app_dir / "app.py"

    if not app_file.exists():
        console.print(f"[red]Error:[/red] App '{app_name}' not found")
        raise SystemExit(1)

    # Print the full command to run the app
    click.echo(f"uv run --directory {app_dir} python {app_file}")


@click.command("rm")
@click.argument("app_name")
@click.option("--force", "-f", is_flag=True, help="Skip confirmation prompt")
def rm_app(app_name: str, force: bool) -> None:
    """Remove an scry-run app.

    This permanently deletes the app directory and all its contents.

    Example:
        scry-run rm myapp
        scry-run rm myapp --force
    """
    app_dir = get_app_dir(app_name)

    if not app_dir.exists():
        console.print(f"[red]Error:[/red] App '{app_name}' not found")
        raise SystemExit(1)

    if not force:
        if not Confirm.ask(f"[yellow]Delete app '{app_name}' and all its contents?[/yellow]"):
            console.print("[dim]Aborted.[/dim]")
            raise SystemExit(0)

    shutil.rmtree(app_dir)
    console.print(f"[green]Removed app '{app_name}'[/green]")


@click.command("reset")
@click.argument("app_name")
@click.option("--force", "-f", is_flag=True, help="Skip confirmation prompt")
def reset_app(app_name: str, force: bool) -> None:
    """Reset an app to initial state, preserving name and description.

    This removes the generated code and cache, then reinitializes the app
    as if 'scry-run init' was run with the same name and description.

    Example:
        scry-run reset myapp
        scry-run reset myapp --force
    """
    from scry_run.cli.init import MAIN_TEMPLATE, to_class_name

    app_dir = get_app_dir(app_name)

    if not app_dir.exists():
        console.print(f"[red]Error:[/red] App '{app_name}' not found")
        raise SystemExit(1)

    # Extract description before deleting
    description = extract_description_from_app(app_dir)
    if not description:
        description = f"A {app_name} application powered by LLM code generation"

    if not force:
        console.print(f"[yellow]This will reset app '{app_name}':[/yellow]")
        console.print(f"  - Remove all generated code")
        console.print(f"  - Clear the cache")
        console.print(f"  - Keep name: [cyan]{app_name}[/cyan]")
        console.print(f"  - Keep description: [cyan]{description}[/cyan]")
        if not Confirm.ask("Continue?"):
            console.print("[dim]Aborted.[/dim]")
            raise SystemExit(0)

    # Remove existing directory
    shutil.rmtree(app_dir)

    # Recreate app directory
    app_dir.mkdir(parents=True, exist_ok=True)

    # Create virtual environment and install scry-run
    ensure_scry_run_installed(app_dir)

    class_name = to_class_name(app_name)

    # Create app.py
    app_file = app_dir / "app.py"
    app_content = MAIN_TEMPLATE.format(
        name=app_name,
        description=description,
        class_name=class_name,
    )
    app_file.write_text(app_content)
    app_file.chmod(app_file.stat().st_mode | 0o111)  # Make executable

    # Create empty cache.json
    cache_file = app_dir / "cache.json"
    cache_file.write_text(json.dumps({}))

    # Create logs directory
    logs_dir = app_dir / "logs"
    logs_dir.mkdir(exist_ok=True)

    console.print(f"[green]Reset app '{app_name}' successfully[/green]")
    console.print(f"  [dim]Description:[/dim] {description}")


def _format_size(size_bytes: int) -> str:
    """Format a size in bytes to human-readable format."""
    if size_bytes < 1024:
        return f"{size_bytes} B"
    elif size_bytes < 1024 * 1024:
        return f"{size_bytes / 1024:.1f} KB"
    else:
        return f"{size_bytes / (1024 * 1024):.1f} MB"


def _format_timestamp(ts: float | None) -> str:
    """Format a timestamp to human-readable format."""
    if ts is None:
        return "—"
    dt = datetime.fromtimestamp(ts)
    return dt.strftime("%Y-%m-%d %H:%M")


def _get_python_version(app_dir: Path) -> str | None:
    """Get the Python version in the app's venv."""
    python_path = get_venv_python(app_dir)
    if not python_path.exists():
        return None
    try:
        result = subprocess.run(
            [str(python_path), "--version"],
            capture_output=True,
            text=True,
        )
        if result.returncode == 0:
            # Output is "Python 3.12.12"
            return result.stdout.strip().replace("Python ", "")
    except Exception:
        pass
    return None


def _get_cache_stats(cache_file: Path) -> dict:
    """Get statistics about the cache file."""
    stats = {
        "methods": 0,
        "total_lines": 0,
        "size_bytes": 0,
        "last_cached": None,
        "entries": [],
    }

    if not cache_file.exists():
        return stats

    stats["size_bytes"] = cache_file.stat().st_size

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

        for class_name, methods in data.items():
            for attr_name, entry in methods.items():
                stats["methods"] += 1
                code = entry.get("code", "")
                stats["total_lines"] += code.count("\n") + 1
                stats["entries"].append({
                    "class": class_name,
                    "attr": attr_name,
                    "type": entry.get("code_type", "method"),
                    "created_at": entry.get("created_at"),
                })

                # Track most recent cache entry
                created_at = entry.get("created_at")
                if created_at:
                    try:
                        ts = datetime.fromisoformat(created_at).timestamp()
                        if stats["last_cached"] is None or ts > stats["last_cached"]:
                            stats["last_cached"] = ts
                    except ValueError:
                        pass
    except (json.JSONDecodeError, KeyError):
        pass

    return stats


def _get_venv_size(venv_path: Path) -> int:
    """Get total size of venv directory."""
    if not venv_path.exists():
        return 0
    total = 0
    for f in venv_path.rglob("*"):
        if f.is_file():
            total += f.stat().st_size
    return total


@click.command("info")
@click.argument("app_name")
def info_app(app_name: str) -> None:
    """Show detailed information about an app.

    Displays paths, cache statistics, environment info, and timestamps.

    Example:
        scry-run info myapp
    """
    app_dir = get_app_dir(app_name)
    app_file = app_dir / "app.py"
    cache_file = app_dir / "cache.json"
    venv_path = app_dir / ".venv"

    if not app_dir.exists():
        console.print(f"[red]Error:[/red] App '{app_name}' not found")
        raise SystemExit(1)

    # Get description
    description = extract_description_from_app(app_dir) or "[dim]No description[/dim]"

    # Get cache stats
    cache_stats = _get_cache_stats(cache_file)

    # Get installed packages
    packages = get_installed_packages(app_dir)
    # Filter out scry-run itself and common built-in packages for display
    user_packages = [p for p in packages if not p.startswith(("pip==", "setuptools==", "wheel=="))]

    # Get Python version
    python_version = _get_python_version(app_dir) or "—"

    # Get timestamps
    app_created = app_file.stat().st_ctime if app_file.exists() else None
    app_modified = app_file.stat().st_mtime if app_file.exists() else None

    # Build output
    lines = []

    # Description section
    lines.append("[bold]Description[/bold]")
    # Indent each line of the description
    for line in description.split("\n"):
        lines.append(f"  {line}")
    lines.append("")

    # Paths section
    lines.append("[bold]Paths[/bold]")
    lines.append(f"  [dim]App directory[/dim]    {app_dir}")
    lines.append(f"  [dim]Main file[/dim]        {app_file}")
    lines.append(f"  [dim]Cache file[/dim]       {cache_file}")
    lines.append("")

    # Cache section
    lines.append("[bold]Cache[/bold]")
    if cache_stats["methods"] > 0:
        lines.append(f"  [dim]Methods[/dim]          {cache_stats['methods']}")
        lines.append(f"  [dim]Total lines[/dim]      {cache_stats['total_lines']}")
        lines.append(f"  [dim]Cache size[/dim]       {_format_size(cache_stats['size_bytes'])}")

        # Show method breakdown
        if cache_stats["entries"]:
            method_list = ", ".join(
                f"{e['attr']}" for e in cache_stats["entries"][:5]
            )
            if len(cache_stats["entries"]) > 5:
                method_list += f", ... (+{len(cache_stats['entries']) - 5} more)"
            lines.append(f"  [dim]Cached[/dim]           {method_list}")
    else:
        lines.append("  [dim]No cached methods[/dim]")
    lines.append("")

    # Environment section
    lines.append("[bold]Environment[/bold]")
    if venv_path.exists():
        venv_size = _get_venv_size(venv_path)
        lines.append(f"  [dim]Venv[/dim]             {venv_path}")
        lines.append(f"  [dim]Venv size[/dim]        {_format_size(venv_size)}")
        lines.append(f"  [dim]Python[/dim]           {python_version}")
        lines.append(f"  [dim]Packages[/dim]         {len(user_packages)}")

        # Show top packages (excluding scry-run internals)
        display_packages = [p.split("==")[0] for p in user_packages if not p.startswith("scry-run")][:5]
        if display_packages:
            pkg_list = ", ".join(display_packages)
            if len(user_packages) > 5:
                pkg_list += f", ... (+{len(user_packages) - 5} more)"
            lines.append(f"  [dim]Top packages[/dim]     {pkg_list}")
    else:
        lines.append("  [dim]No virtual environment[/dim]")
    lines.append("")

    # Timestamps section
    lines.append("[bold]Timestamps[/bold]")
    lines.append(f"  [dim]Created[/dim]          {_format_timestamp(app_created)}")
    lines.append(f"  [dim]Last modified[/dim]    {_format_timestamp(app_modified)}")
    lines.append(f"  [dim]Last cached[/dim]      {_format_timestamp(cache_stats['last_cached'])}")

    # Print as panel
    panel = Panel(
        "\n".join(lines),
        title=f"[bold]{app_name}[/bold]",
        border_style="blue",
    )
    console.print(panel)
