"""CLI interface for Ogham memory operations."""

from typing import Optional

import typer
from rich.console import Console
from rich.table import Table

app = typer.Typer(
    name="ogham",
    help="Ogham Shared Memory — persistent memory for AI clients.",
    invoke_without_command=True,
)
console = Console()


def _run_server(
    transport: str | None = None,
    host: str | None = None,
    port: int | None = None,
):
    from ogham.server import main as server_main

    server_main(transport=transport, host=host, port=port)


@app.callback()
def main_callback(ctx: typer.Context):
    """Ogham MCP — persistent memory for AI clients."""
    if ctx.invoked_subcommand is None:
        _run_server()


@app.command()
def serve(
    transport: Optional[str] = typer.Option(None, help="Transport: stdio or sse"),
    host: Optional[str] = typer.Option(None, help="SSE bind host (default 127.0.0.1)"),
    port: Optional[int] = typer.Option(None, help="SSE port (default 8742)"),
):
    """Start the MCP server."""
    _run_server(transport=transport, host=host, port=port)


@app.command()
def store(
    content: str = typer.Argument(help="The text content to remember"),
    profile: str = typer.Option(None, help="Profile to store in"),
    tags: Optional[list[str]] = typer.Option(None, "--tag", help="Tags for the memory"),
    source: str = typer.Option("cli", help="Source identifier"),
):
    """Store a new memory."""
    from datetime import datetime, timedelta, timezone

    from ogham.config import settings
    from ogham.database import get_profile_ttl
    from ogham.database import store_memory as db_store
    from ogham.embeddings import generate_embedding

    target = profile or settings.default_profile
    embedding = generate_embedding(content)

    ttl_days = get_profile_ttl(target)
    expires_at = None
    if ttl_days is not None:
        expires_at = (datetime.now(timezone.utc) + timedelta(days=ttl_days)).isoformat()

    result = db_store(
        content=content,
        embedding=embedding,
        profile=target,
        source=source,
        tags=tags,
        expires_at=expires_at,
    )
    console.print(f"[green]Stored memory {result['id']} in profile '{target}'[/green]")
    if expires_at:
        console.print(f"[dim]Expires: {expires_at[:19]}[/dim]")


@app.command()
def health():
    """Check connectivity to Supabase and embedding provider."""
    from ogham.health import full_health_check

    result = full_health_check()

    table = Table(title="Health Check")
    table.add_column("Component", style="bold")
    table.add_column("Status")
    table.add_column("Details")

    for component, details in result.items():
        status = details.get("status", "unknown")
        style = "green" if status == "ok" else "red" if status == "error" else "yellow"
        info = {k: v for k, v in details.items() if k != "status"}
        table.add_row(component, f"[{style}]{status}[/{style}]", str(info) if info else "")

    console.print(table)


@app.command()
def profiles():
    """List all memory profiles and their counts."""
    from ogham.database import list_profiles as db_list_profiles

    data = db_list_profiles()

    table = Table(title="Profiles")
    table.add_column("Profile", style="bold")
    table.add_column("Memories", justify="right")

    for row in data:
        table.add_row(row["profile"], str(row["count"]))

    console.print(table)


@app.command()
def stats(profile: str = typer.Option(None, help="Profile to show stats for")):
    """Show statistics for a memory profile."""
    from ogham.config import settings
    from ogham.database import get_memory_stats

    target = profile or settings.default_profile
    data = get_memory_stats(profile=target)

    console.print(f"\n[bold]Profile:[/bold] {data.get('profile', target)}")
    console.print(f"[bold]Total memories:[/bold] {data.get('total', 0)}")

    sources = data.get("sources") or {}
    if sources:
        source_str = ", ".join(f"{k}: {v}" for k, v in sources.items())
        console.print(f"[bold]Sources:[/bold] {source_str}")

    top_tags = data.get("top_tags") or []
    if top_tags:
        tag_str = ", ".join(f"{t['tag']} ({t['count']})" for t in top_tags[:10])
        console.print(f"[bold]Top tags:[/bold] {tag_str}")

    console.print()


@app.command()
def search(
    query: str = typer.Argument(help="Search query"),
    limit: int = typer.Option(10, help="Max results"),
    profile: str = typer.Option(None, help="Profile to search"),
    tags: Optional[list[str]] = typer.Option(None, "--tag", help="Filter by tag"),
):
    """Search memories by meaning and keywords (hybrid search)."""
    from ogham.config import settings
    from ogham.database import hybrid_search_memories
    from ogham.embeddings import generate_embedding

    target = profile or settings.default_profile
    embedding = generate_embedding(query)
    results = hybrid_search_memories(
        query_text=query,
        query_embedding=embedding,
        profile=target,
        limit=limit,
        tags=tags,
    )

    if not results:
        console.print("[yellow]No matching memories found.[/yellow]")
        return

    table = Table(title=f"Search Results ({len(results)} matches)")
    table.add_column("Relevance", justify="right", width=10)
    table.add_column("Content")
    table.add_column("Tags")

    for r in results:
        relevance = f"{r.get('relevance', 0):.3f}"
        content = r["content"][:120]
        tags_str = ", ".join(r.get("tags", []))
        table.add_row(relevance, content, tags_str)

    console.print(table)


