"""Actor-scoped context management commands.

Implements ``agents actor context remove``, ``agents actor context export``,
and ``agents actor context import`` per the v3 specification.  These commands
manage named conversation contexts stored under ``~/.cleveragents/context/``
using the :class:`~cleveragents.reactive.context_manager.ContextManager`
persistence layer.
"""

from __future__ import annotations

import hashlib
import json
from pathlib import Path
from typing import Annotated, Any

import typer
import yaml
from rich.console import Console
from rich.panel import Panel

from cleveragents.cli.formatting import OutputFormat, format_output
from cleveragents.reactive.context_manager import ContextManager

app = typer.Typer(
    help="Manage manual contexts for actor runs.",
)
console = Console()

_FORMAT_HELP = "Output format: json, yaml, plain, table, rich, or color (default: rich)"

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _default_context_base(context_dir: Path | None) -> Path:
    """Return the base directory where named contexts are stored."""
    if context_dir is not None:
        return context_dir
    return Path.home() / ".cleveragents" / "context"


def _list_context_names(base: Path) -> list[str]:
    """Return sorted list of context names present under *base*."""
    if not base.exists():
        return []
    return sorted(d.name for d in base.iterdir() if d.is_dir())


def _context_size_kb(ctx_mgr: ContextManager) -> float:
    """Estimate total file size of the context directory in KB."""
    total = 0
    if ctx_mgr.context_dir.exists():
        for f in ctx_mgr.context_dir.rglob("*"):
            if f.is_file():
                total += f.stat().st_size
    return round(total / 1024, 1)


def _file_checksum(path: Path) -> str:
    """Return ``sha256:<hex>`` checksum for a file."""
    h = hashlib.sha256()
    with open(path, "rb") as fh:
        for chunk in iter(lambda: fh.read(8192), b""):
            h.update(chunk)
    digest = h.hexdigest()
    return f"sha256:{digest[:4]}...{digest[-4:]}"


def _render_output(
    data: dict[str, Any],
    fmt: str,
    rich_panels: list[tuple[str, str]] | None = None,
    ok_message: str = "",
) -> None:
    """Emit command output in the requested format.

    For ``rich`` format the *rich_panels* list of ``(title, body)`` pairs
    are rendered via :class:`rich.panel.Panel`.  For all other formats the
    flat *data* dict is passed through :func:`format_output`.
    """
    if fmt == OutputFormat.RICH.value and rich_panels:
        for title, body in rich_panels:
            console.print(Panel(body, title=title, expand=False))
        if ok_message:
            console.print(f"[green]✓ OK[/green] {ok_message}")
        return

    # Machine-readable / non-rich
    console.print(format_output(data, fmt))


# ---------------------------------------------------------------------------
# Commands
# ---------------------------------------------------------------------------


