"""
Mnemo Cortex CLI
================
The command-line interface for managing Mnemo Cortex.

  mnemo-cortex init      → Interactive setup wizard
  mnemo-cortex start     → Start the server
  mnemo-cortex stop      → Stop the server
  mnemo-cortex status    → Check health + session stats
  mnemo-cortex watch     → Auto-capture sessions TO Mnemo
  mnemo-cortex refresh   → Write Mnemo context to workspace (FROM Mnemo)
  mnemo-cortex recall    → Exact-match memory search (SQLite FTS5)
  mnemo-cortex logs      → Tail the server logs
  mnemo-cortex test      → Quick connectivity test

https://github.com/GuyMannDude/mnemo-cortex
"""

import os
import sys
import json
import signal
import subprocess
import time
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 Prompt, Confirm
from rich import print as rprint

console = Console()

CONFIG_DIR = Path.home() / ".config" / "agentb"
CONFIG_FILE = CONFIG_DIR / "agentb.yaml"
DATA_DIR = Path.home() / ".agentb"
PID_FILE = DATA_DIR / "mnemo.pid"
LOG_FILE = DATA_DIR / "logs" / "mnemo.log"

BANNER = """[bold yellow]
  ⚡ Mnemo Cortex
[/bold yellow][dim]  "I remember everything so your agent doesn't have to."[/dim]
"""

# ─────────────────────────────────────────────
#  Main CLI Group
# ─────────────────────────────────────────────

from agentb.doctor import doctor
from agentb.health import health

@click.group(invoke_without_command=True)
@click.pass_context
@click.version_option(version="2.9.0", prog_name="mnemo-cortex")
def main(ctx):
    """⚡ Mnemo Cortex — Drop-in memory superhero for AI agents."""
    if ctx.invoked_subcommand is None:
        console.print(BANNER)
        console.print("  Commands: [bold]init[/] · [bold]start[/] · [bold]stop[/] · [bold]health[/] · [bold]doctor[/] · [bold]watch[/] · [bold]refresh[/] · [bold]recall[/] · [bold]logs[/] · [bold]test[/]")
        console.print()
        if not CONFIG_FILE.exists():
            console.print("  [yellow]→ Run [bold]mnemo-cortex init[/bold] to get started![/]")
        else:
            console.print(f"  Config: {CONFIG_FILE}")
            console.print(f"  Data:   {DATA_DIR}")
        console.print()


# ─────────────────────────────────────────────
#  Init — Interactive Setup Wizard
# ─────────────────────────────────────────────