@app.command(name="list")
def list_memories(
    limit: int = typer.Option(10, help="Max results"),
    profile: str = typer.Option(None, help="Profile to list"),
    tags: Optional[list[str]] = typer.Option(None, "--tag", help="Filter by tag"),
    source: Optional[str] = typer.Option(None, help="Filter by source"),
):
    """List recent memories."""
    from ogham.config import settings
    from ogham.database import list_recent_memories

    target = profile or settings.default_profile
    results = list_recent_memories(profile=target, limit=limit, source=source, tags=tags)

    if not results:
        console.print("[yellow]No memories found.[/yellow]")
        return

    table = Table(title=f"Recent Memories ({len(results)})")
    table.add_column("Created", width=20)
    table.add_column("Content")
    table.add_column("Tags")
    table.add_column("Source")

    for r in results:
        table.add_row(
            r.get("created_at", "")[:19],
            r["content"][:100],
            ", ".join(r.get("tags", [])),
            r.get("source", ""),
        )

    console.print(table)


@app.command()
def cleanup(
    profile: str = typer.Option(None, help="Profile to clean"),
    yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
):
    """Remove expired memories."""
    from ogham.config import settings
    from ogham.database import cleanup_expired as db_cleanup_expired
    from ogham.database import count_expired as db_count_expired

    target = profile or settings.default_profile
    count = db_count_expired(target)

    if count == 0:
        console.print(f"[green]No expired memories in profile '{target}'.[/green]")
        return

    console.print(f"Found [bold]{count}[/bold] expired memories in profile '{target}'.")

    if not yes:
        confirm = typer.confirm("Delete them?")
        if not confirm:
            console.print("[yellow]Aborted.[/yellow]")
            return

    deleted = db_cleanup_expired(target)
    console.print(f"[green]Deleted {deleted} expired memories.[/green]")


@app.command(name="export")
def export_cmd(
    profile: str = typer.Option(None, help="Profile to export"),
    format: str = typer.Option("json", help="Output format: json or markdown"),
    output: Optional[str] = typer.Option(
        None, "--output", "-o", help="Output file (stdout if omitted)"
    ),
):
    """Export memories from a profile."""
    from ogham.config import settings
    from ogham.export_import import export_memories

    target = profile or settings.default_profile
    data = export_memories(target, format=format)

    if output:
        with open(output, "w") as f:
            f.write(data)
        console.print(f"[green]Exported to {output}[/green]")
    else:
        console.print(data)


@app.command(name="import")
def import_cmd(
    file: str = typer.Argument(help="JSON file to import"),
    profile: str = typer.Option(None, help="Profile to import into"),
    dedup: float = typer.Option(0.8, help="Dedup threshold (0 to disable)"),
):
    """Import memories from a JSON export file."""
    from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeRemainingColumn

    from ogham.config import settings
    from ogham.export_import import import_memories

    target = profile or settings.default_profile

    with open(file) as f:
        data = f.read()

    with Progress(
        SpinnerColumn(),
        TextColumn("[progress.description]{task.description}"),
        BarColumn(),
        TextColumn("{task.completed}/{task.total}"),
        TimeRemainingColumn(),
        console=console,
    ) as progress:
        embed_task = progress.add_task("Embedding...", total=None)
        dedup_task = progress.add_task("Deduplicating...", total=None, visible=False)

        def on_embed_progress(embedded, total):
            if progress.tasks[embed_task].total is None:
                progress.update(embed_task, total=total)
            progress.update(embed_task, completed=embedded)

        def on_progress(imported, skipped, total):
            progress.update(embed_task, visible=False)
            if progress.tasks[dedup_task].total is None:
                progress.update(dedup_task, total=total, visible=True)
            progress.update(
                dedup_task,
                completed=imported + skipped,
                description=f"Processing ({imported} new, {skipped} skipped)",
            )

        result = import_memories(
            data,
            profile=target,
            dedup_threshold=dedup,
            on_progress=on_progress,
            on_embed_progress=on_embed_progress,
        )

    console.print(
        f"[green]Imported {result['imported']} memories, "
        f"skipped {result['skipped']} duplicates.[/green]"
    )


