"""CLI for claude-engram."""

from __future__ import annotations

import argparse
import json
import os
import socket
import subprocess
import sys
from typing import Any

from claude_engram.paths import SOCKET_PATH

# Maximum response size (1 MB) to prevent unbounded memory usage
MAX_RESPONSE_SIZE = 1_048_576


def send_command(request: dict[str, Any]) -> dict[str, Any]:
    """Send command to daemon and return response."""
    if not SOCKET_PATH.exists():
        print("Error: Daemon not running. Start with: claude-engram start", file=sys.stderr)
        sys.exit(1)

    client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    client.settimeout(10)
    try:
        client.connect(str(SOCKET_PATH))
        client.sendall(json.dumps(request).encode() + b"\n")

        response = b""
        while True:
            chunk = client.recv(4096)
            if not chunk:
                break
            response += chunk
            if len(response) > MAX_RESPONSE_SIZE:
                print(
                    f"Error: Response exceeds {MAX_RESPONSE_SIZE} bytes",
                    file=sys.stderr,
                )
                sys.exit(1)
            if b"\n" in response:
                break

        result: dict[str, Any] = json.loads(response.decode().strip())
        return result
    except TimeoutError:
        print("Error: Daemon connection timed out after 10s", file=sys.stderr)
        sys.exit(1)
    finally:
        client.close()


def cmd_start(args: argparse.Namespace) -> None:
    """Start the daemon."""
    from .daemon import is_running

    if pid := is_running():
        print(f"Daemon already running (PID: {pid})")
        return

    # Start daemon in background
    python = sys.executable
    subprocess.Popen(
        [python, "-m", "claude_engram.daemon"],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
        start_new_session=True,
    )
    print("Daemon started")


def cmd_stop(args: argparse.Namespace) -> None:
    """Stop the daemon."""
    from .daemon import is_running

    if pid := is_running():
        os.kill(pid, 15)  # SIGTERM
        print(f"Stopped daemon (PID: {pid})")
    else:
        print("Daemon not running")


def cmd_status(args: argparse.Namespace) -> None:
    """Check daemon status."""
    from .daemon import is_running

    if file_pid := is_running():
        response = send_command({"action": "ping"})
        version = response.get("version", "?")

        # Get actual PID from daemon via health check (file PID may be stale)
        actual_pid = file_pid
        try:
            health_response = send_command({"action": "health_check"})
            if health_response.get("status") == "ok":
                health = health_response.get("health", {})
                if "pid" in health:
                    actual_pid = health["pid"]
        except (SystemExit, Exception):
            pass  # Fall back to file PID

        print(f"Running (PID: {actual_pid}, version: {version})")
    else:
        print("Not running")


def cmd_store(args: argparse.Namespace) -> None:
    """Store a memory."""
    request: dict[str, Any] = {
        "action": "store",
        "content": args.content,
        "category": args.category,
    }
    if getattr(args, "proj_tag", None):
        request["proj_tag"] = args.proj_tag
    response = send_command(request)
    print(f"Stored memory #{response['id']}")


def cmd_search(args: argparse.Namespace) -> None:
    """Search memories."""
    request: dict[str, Any] = {"action": "search", "query": args.query, "limit": args.limit}
    if getattr(args, "proj_tag", None):
        request["proj_tag"] = args.proj_tag
    if getattr(args, "search_mode", "vector") != "vector":
        request["search_mode"] = args.search_mode
    response = send_command(request)

    if args.json_output:
        print(json.dumps(response, indent=2, ensure_ascii=False))
        return

    if not response.get("results"):
        print("No results found")
        return

    for r in response["results"]:
        sim = r.get("similarity", 0) * 100
        content = r["content"] if args.full else r["content"][:100]
        ellipsis = "" if args.full or len(r["content"]) <= 100 else "..."
        print(f"[{r['category']}] ({sim:.0f}%) {content}{ellipsis}")