@app.command("remove")
def context_remove(
    name: Annotated[
        str | None,
        typer.Argument(help="Context name to remove"),
    ] = None,
    all_contexts: Annotated[
        bool,
        typer.Option("--all", "-a", help="Remove all contexts"),
    ] = False,
    yes: Annotated[
        bool,
        typer.Option("--yes", "-y", help="Skip confirmation prompt"),
    ] = False,
    context_dir: Annotated[
        Path | None,
        typer.Option(
            "--context-dir",
            help="Directory where contexts are stored",
            resolve_path=True,
        ),
    ] = None,
    fmt: Annotated[
        str,
        typer.Option("--format", "-f", help=_FORMAT_HELP),
    ] = "rich",
) -> None:
    """Remove a named actor context or all actor contexts.

    Context data is **not recoverable** after removal.  Use ``--yes`` to
    skip the interactive confirmation prompt.

    Examples::

        agents actor context remove docs
        agents actor context remove --all --yes
    """
    import shutil

    if name and all_contexts:
        typer.echo("Error: Cannot specify NAME when using --all", err=True)
        raise typer.Exit(code=1)

    if not name and not all_contexts:
        typer.echo("Error: Must specify NAME or use --all", err=True)
        raise typer.Exit(code=1)

    base = _default_context_base(context_dir)

    if all_contexts:
        names = _list_context_names(base)
        if not names:
            typer.echo("No contexts found to remove.")
            return

        if not yes:
            typer.echo(f"Found {len(names)} context(s) to remove:")
            for cname in names:
                typer.echo(f"  - {cname}")
            if not typer.confirm("Remove all?"):
                typer.echo("Remove cancelled.")
                return

        for cname in names:
            shutil.rmtree(base / cname)

        data: dict[str, Any] = {
            "context_removed": {
                "context": "all",
                "status": "removed",
                "count": len(names),
            },
            "stats": {
                "remaining_size_kb": 0,
            },
        }
        panels = [
            (
                "Context Removed",
                (
                    f"[bold]Context:[/bold] all ({len(names)} removed)\n"
                    f"[bold]Status:[/bold] removed"
                ),
            ),
            (
                "Stats",
                "[bold]Remaining Size:[/bold] 0 KB",
            ),
        ]
        _render_output(data, fmt, rich_panels=panels, ok_message="Context updated")
        return

    # Single context removal
    assert name is not None
    target = base / name
    if not target.exists():
        typer.echo(f"Error: Context '{name}' does not exist.", err=True)
        raise typer.Exit(code=1)

    ctx_mgr = ContextManager(name, context_dir)

    if not yes and not typer.confirm(f"Remove context '{name}'?"):
        typer.echo("Remove cancelled.")
        return

    ctx_mgr.delete()

    # Compute remaining size across all contexts
    remaining_total = 0.0
    for other_name in _list_context_names(base):
        other_mgr = ContextManager(other_name, context_dir)
        remaining_total += _context_size_kb(other_mgr)

    data = {
        "context_removed": {
            "context": name,
            "status": "removed",
        },
        "stats": {
            "remaining_size_kb": remaining_total,
        },
    }
    panels = [
        (
            "Context Removed",
            (f"[bold]Context:[/bold] {name}\n[bold]Status:[/bold] removed"),
        ),
        (
            "Stats",
            (f"[bold]Remaining Size:[/bold] {remaining_total} KB"),
        ),
    ]
    _render_output(data, fmt, rich_panels=panels, ok_message="Context updated")


@app.command("export")
def context_export(
    name: Annotated[
        str,
        typer.Argument(help="Context name to export"),
    ],
    output: Annotated[
        Path,
        typer.Option(
            "--output",
            "-o",
            help="Output file path (JSON or YAML)",
            resolve_path=True,
        ),
    ] = ...,  # type: ignore[assignment]
    context_dir: Annotated[
        Path | None,
        typer.Option(
            "--context-dir",
            help="Directory where contexts are stored",
            resolve_path=True,
        ),
    ] = None,
    fmt: Annotated[
        str,
        typer.Option("--format", "-f", help=_FORMAT_HELP),
    ] = "rich",
) -> None:
    """Export a named actor context to a JSON or YAML file.

    The exported file contains messages, metadata, state, and
    global_context and can later be re-imported with
    ``agents actor context import``.

    Examples::

        agents actor context export docs --output /tmp/docs-context.json
        agents actor context export docs -o ctx.yaml --format json
    """
    base = _default_context_base(context_dir)
    if not (base / name).exists():
        typer.echo(f"Error: Context '{name}' does not exist.", err=True)
        raise typer.Exit(code=1)

    ctx_mgr = ContextManager(name, context_dir)

    # Build export payload
    export_data: dict[str, Any] = {
        "context_name": ctx_mgr.context_name,
        "messages": ctx_mgr.messages,
        "metadata": ctx_mgr.metadata,
        "state": ctx_mgr.state,
        "global_context": ctx_mgr.global_context,
    }

    # Determine serialisation format from file extension
    output.parent.mkdir(parents=True, exist_ok=True)
    suffix = output.suffix.lower()
    if suffix in (".yaml", ".yml"):
        with open(output, "w", encoding="utf-8") as fh:
            yaml.dump(export_data, fh, default_flow_style=False, sort_keys=False)
    else:
        with open(output, "w", encoding="utf-8") as fh:
            json.dump(export_data, fh, indent=2)

    items = len(ctx_mgr.messages)
    size_kb = round(output.stat().st_size / 1024, 1)
    checksum = _file_checksum(output)

    data: dict[str, Any] = {
        "context_export": {
            "context": name,
            "output": str(output),
            "items": items,
            "size_kb": size_kb,
        },
        "integrity": {
            "checksum": checksum,
            "compressed": False,
        },
    }
    panels = [
        (
            "Context Export",
            (
                f"[bold]Context:[/bold] {name}\n"
                f"[bold]Output:[/bold] {output}\n"
                f"[bold]Items:[/bold] {items}\n"
                f"[bold]Size:[/bold] {size_kb} KB"
            ),
        ),
        (
            "Integrity",
            (f"[bold]Checksum:[/bold] {checksum}\n[bold]Compressed:[/bold] no"),
        ),
    ]
    _render_output(data, fmt, rich_panels=panels, ok_message="Export completed")