@app.command()
def init(
    db_url: str = typer.Option(None, help="PostgreSQL connection string"),
    provider: str = typer.Option(
        None, help="Embedding provider (ollama/openai/voyage/mistral/gemini/onnx)"
    ),
    api_key: str = typer.Option(None, help="Embedding provider API key"),
    backend: str = typer.Option(None, help="Database backend (supabase/postgres)"),
    supabase_url: str = typer.Option(None, help="Supabase project URL"),
    supabase_key: str = typer.Option(None, help="Supabase anon key"),
    dim: int = typer.Option(None, help="Embedding dimensions (default: 512)"),
    mode: str = typer.Option(None, help="Execution mode (uvx/docker)"),
    skip_schema: bool = typer.Option(False, help="Skip schema migration"),
    skip_clients: bool = typer.Option(False, help="Skip MCP client configuration"),
    skip_test: bool = typer.Option(False, help="Skip connection test"),
):
    """Interactive setup wizard. Configures database, embeddings, and MCP clients."""
    from ogham.init_wizard import run_init

    run_init(
        db_url=db_url,
        provider=provider,
        api_key=api_key,
        backend=backend,
        supabase_url=supabase_url,
        supabase_key=supabase_key,
        dim=dim,
        mode=mode,
        skip_schema=skip_schema,
        skip_clients=skip_clients,
        skip_test=skip_test,
    )


@app.command("download-model")
def download_model(
    model: str = typer.Argument("bge-m3", help="Model to download (only bge-m3 supported)"),
    path: Optional[str] = typer.Option(
        None, "--path", help="Download directory (default: ~/.cache/ogham/bge-m3-onnx)"
    ),
):
    """Download ONNX model files for local embedding."""
    import os
    import shutil
    import tempfile
    import zipfile
    from pathlib import Path
    from urllib.request import urlretrieve

    from rich.progress import BarColumn, DownloadColumn, Progress, TextColumn, TransferSpeedColumn

    MODELS = {
        "bge-m3": {
            "url": "https://github.com/yuniko-software/bge-m3-onnx/releases/download/1.01/onnx.zip",
            "default_dir": Path.home() / ".cache" / "ogham" / "bge-m3-onnx",
            "expected_files": ["bge_m3_model.onnx", "bge_m3_model.onnx_data"],
        },
    }

    if model not in MODELS:
        console.print(f"[red]Unknown model '{model}'. Available: {', '.join(MODELS)}[/red]")
        raise typer.Exit(1)

    info = MODELS[model]
    dest = Path(path) if path else info["default_dir"]

    # Check if already downloaded
    if all((dest / f).exists() for f in info["expected_files"]):
        console.print(f"[green]Model '{model}' already exists at {dest}[/green]")
        raise typer.Exit(0)

    console.print(f"Downloading [bold]{model}[/bold] to {dest}...")

    with Progress(
        TextColumn("[progress.description]{task.description}"),
        BarColumn(),
        DownloadColumn(),
        TransferSpeedColumn(),
        console=console,
    ) as progress:
        task = progress.add_task("Downloading...", total=None)

        def _reporthook(block_num, block_size, total_size):
            if total_size > 0 and progress.tasks[task].total is None:
                progress.update(task, total=total_size)
            if total_size > 0:
                done = min(block_num * block_size, total_size)
            else:
                done = block_num * block_size
            progress.update(task, completed=done)

        with tempfile.TemporaryDirectory() as tmpdir:
            zip_path = Path(tmpdir) / "model.zip"
            urlretrieve(info["url"], zip_path, reporthook=_reporthook)  # noqa: S310

            progress.update(task, description="Extracting...")
            with zipfile.ZipFile(zip_path) as zf:
                # Reject zip members with path traversal
                for member in zf.namelist():
                    if os.path.isabs(member) or ".." in member.split("/"):
                        console.print(f"[red]Unsafe zip member: {member}[/red]")
                        raise typer.Exit(1)
                zf.extractall(tmpdir)

            # The zip contains files at the top level or in a subdirectory —
            # find the expected files and copy them to dest.
            dest.mkdir(parents=True, exist_ok=True)
            try:
                for expected in info["expected_files"]:
                    candidates = list(Path(tmpdir).rglob(expected))
                    if not candidates:
                        console.print(f"[red]Expected file '{expected}' not found in archive[/red]")
                        raise typer.Exit(1)
                    shutil.copy2(candidates[0], dest / expected)
            except Exception:
                # Clean up partial files on failure
                for f in info["expected_files"]:
                    (dest / f).unlink(missing_ok=True)
                raise

    console.print(f"[green]Model '{model}' downloaded to {dest}[/green]")
    model_file = dest / info["expected_files"][0]
    console.print(f"[dim]Set ONNX_MODEL_PATH={model_file} or use the default path.[/dim]")


@app.command()
def openapi(
    output: str = typer.Option("docs/openapi.json", help="Output file path"),
):
    """Generate OpenAPI spec from MCP tool definitions."""
    from ogham.openapi import write_openapi_spec

    write_openapi_spec(output)
    console.print(f"[green]OpenAPI spec written to {output}[/green]")


def _register_subcommands():
    """Register sub-command groups (lazy to avoid import-time overhead)."""
    from ogham.hooks_cli import hooks_app

    app.add_typer(hooks_app)


_register_subcommands()


def main():
    try:
        app()
    except KeyboardInterrupt:
        pass


if __name__ == "__main__":
    main()