@main.command()
def init():
    """Interactive setup wizard. Gets you running in 2 minutes."""
    console.print(BANNER)
    console.print(Panel(
        "[bold]Welcome to Mnemo Cortex setup![/]\n\n"
        "This wizard will configure your memory coprocessor.\n"
        "You'll pick a reasoning provider, embedding provider, and we'll test the connection.",
        title="⚡ Setup Wizard",
        border_style="yellow",
    ))
    console.print()

    # Step 1: Reasoning provider
    console.print("[bold cyan]Step 1/4: Reasoning Provider[/]")
    console.print("This model handles preflight checks (validating your agent's responses).\n")

    reasoning_choice = Prompt.ask(
        "Provider",
        choices=["ollama", "openai", "anthropic", "openrouter", "google"],
        default="ollama",
    )

    reasoning_config = _configure_provider(reasoning_choice, "reasoning")

    # Step 2: Embedding provider
    console.print()
    console.print("[bold cyan]Step 2/4: Embedding Provider[/]")
    console.print("This model powers semantic memory search.\n")

    embedding_choice = Prompt.ask(
        "Provider",
        choices=["ollama", "openai", "huggingface", "google", "openrouter"],
        default="ollama" if reasoning_choice == "ollama" else "openai",
    )

    embedding_config = _configure_provider(embedding_choice, "embedding")

    # Step 3: Server settings
    console.print()
    console.print("[bold cyan]Step 3/4: Server Settings[/]")
    console.print(
        "[dim]The server defaults to loopback (127.0.0.1) — only this machine can reach it.\n"
        "Bind to 0.0.0.0 only if you need other machines on your network to connect,\n"
        "and set an auth token in that case.[/dim]"
    )
    host = Prompt.ask("Bind host", default="127.0.0.1")
    port = Prompt.ask("Port", default="50001")

    # Auth: blank is OK on loopback, REQUIRED off-loopback.
    is_loopback = host in ("127.0.0.1", "localhost", "::1")
    auth_prompt = (
        "API auth token (blank OK for loopback)" if is_loopback
        else "API auth token (REQUIRED — server is bound off-loopback)"
    )
    while True:
        auth = Prompt.ask(auth_prompt, default="")
        if is_loopback or auth.strip():
            break
        console.print(
            "[red]Auth token is required when host is not loopback. "
            "Set one, or change host back to 127.0.0.1.[/red]"
        )

    # CORS: locked-down list when loopback, broader (but not wildcard) off-loopback.
    if is_loopback:
        cors_list = '["http://127.0.0.1", "http://localhost"]'
    else:
        cors_list = f'["http://{host}", "http://localhost"]'

    # Step 4: Agent setup
    console.print()
    console.print("[bold cyan]Step 4/4: Agent Setup[/]")
    console.print("Name your agents so their memories stay isolated.\n")

    agents = {}
    while True:
        agent_name = Prompt.ask("Agent name (or 'done' to finish)", default="done")
        if agent_name.lower() == "done":
            break
        persona = Prompt.ask(
            f"  Persona for {agent_name}",
            choices=["default", "strict", "creative"],
            default="default",
        )
        agents[agent_name] = {"persona": persona}

    # Build config YAML
    yaml_lines = [
        "# Mnemo Cortex Configuration",
        f"# Generated by mnemo-cortex init on {time.strftime('%Y-%m-%d %H:%M')}",
        "",
        f"data_dir: {DATA_DIR}",
        "log_level: info",
        "",
        "# Reasoning provider (preflight checks)",
        "reasoning:",
        f"  provider: {reasoning_config['provider']}",
        f"  model: {reasoning_config['model']}",
    ]
    if reasoning_config.get("api_key"):
        yaml_lines.append(f"  api_key: {reasoning_config['api_key']}")
    if reasoning_config.get("api_base"):
        yaml_lines.append(f"  api_base: {reasoning_config['api_base']}")
    yaml_lines.append(f"  timeout: {reasoning_config.get('timeout', 30)}")

    yaml_lines.extend([
        "",
        "# Embedding provider (semantic search)",
        "embedding:",
        f"  provider: {embedding_config['provider']}",
        f"  model: {embedding_config['model']}",
    ])
    if embedding_config.get("api_key"):
        yaml_lines.append(f"  api_key: {embedding_config['api_key']}")
    if embedding_config.get("api_base"):
        yaml_lines.append(f"  api_base: {embedding_config['api_base']}")

    yaml_lines.extend([
        "",
        "# Cache settings",
        "cache:",
        "  l1_max_bundles: 50",
        "  l1_ttl_seconds: 86400",
        "  l1_similarity_threshold: 0.75",
        "  l2_similarity_threshold: 0.5",
        "  l3_similarity_threshold: 0.4",
        "",
        "# Server",
        "server:",
        f"  host: {host}",
        f"  port: {port}",
        f"  cors_origins: {cors_list}",
    ])
    if auth:
        yaml_lines.append(f"  auth_token: {auth}")

    if agents:
        yaml_lines.extend(["", "# Agents"])
        yaml_lines.append("agents:")
        for name, cfg in agents.items():
            yaml_lines.append(f"  {name}:")
            yaml_lines.append(f"    persona: {cfg['persona']}")

    yaml_content = "\n".join(yaml_lines) + "\n"

    # Write config
    CONFIG_DIR.mkdir(parents=True, exist_ok=True)
    DATA_DIR.mkdir(parents=True, exist_ok=True)
    (DATA_DIR / "logs").mkdir(parents=True, exist_ok=True)

    CONFIG_FILE.write_text(yaml_content)

    console.print()
    console.print(Panel(
        f"[green]Config saved to:[/] {CONFIG_FILE}\n"
        f"[green]Data directory:[/]  {DATA_DIR}\n\n"
        "[bold]Next steps:[/]\n"
        "  [yellow]mnemo-cortex start[/]   — Start the server\n"
        "  [yellow]mnemo-cortex status[/]  — Check everything is working\n"
        "  [yellow]mnemo-cortex test[/]    — Quick connectivity test",
        title="⚡ Setup Complete!",
        border_style="green",
    ))


def _configure_provider(provider: str, role: str) -> dict:
    """Interactive configuration for a specific provider."""
    config = {"provider": provider}

    if provider == "ollama":
        config["api_base"] = Prompt.ask("  Ollama URL", default="http://localhost:11434")
        if role == "reasoning":
            config["model"] = Prompt.ask("  Model", default="qwen2.5:32b-instruct")
        else:
            config["model"] = Prompt.ask("  Model", default="nomic-embed-text")
        config["timeout"] = 30

    elif provider == "openai":
        config["api_key"] = Prompt.ask("  API key (or env var like ${OPENAI_API_KEY})")
        if role == "reasoning":
            config["model"] = Prompt.ask("  Model", default="gpt-4o-mini")
        else:
            config["model"] = Prompt.ask("  Model", default="text-embedding-3-small")

    elif provider == "anthropic":
        config["api_key"] = Prompt.ask("  API key (or env var like ${ANTHROPIC_API_KEY})")
        config["model"] = Prompt.ask("  Model", default="claude-sonnet-4-5-20250929")

    elif provider == "openrouter":
        config["api_key"] = Prompt.ask("  API key (or env var like ${OPENROUTER_API_KEY})")
        if role == "reasoning":
            config["model"] = Prompt.ask("  Model", default="nousresearch/hermes-3-llama-3.1-405b:free")
        else:
            config["model"] = Prompt.ask("  Model", default="thenlper/gte-base")

    elif provider == "google":
        config["api_key"] = Prompt.ask("  API key (or env var like ${GEMINI_API_KEY})")
        if role == "reasoning":
            config["model"] = Prompt.ask("  Model", default="gemini-2.5-flash")
        else:
            config["model"] = Prompt.ask("  Model", default="gemini-embedding-001")

    elif provider == "huggingface":
        config["model"] = Prompt.ask("  Model", default="sentence-transformers/all-MiniLM-L6-v2")
        api_base = Prompt.ask("  Local server URL (blank for HF API)", default="")
        if api_base:
            config["api_base"] = api_base
        else:
            config["api_key"] = Prompt.ask("  HuggingFace API token")

    return config