@app.command("import")
def context_import(
    name: Annotated[
        str | None,
        typer.Argument(
            help="Name for the imported context (inferred from file if omitted)"
        ),
    ] = None,
    input_file: Annotated[
        Path,
        typer.Option(
            "--input",
            "-i",
            help="Input file path (JSON or YAML)",
            exists=True,
            file_okay=True,
            dir_okay=False,
            readable=True,
            resolve_path=True,
        ),
    ] = ...,  # type: ignore[assignment]
    update: Annotated[
        bool,
        typer.Option("--update", help="Replace existing context with same name"),
    ] = False,
    context_dir: Annotated[
        Path | None,
        typer.Option(
            "--context-dir",
            help="Directory where contexts are stored",
            resolve_path=True,
        ),
    ] = None,
    fmt: Annotated[
        str,
        typer.Option("--format", "-f", help=_FORMAT_HELP),
    ] = "rich",
) -> None:
    """Import a context from a JSON or YAML file.

    If ``NAME`` is omitted the context name is inferred from the
    ``context_name`` key inside the file.  Use ``--update`` to replace
    an existing context with the same name.

    Examples::

        agents actor context import docs --input /tmp/docs-context.json
        agents actor context import --input ctx.yaml --update
    """
    # Parse input file (JSON or YAML)
    text = input_file.read_text(encoding="utf-8")
    suffix = input_file.suffix.lower()
    if suffix in (".yaml", ".yml"):
        file_data: dict[str, Any] = yaml.safe_load(text) or {}
    else:
        try:
            file_data = json.loads(text)
        except json.JSONDecodeError:
            # Fallback to YAML
            file_data = yaml.safe_load(text) or {}

    if not isinstance(file_data, dict):
        typer.echo("Error: Import file must contain a JSON/YAML object.", err=True)
        raise typer.Exit(code=1)

    # Determine context name
    resolved_name = name or file_data.get("context_name")
    if not resolved_name:
        # Infer from filename (stem)
        resolved_name = input_file.stem

    ctx_mgr = ContextManager(resolved_name, context_dir)

    if ctx_mgr.exists() and not update:
        typer.echo(
            f"Error: Context '{resolved_name}' already exists. "
            "Use --update to replace.",
            err=True,
        )
        raise typer.Exit(code=1)

    strategy = "replace" if ctx_mgr.exists() else "create"

    # Import via ContextManager
    ctx_mgr.import_context(input_file)

    items = len(ctx_mgr.messages)

    data: dict[str, Any] = {
        "context_import": {
            "context": resolved_name,
            "input": str(input_file),
            "items": items,
        },
        "merge": {
            "strategy": strategy,
            "conflicts": 0,
        },
    }
    panels = [
        (
            "Context Import",
            (
                f"[bold]Context:[/bold] {resolved_name}\n"
                f"[bold]Input:[/bold] {input_file}\n"
                f"[bold]Items:[/bold] {items}"
            ),
        ),
        (
            "Merge",
            (f"[bold]Strategy:[/bold] {strategy}\n[bold]Conflicts:[/bold] 0"),
        ),
    ]
    _render_output(data, fmt, rich_panels=panels, ok_message="Import completed")