def cmd_recent(args: argparse.Namespace) -> None:
    """Show recent memories."""
    request: dict[str, Any] = {"action": "recent", "limit": args.limit}
    if getattr(args, "proj_tag", None):
        request["proj_tag"] = args.proj_tag
    response = send_command(request)

    if args.json_output:
        print(json.dumps(response, indent=2, ensure_ascii=False))
        return

    if not response.get("results"):
        print("No memories stored")
        return

    for r in response["results"]:
        content = r["content"] if args.full else r["content"][:100]
        ellipsis = "" if args.full or len(r["content"]) <= 100 else "..."
        print(f"[{r['category']}] {content}{ellipsis}")


def cmd_delete(args: argparse.Namespace) -> None:
    """Delete a memory by ID."""
    response = send_command({"action": "delete", "id": args.id})
    if response.get("deleted"):
        print(f"Deleted memory #{args.id}")
    else:
        print(f"Memory #{args.id} not found")


def cmd_stats(args: argparse.Namespace) -> None:
    """Show memory statistics."""
    response = send_command({"action": "stats"})
    print(f"Total memories: {response['total_memories']}")
    print(f"Database: {response['db_path']}")
    if response.get("categories"):
        print("Categories:")
        for cat, count in response["categories"].items():
            print(f"  {cat}: {count}")


def cmd_consolidate(args: argparse.Namespace) -> None:
    """Consolidate duplicate lesson memories."""
    dry_run = not args.execute
    request: dict[str, Any] = {
        "action": "consolidate",
        "dry_run": dry_run,
        "min_cluster_size": args.min_cluster_size,
    }

    if args.json_output:
        response = send_command(request)
        print(json.dumps(response, indent=2, ensure_ascii=False))
        return

    print(f"{'DRY RUN — no changes will be made' if dry_run else 'EXECUTING consolidation...'}")
    print()

    response = send_command(request)

    if response.get("status") == "error":
        print(f"Error: {response.get('error')}", file=sys.stderr)
        sys.exit(1)

    print(f"Total lessons:      {response.get('total_lessons', 0)}")
    print(f"Clusters found:     {response.get('clusters_found', 0)}")
    print(f"Would consolidate:  {response.get('memories_consolidated', 0)} memories")
    print(f"Token savings:      ~{response.get('tokens_saved', 0)} tokens")
    print(f"Unclustered (noise): {response.get('noise_count', 0)}")
    print()

    clusters = response.get("clusters", [])
    for cluster in clusters:
        rep_id = cluster.get("representative_id", "?")
        members = cluster.get("member_ids", [])
        superseded = cluster.get("superseded_ids", [])
        savings = cluster.get("token_savings", 0)

        print(f"Cluster #{cluster.get('cluster_id', '?')} ({len(members)} members):")
        print(f"  Representative: #{rep_id}")
        print(f"    {cluster.get('representative_preview', '')[:100]}")
        for sid in superseded:
            action = "Superseded" if not dry_run else "Would supersede"
            print(f"  {action}: #{sid}")
        print(f"  Token savings: ~{savings}")
        print()

    if dry_run and clusters:
        print("Run with --execute to apply consolidation.")


def cmd_health(args: argparse.Namespace) -> None:
    """Show daemon health report."""
    response = send_command({"action": "health_check"})

    if args.json_output:
        print(json.dumps(response, indent=2, ensure_ascii=False))
        return

    if response.get("status") == "error":
        print(f"Health check failed: {response.get('error')}")
        return

    health = response.get("health", {})

    # Format database size
    db_size = health.get("db_size_bytes", 0)
    db_size_mb = db_size / (1024 * 1024)

    # Format WAL size
    wal_size = health.get("wal_size_bytes", 0)
    wal_size_kb = wal_size / 1024

    # Socket status
    socket_status = "active" if health.get("socket_exists") else "inactive"

    # PID file status
    pid_file_status = "yes" if health.get("pid_file_exists") else "no"

    # Embedding status
    embedding_status = "loaded" if health.get("embedding_model_loaded") else "not loaded"

    print("Health Check:")
    print(f"  Database:     {db_size_mb:.1f} MB ({health.get('memory_count', 0)} memories)")
    if health.get("oldest_memory"):
        print(f"  Oldest:       {health['oldest_memory']}")
    if health.get("newest_memory"):
        print(f"  Newest:       {health['newest_memory']}")
    print(f"  WAL:          {wal_size_kb:.1f} KB")
    print(f"  Socket:       {health.get('socket_path', '?')} ({socket_status})")
    print(f"  PID:          {health.get('pid', '?')} (pid file: {pid_file_status})")
    print(f"  Embedding:    {embedding_status}")
    print(f"  Cache:        {health.get('cache_size', 0)} entries")
    print(f"  Checkpoints:  {health.get('checkpoint_sessions', 0)} active sessions")