# ─────────────────────────────────────────────
#  Start
# ─────────────────────────────────────────────

@main.command()
@click.option("--foreground", "-f", is_flag=True, help="Run in foreground (don't daemonize)")
@click.option("--port", "-p", type=int, default=None, help="Override port")
def start(foreground, port):
    """Start the Mnemo Cortex server."""
    console.print(BANNER)

    if not CONFIG_FILE.exists():
        console.print("[red]No config found. Run [bold]mnemo-cortex init[/bold] first![/]")
        sys.exit(1)

    if _is_running():
        console.print("[yellow]Mnemo Cortex is already running.[/]")
        console.print(f"  PID: {_get_pid()}")
        console.print("  Run [bold]mnemo-cortex stop[/] first, or [bold]mnemo-cortex status[/] to check.")
        return

    DATA_DIR.mkdir(parents=True, exist_ok=True)
    (DATA_DIR / "logs").mkdir(parents=True, exist_ok=True)

    env = os.environ.copy()
    env["AGENTB_CONFIG"] = str(CONFIG_FILE)
    if port is not None:
        env["MNEMO_PORT"] = str(port)

    cmd = [sys.executable, "-m", "agentb.server"]

    if foreground:
        console.print("[yellow]Starting in foreground... (Ctrl+C to stop)[/]")
        console.print()
        try:
            subprocess.run(cmd, env=env)
        except KeyboardInterrupt:
            console.print("\n[yellow]Stopped.[/]")
    else:
        log_fh = open(LOG_FILE, "a")
        proc = subprocess.Popen(
            cmd, env=env,
            stdout=log_fh, stderr=subprocess.STDOUT,
            start_new_session=True,
        )
        PID_FILE.write_text(str(proc.pid))

        # Wait a moment and check
        time.sleep(2)
        if proc.poll() is None:
            console.print(f"[green]⚡ Mnemo Cortex started![/]")
            console.print(f"  PID:  {proc.pid}")
            console.print(f"  Log:  {LOG_FILE}")
            console.print(f"  URL:  http://localhost:{port or 50001}")

            # Auto-capture gate: start watcher automatically if enabled
            if os.environ.get("MNEMO_AUTO_CAPTURE", "").lower() in ("true", "1", "yes"):
                if not _is_watcher_running():
                    console.print()
                    console.print("  [green]MNEMO_AUTO_CAPTURE=true → starting session watcher...[/]")
                    ctx = click.Context(watch)
                    ctx.invoke(watch, backfill=False, backfill_count=10, foreground=False)
                else:
                    console.print(f"  Watcher:  [green]already running[/]")

            console.print()
            console.print("  [dim]mnemo-cortex status  — check health[/]")
            console.print("  [dim]mnemo-cortex logs    — watch logs[/]")
            console.print("  [dim]mnemo-cortex stop    — stop server[/]")
        else:
            console.print("[red]Failed to start. Check logs:[/]")
            console.print(f"  {LOG_FILE}")
            # Show last few lines
            try:
                lines = LOG_FILE.read_text().strip().split("\n")[-5:]
                for line in lines:
                    console.print(f"  [dim]{line}[/]")
            except Exception:
                pass


# ─────────────────────────────────────────────
#  Stop
# ─────────────────────────────────────────────

@main.command()
def stop():
    """Stop the Mnemo Cortex server."""
    if not _is_running():
        console.print("[yellow]Mnemo Cortex is not running.[/]")
        PID_FILE.unlink(missing_ok=True)
        return

    pid = _get_pid()
    try:
        os.kill(pid, signal.SIGTERM)
        console.print(f"[green]Stopped Mnemo Cortex (PID {pid})[/]")
    except ProcessLookupError:
        console.print("[yellow]Process already gone.[/]")
    PID_FILE.unlink(missing_ok=True)


# ─────────────────────────────────────────────
#  Status
# ─────────────────────────────────────────────

@main.command()
def status():
    """Check Mnemo Cortex health and stats."""
    console.print(BANNER)

    # Process status
    if _is_running():
        console.print(f"  Process: [green]running[/] (PID {_get_pid()})")
    else:
        console.print("  Process: [red]stopped[/]")
        console.print("  Run [bold]mnemo-cortex start[/] to launch.")
        return

    # Health check
    try:
        import httpx
        resp = httpx.get("http://localhost:50001/health", timeout=5.0)
        health = resp.json()

        status_color = {"ok": "green", "degraded": "yellow", "down": "red"}.get(health["status"], "red")
        console.print(f"  Status:  [{status_color}]{health['status']}[/{status_color}]")
        console.print(f"  Version: {health.get('version', 'unknown')}")
        console.print()

        # Reasoning
        r = health.get("reasoning", {})
        r_status = "[green]healthy[/]" if r.get("healthy") else "[red]down[/]"
        r_active = r.get("active", r.get("primary", "unknown"))
        console.print(f"  Reasoning:  {r_status} → {r_active}")
        if r.get("failed_over"):
            console.print(f"              [yellow]⚠ Failed over from {r.get('primary', '?')}[/]")
            if r.get("primary_retry_in"):
                console.print(f"              [dim]Primary retry in {r['primary_retry_in']}[/]")

        # Embedding
        e = health.get("embedding", {})
        e_status = "[green]healthy[/]" if e.get("healthy") else "[red]down[/]"
        e_active = e.get("active", e.get("primary", "unknown"))
        console.print(f"  Embedding:  {e_status} → {e_active}")

        # Sessions
        sessions = health.get("sessions", {})
        if sessions:
            console.print()
            console.print(f"  Sessions:   [cyan]{sessions.get('hot', 0)}[/] hot · "
                         f"[blue]{sessions.get('warm', 0)}[/] warm · "
                         f"[dim]{sessions.get('cold', 0)}[/] cold")

        # Agents
        agents = health.get("agents_configured", [])
        if agents:
            console.print(f"  Agents:     {', '.join(agents)}")

        # Watcher
        if _is_watcher_running():
            console.print(f"  Watcher:    [green]running[/] (PID {_get_watcher_pid()}) — auto-capturing sessions")
        else:
            console.print(f"  Watcher:    [yellow]stopped[/] — run [bold]mnemo-cortex watch[/] to auto-capture")

        # Refresh daemon
        if _is_refresh_running():
            console.print(f"  Refresh:    [green]running[/] (PID {_get_refresh_pid()}) — writing context to workspace")
        else:
            console.print(f"  Refresh:    [yellow]stopped[/] — run [bold]mnemo-cortex refresh --watch[/] to auto-inject")

        console.print()

    except Exception as e:
        console.print(f"  [red]Cannot reach server: {e}[/]")
        console.print("  Is it running? Check [bold]mnemo-cortex logs[/]")


# ─────────────────────────────────────────────
#  Logs
# ─────────────────────────────────────────────

@main.command()
@click.option("--lines", "-n", default=50, help="Number of lines to show")
@click.option("--follow", "-f", is_flag=True, help="Follow log output")
def logs(lines, follow):
    """View Mnemo Cortex server logs."""
    if not LOG_FILE.exists():
        console.print("[yellow]No logs yet. Start the server first.[/]")
        return

    if follow:
        try:
            subprocess.run(["tail", "-f", "-n", str(lines), str(LOG_FILE)])
        except KeyboardInterrupt:
            pass
    else:
        try:
            output = subprocess.run(
                ["tail", "-n", str(lines), str(LOG_FILE)],
                capture_output=True, text=True,
            )
            console.print(output.stdout)
        except Exception as e:
            console.print(f"[red]Error reading logs: {e}[/]")


# ─────────────────────────────────────────────
#  Test
# ─────────────────────────────────────────────

@main.command()
@click.option("--agent", "-a", default=None, help="Agent ID to test with")
def test(agent):
    """Quick connectivity and functionality test."""
    console.print(BANNER)
    console.print("[bold]Running tests...[/]\n")

    base = "http://localhost:50001"
    passed = 0
    failed = 0

    def _test(name, fn):
        nonlocal passed, failed
        try:
            result = fn()
            console.print(f"  [green]✓[/] {name}: {result}")
            passed += 1
        except Exception as e:
            console.print(f"  [red]✗[/] {name}: {e}")
            failed += 1

    import httpx

    # Health
    _test("Health check", lambda: httpx.get(f"{base}/health", timeout=5).json()["status"])

    # Ingest
    _test("Ingest (live wire)", lambda: httpx.post(
        f"{base}/ingest",
        json={"prompt": "Test prompt from CLI", "response": "Test response", "agent_id": agent},
        timeout=5,
    ).json()["status"])

    # Context
    _test("Context search", lambda: f"{httpx.post(f'{base}/context', json={'prompt': 'test', 'agent_id': agent}, timeout=10).json()['total_found']} chunks found")

    # Sessions
    _test("Session listing", lambda: f"{len(httpx.get(f'{base}/sessions', params={'agent_id': agent} if agent else {}, timeout=5).json().get('hot', []))} hot sessions")

    console.print()
    if failed == 0:
        console.print(f"  [green bold]All {passed} tests passed! ⚡[/]")
    else:
        console.print(f"  [yellow]{passed} passed, {failed} failed[/]")


# ─────────────────────────────────────────────
#  Watch — Session Watcher
# ─────────────────────────────────────────────

WATCHER_PID_FILE = DATA_DIR / "watcher.pid"

@main.command()
@click.option("--backfill", "-b", is_flag=True, help="Backfill existing sessions first")
@click.option("--backfill-count", default=10, help="Number of recent sessions to backfill")
@click.option("--foreground", "-f", is_flag=True, help="Run in foreground")
def watch(backfill, backfill_count, foreground):
    """Start the session watcher (auto-captures OpenClaw conversations)."""
    console.print(BANNER)

    if _is_watcher_running():
        console.print("[yellow]Watcher is already running.[/]")
        return

    if backfill:
        console.print("[cyan]Backfilling existing sessions...[/]")
        from agentb.watcher import backfill_sessions
        backfill_sessions(backfill_count)
        console.print()

    env = os.environ.copy()
    env.setdefault("MNEMO_URL", "http://localhost:50001")
    env.setdefault("MNEMO_AGENT_ID", "rocky")

    watcher_script = Path(__file__).parent / "watcher.py"
    cmd = [sys.executable, str(watcher_script)]

    if foreground:
        console.print("[yellow]Watcher running in foreground... (Ctrl+C to stop)[/]")
        try:
            subprocess.run(cmd, env=env)
        except KeyboardInterrupt:
            console.print("\n[yellow]Watcher stopped.[/]")
    else:
        log_file = DATA_DIR / "logs" / "watcher.log"
        (DATA_DIR / "logs").mkdir(parents=True, exist_ok=True)
        log_fh = open(log_file, "a")
        proc = subprocess.Popen(
            cmd, env=env,
            stdout=log_fh, stderr=subprocess.STDOUT,
            start_new_session=True,
        )
        WATCHER_PID_FILE.write_text(str(proc.pid))

        time.sleep(1)
        if proc.poll() is None:
            console.print(f"[green]⚡ Session watcher started![/]")
            console.print(f"  PID: {proc.pid}")
            console.print(f"  Log: {log_file}")
            console.print(f"  Watching: ~/.openclaw/agents/main/sessions/")
            console.print()
            console.print("  [dim]Every exchange Rocky has is now auto-captured.[/]")
            console.print("  [dim]mnemo-cortex unwatch — stop the watcher[/]")
        else:
            console.print("[red]Watcher failed to start. Check logs.[/]")