def main() -> None:
    """CLI entry point."""
    parser = argparse.ArgumentParser(
        prog="claude-engram",
        description="Singleton memory daemon for Claude Code",
    )
    subparsers = parser.add_subparsers(dest="command", required=True)

    # start
    subparsers.add_parser("start", help="Start the daemon")

    # stop
    subparsers.add_parser("stop", help="Stop the daemon")

    # status
    subparsers.add_parser("status", help="Check daemon status")

    # store
    store_parser = subparsers.add_parser("store", help="Store a memory")
    store_parser.add_argument("content", help="Memory content")
    store_parser.add_argument("-c", "--category", default="general", help="Category")
    store_parser.add_argument("--proj-tag", "-p", help="Project tag")

    # search
    search_parser = subparsers.add_parser("search", help="Search memories")
    search_parser.add_argument("query", help="Search query")
    search_parser.add_argument("-n", "--limit", type=int, default=5, help="Max results")
    search_parser.add_argument("--proj-tag", "-p", help="Project tag")
    search_parser.add_argument("--full", action="store_true", help="Show full content")
    search_parser.add_argument(
        "--json", dest="json_output", action="store_true", help="Output raw JSON"
    )
    search_parser.add_argument(
        "-m",
        "--search-mode",
        default="vector",
        choices=["vector", "hybrid", "keyword"],
        help="Search strategy: vector (default), hybrid (keyword+semantic), keyword (BM25 only)",
    )

    # recent
    recent_parser = subparsers.add_parser("recent", help="Show recent memories")
    recent_parser.add_argument("-n", "--limit", type=int, default=10, help="Max results")
    recent_parser.add_argument("--proj-tag", "-p", help="Project tag")
    recent_parser.add_argument("--full", action="store_true", help="Show full content")
    recent_parser.add_argument(
        "--json", dest="json_output", action="store_true", help="Output raw JSON"
    )

    # delete
    delete_parser = subparsers.add_parser("delete", help="Delete a memory by ID")
    delete_parser.add_argument("id", type=int, help="Memory ID to delete")

    # stats
    subparsers.add_parser("stats", help="Show statistics")

    # consolidate
    consolidate_parser = subparsers.add_parser(
        "consolidate", help="Consolidate duplicate lesson memories"
    )
    consolidate_parser.add_argument(
        "--execute", action="store_true", help="Actually perform consolidation (default: dry-run)"
    )
    consolidate_parser.add_argument(
        "--min-cluster-size",
        type=int,
        default=3,
        help="Minimum lessons to form a cluster (default: 3)",
    )
    consolidate_parser.add_argument(
        "--json", dest="json_output", action="store_true", help="Output raw JSON"
    )

    # health
    health_parser = subparsers.add_parser("health", help="Show health check")
    health_parser.add_argument(
        "--json", dest="json_output", action="store_true", help="Output raw JSON"
    )

    args = parser.parse_args()

    commands = {
        "start": cmd_start,
        "stop": cmd_stop,
        "status": cmd_status,
        "store": cmd_store,
        "search": cmd_search,
        "recent": cmd_recent,
        "delete": cmd_delete,
        "stats": cmd_stats,
        "consolidate": cmd_consolidate,
        "health": cmd_health,
    }

    commands[args.command](args)


if __name__ == "__main__":
    main()