@main.command()
def unwatch():
    """Stop the session watcher."""
    if not _is_watcher_running():
        console.print("[yellow]Watcher is not running.[/]")
        WATCHER_PID_FILE.unlink(missing_ok=True)
        return

    pid = _get_watcher_pid()
    try:
        os.kill(pid, signal.SIGTERM)
        console.print(f"[green]Watcher stopped (PID {pid})[/]")
    except ProcessLookupError:
        console.print("[yellow]Watcher process already gone.[/]")
    WATCHER_PID_FILE.unlink(missing_ok=True)


# ─────────────────────────────────────────────
#  Refresh — Write Mnemo context to workspace
# ─────────────────────────────────────────────

REFRESH_PID_FILE = DATA_DIR / "refresh.pid"

@main.command()
@click.option("--workspace", "-w", default=None, help="Path to agent workspace (auto-detects OpenClaw)")
@click.option("--output", "-o", default="MNEMO-CONTEXT.md", help="Output filename")
@click.option("--agent", "-a", default=None, help="Agent ID")
@click.option("--watch", "watch_mode", is_flag=True, help="Keep refreshing every 60 seconds (daemon mode)")
@click.option("--interval", default=60, help="Refresh interval in seconds (with --watch)")
@click.option("--recent", "-n", default=15, help="Number of recent exchanges to include")
@click.option("--foreground", "-f", is_flag=True, help="Run daemon in foreground")
def refresh(workspace, output, agent, watch_mode, interval, recent, foreground):
    """Write Mnemo memory context to your agent's workspace.

    Creates a MNEMO-CONTEXT.md file that your agent reads at boot.
    No hooks required — works with any agent framework that reads workspace files.

    \b
    Examples:
      mnemo-cortex refresh                   # one-time write
      mnemo-cortex refresh --watch           # keep refreshing every 60s
      mnemo-cortex refresh --watch -f        # daemon in foreground
      mnemo-cortex refresh -w /path/to/ws    # custom workspace path
    """
    console.print(BANNER)

    # Auto-detect workspace
    workspace_path = _detect_workspace(workspace)
    if not workspace_path:
        console.print("[red]Cannot find agent workspace.[/]")
        console.print("  Specify with: [bold]mnemo-cortex refresh -w /path/to/workspace[/]")
        return

    # Detect Mnemo URL
    import httpx
    mnemo_url = os.environ.get("MNEMO_URL", "http://localhost:50001")
    try:
        health = httpx.get(f"{mnemo_url}/health", timeout=3).json()
        if health.get("status") not in ("ok", "degraded"):
            console.print(f"[red]Mnemo server unhealthy at {mnemo_url}[/]")
            return
    except Exception:
        console.print(f"[red]Cannot reach Mnemo at {mnemo_url}[/]")
        console.print("  Set MNEMO_URL environment variable or start the server.")
        return

    agent_id = agent or os.environ.get("MNEMO_AGENT_ID", "rocky")
    output_path = workspace_path / output

    if not watch_mode:
        # One-time refresh
        success = _do_refresh(mnemo_url, agent_id, recent, output_path)
        if success:
            console.print(f"[green]⚡ Context written to:[/] {output_path}")
            console.print(f"  Agent: {agent_id}")
            console.print(f"  Exchanges: up to {recent}")
            console.print()
            console.print("  [dim]Your agent will read this file at next boot.[/]")
        else:
            console.print("[yellow]No context available from Mnemo yet.[/]")
        return

    # Daemon mode
    if _is_refresh_running():
        console.print("[yellow]Refresh daemon is already running.[/]")
        return

    if foreground:
        console.print(f"[yellow]Refreshing {output_path} every {interval}s... (Ctrl+C to stop)[/]")
        try:
            while True:
                _do_refresh(mnemo_url, agent_id, recent, output_path)
                console.print(f"  [dim]{time.strftime('%H:%M:%S')} — refreshed[/]")
                time.sleep(interval)
        except KeyboardInterrupt:
            console.print("\n[yellow]Refresh daemon stopped.[/]")
    else:
        # Background daemon
        env = os.environ.copy()
        env["MNEMO_URL"] = mnemo_url
        env["MNEMO_AGENT_ID"] = agent_id

        refresh_script = Path(__file__).parent / "refresher.py"
        cmd = [
            sys.executable, str(refresh_script),
            str(workspace_path), output, str(recent), str(interval),
        ]

        log_file = DATA_DIR / "logs" / "refresh.log"
        (DATA_DIR / "logs").mkdir(parents=True, exist_ok=True)
        log_fh = open(log_file, "a")
        proc = subprocess.Popen(
            cmd, env=env,
            stdout=log_fh, stderr=subprocess.STDOUT,
            start_new_session=True,
        )
        REFRESH_PID_FILE.write_text(str(proc.pid))

        time.sleep(1)
        if proc.poll() is None:
            console.print(f"[green]⚡ Refresh daemon started![/]")
            console.print(f"  PID: {proc.pid}")
            console.print(f"  Output: {output_path}")
            console.print(f"  Interval: every {interval}s")
            console.print(f"  Log: {log_file}")
            console.print()
            console.print("  [dim]mnemo-cortex unrefresh — stop the daemon[/]")
        else:
            console.print("[red]Refresh daemon failed to start. Check logs.[/]")


@main.command()
def unrefresh():
    """Stop the refresh daemon."""
    if not _is_refresh_running():
        console.print("[yellow]Refresh daemon is not running.[/]")
        REFRESH_PID_FILE.unlink(missing_ok=True)
        return

    pid = _get_refresh_pid()
    try:
        os.kill(pid, signal.SIGTERM)
        console.print(f"[green]Refresh daemon stopped (PID {pid})[/]")
    except ProcessLookupError:
        console.print("[yellow]Refresh daemon process already gone.[/]")
    REFRESH_PID_FILE.unlink(missing_ok=True)


def _do_refresh(mnemo_url: str, agent_id: str, recent: int, output_path: Path) -> bool:
    """Fetch context from Mnemo and write to file. Returns True if context was written."""
    import httpx
    context_text = ""

    # Try /sessions/recent first
    try:
        resp = httpx.get(
            f"{mnemo_url}/sessions/recent",
            params={"agent_id": agent_id, "n": recent},
            timeout=5,
        )
        if resp.status_code == 200:
            context_text = resp.json().get("context", "")
    except Exception:
        pass

    # Fallback to /context search
    if not context_text.strip():
        try:
            resp = httpx.post(
                f"{mnemo_url}/context",
                json={"prompt": "recent project status active tasks", "agent_id": agent_id, "max_results": 3},
                timeout=8,
            )
            if resp.status_code == 200:
                chunks = resp.json().get("chunks", [])
                if chunks:
                    context_text = "\n\n---\n\n".join(
                        f"[{c.get('cache_tier', '?')}|{c.get('relevance', '?')}] {c.get('content', '')}"
                        for c in chunks
                    )
        except Exception:
            pass

    if not context_text.strip():
        return False

    # Write the file
    header = (
        "# ⚡ Mnemo Cortex — Memory Context\n"
        f"_Auto-refreshed at {time.strftime('%Y-%m-%d %H:%M:%S')}_\n"
        f"_Agent: {agent_id} | Source: {mnemo_url}_\n\n"
    )
    output_path.write_text(header + context_text + "\n")
    return True


def _detect_workspace(explicit_path: str = None) -> Path:
    """Detect the agent workspace directory."""
    if explicit_path:
        p = Path(explicit_path)
        if p.exists():
            return p
        return None

    # Try OpenClaw default
    openclaw_ws = Path.home() / ".openclaw" / "workspace"
    if openclaw_ws.exists():
        return openclaw_ws

    # Try Agent Zero
    a0_ws = Path.home() / ".agent-zero" / "workspace"
    if a0_ws.exists():
        return a0_ws

    # Current directory as fallback
    return Path.cwd()


def _get_refresh_pid() -> int:
    try:
        return int(REFRESH_PID_FILE.read_text().strip())
    except Exception:
        return 0


def _is_refresh_running() -> bool:
    pid = _get_refresh_pid()
    if pid == 0:
        return False
    try:
        os.kill(pid, 0)
        return True
    except ProcessLookupError:
        return False
    except PermissionError:
        return True


def _get_watcher_pid() -> int:
    try:
        return int(WATCHER_PID_FILE.read_text().strip())
    except Exception:
        return 0


def _is_watcher_running() -> bool:
    pid = _get_watcher_pid()
    if pid == 0:
        return False
    try:
        os.kill(pid, 0)
        return True
    except ProcessLookupError:
        return False
    except PermissionError:
        return True


# ─────────────────────────────────────────────
#  Helpers
# ─────────────────────────────────────────────

def _get_pid() -> int:
    try:
        return int(PID_FILE.read_text().strip())
    except Exception:
        return 0


def _is_running() -> bool:
    pid = _get_pid()
    if pid == 0:
        return False
    try:
        os.kill(pid, 0)  # signal 0 = check if process exists
        return True
    except ProcessLookupError:
        return False
    except PermissionError:
        return True  # exists but we can't signal it


# ─────────────────────────────────────────────
#  Recall — Exact-match memory (SQLite FTS5)
# ─────────────────────────────────────────────

@main.group()
def recall():
    """Exact-match memory search via SQLite FTS5.

    Complements Mnemo's semantic search with precise keyword and entity recall.

    \b
    Commands:
      mnemo-cortex recall init      — Initialize recall index
      mnemo-cortex recall index     — Rebuild index from markdown files
      mnemo-cortex recall search    — Search memories by keyword
      mnemo-cortex recall remember  — Store a new memory
      mnemo-cortex recall reflect   — Generate entity summary pages
      mnemo-cortex recall pack      — Generate a memory pack for /new recovery
    """
    pass


def _get_workspace(workspace: str = None) -> Path:
    """Resolve workspace path."""
    if workspace:
        return Path(workspace).expanduser().resolve()
    # Auto-detect OpenClaw workspace
    default = Path.home() / ".openclaw" / "workspace"
    if default.exists():
        return default
    return Path.cwd()


@recall.command("init")
@click.option("--workspace", "-w", default=None, help="Path to agent workspace")
def recall_init(workspace):
    """Initialize the recall memory system in your workspace."""
    from agentb.recall.parser import iter_records
    from agentb.recall.store import connect, default_db_path, rebuild_index

    ws = _get_workspace(workspace)

    # Create standard directories
    (ws / "memory").mkdir(parents=True, exist_ok=True)
    (ws / "bank" / "entities").mkdir(parents=True, exist_ok=True)

    # Seed files if they don't exist
    seed_files = {
        ws / "bank" / "world.md": "# World\n\n",
        ws / "bank" / "experience.md": "# Experience\n\n",
        ws / "bank" / "opinions.md": "# Opinions\n\n",
    }
    for path, body in seed_files.items():
        if not path.exists():
            path.write_text(body, encoding="utf-8")

    conn = connect(default_db_path(ws))
    count = rebuild_index(conn, iter_records(ws))
    console.print(f"[green]⚡ Recall initialized![/] Indexed {count} memory records in {ws}")


@recall.command("index")
@click.option("--workspace", "-w", default=None, help="Path to agent workspace")
def recall_index(workspace):
    """Rebuild the recall index from all markdown memory files."""
    from agentb.recall.parser import iter_records
    from agentb.recall.store import connect, default_db_path, rebuild_index

    ws = _get_workspace(workspace)
    conn = connect(default_db_path(ws))
    count = rebuild_index(conn, iter_records(ws))
    console.print(f"[green]Indexed {count} memory records[/] → {default_db_path(ws)}")


@recall.command("search")
@click.argument("query")
@click.option("--workspace", "-w", default=None, help="Path to agent workspace")
@click.option("--limit", "-n", default=8, help="Max results")
@click.option("--since", default=None, help="Filter by date (e.g., '14d' or '2026-03-01')")
@click.option("--entity", "-e", default=None, help="Filter by entity name")
@click.option("--json-output", "--json", "as_json", is_flag=True, help="Output as JSON")
def recall_search(query, workspace, limit, since, entity, as_json):
    """Search memories by keyword. Fast exact-match via SQLite FTS5."""
    import json as json_mod
    from agentb.recall.store import connect, default_db_path, search
    from agentb.recall.utils import parse_since, relpath

    ws = _get_workspace(workspace)
    conn = connect(default_db_path(ws))
    rows = search(conn, query, limit=limit, since=parse_since(since), entity=entity)

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

    if as_json:
        payload = []
        for row in rows:
            payload.append({
                "kind": row["kind"],
                "date": row["date"],
                "title": row["title"],
                "text": row["text"],
                "source": f"{relpath(Path(row['path']), ws)}#L{row['line_start']}-L{row['line_end']}",
                "score": row["score"],
            })
        console.print(json_mod.dumps(payload, indent=2))
        return

    for i, row in enumerate(rows, start=1):
        src = f"{relpath(Path(row['path']), ws)}#L{row['line_start']}-L{row['line_end']}"
        stamp = f"{row['date']} " if row["date"] else ""
        console.print(f"  [cyan][{i}][/] [bold]{row['kind']}[/] {stamp}")
        console.print(f"      {row['text']}")
        console.print(f"      [dim]source: {src}[/]")
        console.print()


@recall.command("remember")
@click.option("--workspace", "-w", default=None, help="Path to agent workspace")
@click.option("--text", "-t", required=True, help="The memory to store")
@click.option("--kind", "-k", default="fact", help="Memory kind (fact, decision, lesson, etc.)")
@click.option("--entity", "-e", multiple=True, help="Associated entities (repeatable)")
@click.option("--confidence", "-c", type=float, default=None, help="Confidence score 0.0-1.0")
@click.option("--date", "-d", "rec_date", default=None, help="Override date (YYYY-MM-DD)")
def recall_remember(workspace, text, kind, entity, confidence, rec_date):
    """Store a new memory fact, decision, or lesson."""
    from datetime import date as date_mod
    from agentb.recall.models import MemoryRecord
    from agentb.recall.store import connect, default_db_path, append_record

    ws = _get_workspace(workspace)
    stamp = rec_date or date_mod.today().isoformat()
    daily_path = ws / "memory" / f"{stamp}.md"
    daily_path.parent.mkdir(parents=True, exist_ok=True)

    if not daily_path.exists():
        daily_path.write_text(f"# {stamp}\n\n## Retain\n", encoding="utf-8")

    # Build the bullet line
    tags = " ".join(f"@{e}" for e in entity)
    conf = f" c={confidence:.2f}" if confidence is not None else ""
    spacer = " " if tags else ""
    with daily_path.open("a", encoding="utf-8") as f:
        f.write(f"- {kind}{conf} {tags}{spacer}{text}\n")

    line_count = len(daily_path.read_text(encoding="utf-8").splitlines())

    rec = MemoryRecord(
        path=daily_path,
        line_start=line_count,
        line_end=line_count,
        kind=kind,
        text=text,
        entities=list(entity),
        confidence=confidence,
        date=stamp,
        title="Retain",
    )

    conn = connect(default_db_path(ws))
    append_record(conn, rec)
    console.print(f"[green]⚡ Remembered:[/] {kind} → {daily_path.name}")


@recall.command("reflect")
@click.option("--workspace", "-w", default=None, help="Path to agent workspace")
def recall_reflect(workspace):
    """Rebuild index and generate per-entity summary pages."""
    from agentb.recall.parser import iter_records
    from agentb.recall.reflect import write_entity_pages
    from agentb.recall.store import connect, default_db_path, rebuild_index

    ws = _get_workspace(workspace)
    conn = connect(default_db_path(ws))
    indexed = rebuild_index(conn, iter_records(ws))
    written = write_entity_pages(conn, ws)
    console.print(f"[green]Reflected {indexed} records → {written} entity pages[/]")


@recall.command("pack")
@click.option("--workspace", "-w", default=None, help="Path to agent workspace")
@click.option("--since", default="14d", help="How far back to include (e.g., '14d', '2026-03-01')")
@click.option("--limit", "-n", default=12, help="Max memories in pack")
def recall_pack(workspace, since, limit):
    """Generate a memory pack for /new recovery.

    Outputs recent memories as a markdown list that can be injected
    into a new session's context.
    """
    from agentb.recall.store import connect, default_db_path, recent_pack
    from agentb.recall.utils import parse_since, relpath

    ws = _get_workspace(workspace)
    conn = connect(default_db_path(ws))
    rows = recent_pack(conn, since=parse_since(since), limit=limit)

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

    console.print("[bold]# Memory Pack[/]\n")
    for row in rows:
        src = f"{relpath(Path(row['path']), ws)}#L{row['line_start']}-L{row['line_end']}"
        stamp = f"[{row['date']}] " if row["date"] else ""
        console.print(f"- {stamp}{row['kind']}: {row['text']} [dim]_(source: {src})_[/]")


main.add_command(doctor)
main.add_command(health)


# ─────────────────────────────────────────────
#  Dump — Developer Dump (Mnemo v4 Phase 1)
# ─────────────────────────────────────────────

@main.group()
def dump():
    """Inspect Developer Dump files (MCP bridge tool-call captures).

    \b
    Commands:
      mnemo-cortex dump list           — List dump files (size + mtime)
      mnemo-cortex dump tail <agent>   — Live-tail today's dump for an agent

    Dumps are written by the MCP bridge when MNEMO_DUMP=on. Default path:
    ~/.mnemo-cortex/dumps/<agent_id>/<YYYY-MM-DD>.jsonl
    Override with MNEMO_DUMP_DIR.
    """
    pass


def _dump_root() -> Path:
    return Path(os.environ.get(
        "MNEMO_DUMP_DIR",
        str(Path.home() / ".mnemo-cortex" / "dumps"),
    )).expanduser()


@dump.command("list")
def dump_list():
    """List dump files with size + line count + last modified."""
    root = _dump_root()
    if not root.exists():
        console.print(f"[yellow]No dumps yet at {root}.[/]")
        console.print("  Set MNEMO_DUMP=on in your MCP bridge env to start capturing.")
        return

    rows = []
    for agent_dir in sorted(root.iterdir()):
        if not agent_dir.is_dir():
            continue
        for f in sorted(agent_dir.glob("*.jsonl")):
            st = f.stat()
            try:
                with f.open("rb") as fh:
                    lines = sum(1 for _ in fh)
            except OSError:
                lines = -1
            rows.append((agent_dir.name, f.stem, st.st_size, lines, st.st_mtime))

    if not rows:
        console.print(f"[yellow]No dump files under {root}.[/]")
        return

    rows.sort(key=lambda r: r[4], reverse=True)
    table = Table(title=f"Developer Dump — {root}")
    table.add_column("agent", style="cyan")
    table.add_column("date")
    table.add_column("size", justify="right")
    table.add_column("lines", justify="right")
    table.add_column("modified", style="dim")
    for agent, date, size, lines, mtime in rows:
        size_h = f"{size:,} B" if size < 1024 else f"{size / 1024:.1f} KB"
        if size >= 1024 * 1024:
            size_h = f"{size / (1024 * 1024):.1f} MB"
        mtime_h = time.strftime("%Y-%m-%d %H:%M", time.localtime(mtime))
        table.add_row(agent, date, size_h, str(lines), mtime_h)
    console.print(table)


@dump.command("tail")
@click.argument("agent_id")
@click.option("-n", "--lines", default=20, help="Lines to show before tailing")
@click.option("--no-follow", is_flag=True, help="Print and exit instead of following")
def dump_tail(agent_id, lines, no_follow):
    """Live-tail today's dump for AGENT_ID."""
    today = time.strftime("%Y-%m-%d")
    path = _dump_root() / agent_id / f"{today}.jsonl"
    if not path.exists():
        console.print(f"[yellow]No dump file for '{agent_id}' on {today}.[/]")
        console.print(f"  Expected: {path}")
        console.print("  Is MNEMO_DUMP=on in the bridge env? Has the agent made a tool call today?")
        return

    cmd = ["tail", "-n", str(lines)]
    if not no_follow:
        cmd.append("-f")
    cmd.append(str(path))
    try:
        subprocess.run(cmd)
    except KeyboardInterrupt:
        pass


if __name__ == "__main__":
    main()
