#!/home/moltbot/echo-core/.venv/bin/python3
"""Echo Core CLI tool."""

import argparse
import getpass
import json
import os
import shutil
import signal
import sys
from datetime import datetime, timezone
from pathlib import Path

# Add project root to path
PROJECT_ROOT = Path(__file__).resolve().parent
sys.path.insert(0, str(PROJECT_ROOT))

from src.credential_store import set_secret, get_secret, list_secrets, delete_secret, check_secrets

PID_FILE = PROJECT_ROOT / "echo-core.pid"
LOG_FILE = PROJECT_ROOT / "logs" / "echo-core.log"
SESSIONS_FILE = PROJECT_ROOT / "sessions" / "active.json"
CONFIG_FILE = PROJECT_ROOT / "config.json"


# ---------------------------------------------------------------------------
# Subcommand handlers
# ---------------------------------------------------------------------------

SERVICE_NAME = "echo-core.service"
BRIDGE_SERVICE_NAME = "echo-whatsapp-bridge.service"


def _systemctl(*cmd_args) -> tuple[int, str]:
    """Run systemctl --user and return (returncode, stdout)."""
    import subprocess
    result = subprocess.run(
        ["systemctl", "--user", *cmd_args],
        capture_output=True, text=True, timeout=30,
    )
    return result.returncode, result.stdout.strip()


def _get_service_status(service: str) -> dict:
    """Get service ActiveState, SubState, MainPID, and ActiveEnterTimestamp."""
    import subprocess
    result = subprocess.run(
        ["systemctl", "--user", "show", service,
         "--property=ActiveState,SubState,MainPID,ActiveEnterTimestamp"],
        capture_output=True, text=True, timeout=30,
    )
    info = {}
    for line in result.stdout.strip().splitlines():
        if "=" in line:
            k, v = line.split("=", 1)
            info[k] = v
    return info


def cmd_status(args):
    """Show bot status: online/offline, uptime, active sessions."""
    # Echo Core service
    info = _get_service_status(SERVICE_NAME)
    active = info.get("ActiveState", "unknown")
    pid = info.get("MainPID", "0")
    ts = info.get("ActiveEnterTimestamp", "")

    if active == "active":
        # Parse uptime from ActiveEnterTimestamp
        uptime_str = ""
        if ts:
            try:
                started = datetime.strptime(ts.strip(), "%a %Y-%m-%d %H:%M:%S %Z")
                started = started.replace(tzinfo=timezone.utc)
                uptime = datetime.now(timezone.utc) - started
                hours, remainder = divmod(int(uptime.total_seconds()), 3600)
                minutes, seconds = divmod(remainder, 60)
                uptime_str = f"{hours}h {minutes}m {seconds}s"
            except (ValueError, OSError):
                uptime_str = "?"
        print(f"Echo Core: ONLINE (PID {pid})")
        if uptime_str:
            print(f"Uptime:    {uptime_str}")
    else:
        print(f"Echo Core: OFFLINE ({active})")

    # WhatsApp bridge service
    bridge_info = _get_service_status(BRIDGE_SERVICE_NAME)
    bridge_active = bridge_info.get("ActiveState", "unknown")
    bridge_pid = bridge_info.get("MainPID", "0")
    if bridge_active == "active":
        print(f"WA Bridge: ONLINE (PID {bridge_pid})")
    else:
        print(f"WA Bridge: OFFLINE ({bridge_active})")

    _print_session_count()


def _print_session_count():
    """Print the number of active sessions."""
    sessions = _load_sessions_file()
    count = len(sessions)
    print(f"Sessions: {count} active")


def _load_sessions_file() -> dict:
    """Load sessions/active.json, return {} on any error."""
    try:
        text = SESSIONS_FILE.read_text(encoding="utf-8")
        if not text.strip():
            return {}
        return json.loads(text)
    except (FileNotFoundError, json.JSONDecodeError, OSError):
        return {}


def _voice_doctor_checks() -> list[tuple[str, bool]]:
    """Voice-stack health checks (Pas 10).

    Mirrors the logic in tools/voice_setup.py but returns (label, ok) tuples
    so they integrate with cmd_doctor's PASS/FAIL output. All checks degrade
    gracefully — ImportError on optional voice deps is reported as FAIL, never
    raised, so the rest of `eco doctor` is unaffected.
    """
    import importlib.util
    import json as _json
    import urllib.error
    import urllib.request

    results: list[tuple[str, bool]] = []

    # 1. libopus0 loaded by discord.py
    try:
        import discord
        if not discord.opus.is_loaded():
            try:
                discord.opus._load_default()
            except Exception:
                pass
        results.append(("libopus loaded (discord.py)", discord.opus.is_loaded()))
    except ImportError:
        results.append(("libopus loaded (discord.py)", False))
    except Exception:
        results.append(("libopus loaded (discord.py)", False))

    # 2. ffmpeg in PATH
    results.append(("ffmpeg in PATH", shutil.which("ffmpeg") is not None))

    # 3. Supertonic TTS reachable at http://127.0.0.1:7788/
    supertonic_url = "http://127.0.0.1:7788/v1/audio/speech"
    supertonic_ok = False
    try:
        payload = _json.dumps({
            "model": "supertonic-3",
            "input": "test",
            "voice": "M2",
            "response_format": "wav",
            "lang": "ro",
        }).encode("utf-8")
        req = urllib.request.Request(
            supertonic_url,
            data=payload,
            headers={"Content-Type": "application/json"},
            method="POST",
        )
        with urllib.request.urlopen(req, timeout=5) as resp:
            supertonic_ok = resp.status == 200
    except (urllib.error.URLError, ConnectionError, OSError):
        supertonic_ok = False
    except Exception:
        supertonic_ok = False
    results.append(("Supertonic TTS reachable at :7788", supertonic_ok))

    # 4. faster-whisper importable (don't load model — too slow)
    results.append((
        "faster-whisper importable",
        importlib.util.find_spec("faster_whisper") is not None,
    ))

    # 5. silero-vad importable
    results.append((
        "silero-vad importable",
        importlib.util.find_spec("silero_vad") is not None,
    ))

    # 6. discord.ext.voice_recv importable (vendor package)
    voice_recv_ok = False
    try:
        voice_recv_ok = importlib.util.find_spec("discord.ext.voice_recv") is not None
    except (ImportError, ValueError, ModuleNotFoundError):
        voice_recv_ok = False
    except Exception:
        voice_recv_ok = False
    results.append(("discord.ext.voice_recv importable", voice_recv_ok))

    # 7-9. Voice assets present and non-trivial size
    voice_assets = [
        ("assets/voice/thinking.wav", 1024),
        ("assets/voice/beep_200ms.wav", 512),
        ("assets/voice/mhm.wav", 512),
    ]
    for rel_path, min_bytes in voice_assets:
        path = PROJECT_ROOT / rel_path
        ok = False
        try:
            ok = path.exists() and path.stat().st_size > min_bytes
        except OSError:
            ok = False
        label = f"{rel_path} (>{min_bytes}B)"
        results.append((label, ok))

    return results


def cmd_doctor(args):
    """Run diagnostic checks."""
    import re
    import subprocess

    checks = []

    # 1. Discord token present
    token = get_secret("discord_token")
    checks.append(("Discord token in keyring", bool(token)))

    # 2. Keyring working
    try:
        import keyring
        keyring.get_password("echo-core", "_registry")
        checks.append(("Keyring accessible", True))
    except Exception:
        checks.append(("Keyring accessible", False))

    # 3. Claude CLI found and functional
    claude_found = shutil.which("claude") is not None
    checks.append(("Claude CLI found", claude_found))
    if claude_found:
        try:
            result = subprocess.run(
                ["claude", "--version"], capture_output=True, text=True, timeout=10,
            )
            checks.append(("Claude CLI functional", result.returncode == 0))
        except Exception:
            checks.append(("Claude CLI functional", False))

    # 4. Disk space (warn if <1GB free)
    try:
        stat = os.statvfs(str(PROJECT_ROOT))
        free_gb = (stat.f_bavail * stat.f_frsize) / (1024 ** 3)
        checks.append((f"Disk space ({free_gb:.1f} GB free)", free_gb >= 1.0))
    except OSError:
        checks.append(("Disk space", False))

    # 5. config.json valid + no tokens/secrets in plain text
    try:
        with open(CONFIG_FILE, "r", encoding="utf-8") as f:
            config_text = f.read()
        json.loads(config_text)
        checks.append(("config.json valid", True))
        # Scan for token-like patterns
        token_patterns = re.compile(
            r'(sk-[a-zA-Z0-9]{20,}|xoxb-|xoxp-|ghp_|gho_|discord.*token.*["\']:\s*["\'][A-Za-z0-9._-]{20,})',
            re.IGNORECASE,
        )
        has_tokens = bool(token_patterns.search(config_text))
        checks.append(("config.json no plain text secrets", not has_tokens))
    except (FileNotFoundError, json.JSONDecodeError, OSError):
        checks.append(("config.json valid", False))

    # 6. Logs dir writable
    logs_dir = PROJECT_ROOT / "logs"
    try:
        logs_dir.mkdir(parents=True, exist_ok=True)
        test_file = logs_dir / ".write_test"
        test_file.write_text("ok")
        test_file.unlink()
        checks.append(("Logs dir writable", True))
    except OSError:
        checks.append(("Logs dir writable", False))

    # 7. .gitignore correct (must contain key entries)
    gitignore = PROJECT_ROOT / ".gitignore"
    required_gitignore = {"sessions/", "logs/", ".env", "*.sqlite"}
    try:
        gi_text = gitignore.read_text(encoding="utf-8")
        gi_lines = {l.strip() for l in gi_text.splitlines()}
        missing = required_gitignore - gi_lines
        checks.append((".gitignore complete", len(missing) == 0))
        if missing:
            print(f"  (missing from .gitignore: {', '.join(sorted(missing))})")
    except FileNotFoundError:
        checks.append((".gitignore exists", False))

    # 8. File permissions: sessions/ and config.json not world-readable
    for sensitive in [PROJECT_ROOT / "sessions", CONFIG_FILE]:
        if sensitive.exists():
            mode = sensitive.stat().st_mode
            world_read = mode & 0o004
            checks.append((f"{sensitive.name} not world-readable", not world_read))

    # 9. Ollama reachable
    try:
        import urllib.request
        req = urllib.request.urlopen("http://10.0.20.161:11434/api/tags", timeout=5)
        checks.append(("Ollama reachable", req.status == 200))
    except Exception:
        checks.append(("Ollama reachable", False))

    # 10. Telegram token (optional)
    tg_token = get_secret("telegram_token")
    if tg_token:
        checks.append(("Telegram token in keyring", True))
    else:
        checks.append(("Telegram token (optional)", True))  # not required

    # 11. Echo Core service running
    info = _get_service_status(SERVICE_NAME)
    checks.append(("Echo Core service running", info.get("ActiveState") == "active"))

    # 12. WhatsApp bridge service running (optional)
    bridge_info = _get_service_status(BRIDGE_SERVICE_NAME)
    bridge_active = bridge_info.get("ActiveState") == "active"
    if bridge_active:
        checks.append(("WhatsApp bridge running", True))
    else:
        checks.append(("WhatsApp bridge (optional)", True))

    # ---- Voice stack checks (Pas 10) ----
    checks.extend(_voice_doctor_checks())

    # Print results
    all_pass = True
    for label, passed in checks:
        status = "PASS" if passed else "FAIL"
        print(f"  [{status}] {label}")
        if not passed:
            all_pass = False

    print()
    if all_pass:
        print("All checks passed.")
    else:
        print("Some checks failed!")
        sys.exit(1)


def cmd_restart(args):
    """Restart the bot via systemctl (kill + start)."""
    import time

    # Also restart bridge if requested
    if getattr(args, "bridge", False):
        print("Restarting WhatsApp bridge...")
        _systemctl("kill", BRIDGE_SERVICE_NAME)
        time.sleep(2)
        _systemctl("start", BRIDGE_SERVICE_NAME)

    print("Restarting Echo Core...")
    _systemctl("restart", SERVICE_NAME)
    time.sleep(3)

    info = _get_service_status(SERVICE_NAME)
    if info.get("ActiveState") == "active":
        print(f"Echo Core restarted (PID {info.get('MainPID')}).")
    elif info.get("ActiveState") == "activating":
        print("Echo Core starting...")
    else:
        print(f"Warning: Echo Core status is {info.get('ActiveState')}")
        sys.exit(1)


def cmd_stop(args):
    """Stop the bot via systemctl."""
    print("Stopping Echo Core...")
    _systemctl("stop", "--no-block", SERVICE_NAME)
    import time
    time.sleep(2)
    info = _get_service_status(SERVICE_NAME)
    if info.get("ActiveState") in ("inactive", "deactivating"):
        print("Echo Core stopped.")
    else:
        print(f"Echo Core status: {info.get('ActiveState')}")


def cmd_logs(args):
    """Show last N lines from the log file."""
    n = args.lines
    if not LOG_FILE.exists():
        print(f"No log file found at {LOG_FILE}")
        return

    try:
        lines = LOG_FILE.read_text(encoding="utf-8", errors="replace").splitlines()
    except OSError as e:
        print(f"Error reading log file: {e}")
        sys.exit(1)

    tail = lines[-n:]
    for line in tail:
        print(line)


def cmd_sessions(args):
    """Handle sessions subcommand."""
    if args.sessions_action == "list":
        _sessions_list()
    elif args.sessions_action == "clear":
        _sessions_clear(args.channel)


def _sessions_list():
    """List active sessions in tabular format."""
    sessions = _load_sessions_file()
    if not sessions:
        print("No active sessions.")
        return

    # Header
    print(f"{'Channel':<22} {'Model':<8} {'Messages':>8}  {'Last message'}")
    print(f"{'-'*22} {'-'*8} {'-'*8}  {'-'*20}")

    for channel_id, info in sessions.items():
        model = info.get("model", "?")
        count = info.get("message_count", 0)
        last = info.get("last_message_at", "?")
        # Truncate ISO timestamp to readable form
        if last != "?" and len(last) > 19:
            last = last[:19].replace("T", " ")
        print(f"{channel_id:<22} {model:<8} {count:>8}  {last}")


def _sessions_clear(channel: str | None):
    """Clear one or all sessions."""
    from src.claude_session import clear_session, list_sessions as ls_sessions

    if channel:
        if clear_session(channel):
            print(f"Session for '{channel}' cleared.")
        else:
            print(f"No session found for '{channel}'.")
    else:
        sessions = ls_sessions()
        if not sessions:
            print("No active sessions.")
            return
        count = len(sessions)
        for ch in list(sessions.keys()):
            clear_session(ch)
        print(f"Cleared {count} session(s).")


def cmd_channel(args):
    """Handle channel subcommand."""
    if args.channel_action == "add":
        _channel_add(args.id, args.alias)
    elif args.channel_action == "list":
        _channel_list()


def _channel_add(channel_id: str, alias: str):
    """Add a channel to config.json."""
    from src.config import Config
    cfg = Config()
    channels = cfg.get("channels", {})
    if alias in channels:
        print(f"Error: alias '{alias}' already exists")
        sys.exit(1)
    channels[alias] = {"id": channel_id}
    cfg.set("channels", channels)
    cfg.save()
    print(f"Channel '{alias}' (ID: {channel_id}) added.")


def _channel_list():
    """List registered channels."""
    from src.config import Config
    cfg = Config()
    channels = cfg.get("channels", {})
    if not channels:
        print("No channels registered.")
        return

    print(f"{'Alias':<20} {'Channel ID'}")
    print(f"{'-'*20} {'-'*22}")
    for alias, info in channels.items():
        ch_id = info.get("id", "?") if isinstance(info, dict) else str(info)
        print(f"{alias:<20} {ch_id}")


def cmd_send(args):
    """Send a message through the router."""
    from src.config import Config
    from src.router import route_message

    cfg = Config()
    channels = cfg.get("channels", {})
    channel_info = channels.get(args.alias)
    if not channel_info:
        print(f"Error: unknown channel alias '{args.alias}'")
        print(f"Available: {', '.join(channels.keys()) if channels else '(none)'}")
        sys.exit(1)

    channel_id = channel_info.get("id") if isinstance(channel_info, dict) else str(channel_info)
    message = " ".join(args.message)

    print(f"Sending to '{args.alias}' ({channel_id})...")
    response, is_cmd = route_message(channel_id, "cli-user", message)
    print(response)


def cmd_cron(args):
    """Handle cron subcommand."""
    if args.cron_action == "list":
        _cron_list()
    elif args.cron_action == "run":
        _cron_run(args.name)
    elif args.cron_action == "add":
        tools = [t.strip() for t in args.tools.split(",")] if args.tools else []
        if args.prompt == "-":
            prompt = sys.stdin.read().strip()
        else:
            prompt = args.prompt
        _cron_add(args.name, args.expression, args.channel, prompt,
                  args.model, tools)
    elif args.cron_action == "remove":
        _cron_remove(args.name)
    elif args.cron_action == "enable":
        _cron_enable(args.name)
    elif args.cron_action == "disable":
        _cron_disable(args.name)


def _cron_list():
    """List scheduled jobs in tabular format."""
    from src.scheduler import Scheduler
    s = Scheduler()
    s._jobs = s._load_jobs()
    jobs = s.list_jobs()
    if not jobs:
        print("No scheduled jobs.")
        return

    print(f"{'Name':<24} {'Cron':<16} {'Channel':<10} {'Model':<8} {'Enabled':<8} {'Last Status':<12} {'Next Run'}")
    print(f"{'-'*24} {'-'*16} {'-'*10} {'-'*8} {'-'*8} {'-'*12} {'-'*20}")

    for job in jobs:
        name = job.get("name", "?")
        cron = job.get("cron", "?")
        channel = job.get("channel", "?")
        model = job.get("model", "?")
        enabled = "yes" if job.get("enabled") else "no"
        last_status = job.get("last_status") or "-"
        next_run = job.get("next_run") or "-"
        if next_run != "-" and len(next_run) > 19:
            next_run = next_run[:19].replace("T", " ")
        print(f"{name:<24} {cron:<16} {channel:<10} {model:<8} {enabled:<8} {last_status:<12} {next_run}")


def _cron_run(name: str):
    """Force-run a job and print output."""
    import asyncio
    from src.scheduler import Scheduler

    async def _run():
        s = Scheduler()
        s._jobs = s._load_jobs()
        return await s.run_job(name)

    try:
        result = asyncio.run(_run())
        print(result)
    except KeyError as exc:
        print(f"Error: {exc}")
        sys.exit(1)


def _cron_add(name: str, cron: str, channel: str, prompt: str,
              model: str, tools: list[str]):
    """Add a new cron job."""
    from src.scheduler import Scheduler
    s = Scheduler()
    s._jobs = s._load_jobs()
    try:
        job = s.add_job(name, cron, channel, prompt, model, tools or None)
        print(f"Job '{job['name']}' added (cron: {job['cron']}, channel: {job['channel']}, model: {job['model']})")
    except ValueError as exc:
        print(f"Error: {exc}")
        sys.exit(1)


def _cron_remove(name: str):
    """Remove a cron job."""
    from src.scheduler import Scheduler
    s = Scheduler()
    s._jobs = s._load_jobs()
    if s.remove_job(name):
        print(f"Job '{name}' removed.")
    else:
        print(f"Job '{name}' not found.")


def _cron_enable(name: str):
    """Enable a cron job."""
    from src.scheduler import Scheduler
    s = Scheduler()
    s._jobs = s._load_jobs()
    if s.enable_job(name):
        print(f"Job '{name}' enabled.")
    else:
        print(f"Job '{name}' not found.")


def _cron_disable(name: str):
    """Disable a cron job."""
    from src.scheduler import Scheduler
    s = Scheduler()
    s._jobs = s._load_jobs()
    if s.disable_job(name):
        print(f"Job '{name}' disabled.")
    else:
        print(f"Job '{name}' not found.")


def cmd_memory(args):
    """Handle memory subcommand."""
    if args.memory_action == "search":
        _memory_search(args.query)
    elif args.memory_action == "reindex":
        _memory_reindex()


def _memory_search(query: str):
    """Search memory and print results."""
    from src.memory_search import search

    try:
        results = search(query)
    except ConnectionError as e:
        print(f"Error: {e}")
        sys.exit(1)

    if not results:
        print("No results found (index may be empty — run `echo memory reindex`).")
        return

    for i, r in enumerate(results, 1):
        score = r["score"]
        print(f"\n--- Result {i} (score: {score:.3f}) ---")
        print(f"File: {r['file']}")
        preview = r["chunk"][:200]
        if len(r["chunk"]) > 200:
            preview += "..."
        print(preview)


def _memory_reindex():
    """Rebuild memory search index."""
    from src.memory_search import reindex

    print("Reindexing memory files...")
    try:
        stats = reindex()
    except ConnectionError as e:
        print(f"Error: {e}")
        sys.exit(1)

    print(f"Done. Indexed {stats['files']} files, {stats['chunks']} chunks.")


def cmd_heartbeat(args):
    """Run heartbeat health checks."""
    from src.heartbeat import run_heartbeat
    print(run_heartbeat())


def cmd_whatsapp(args):
    """Handle whatsapp subcommand."""
    if args.whatsapp_action == "status":
        _whatsapp_status()
    elif args.whatsapp_action == "qr":
        _whatsapp_qr()


def cmd_openrouter(args):
    """Handle openrouter subcommand."""
    semaphore = PROJECT_ROOT / ".use_openrouter"

    if args.openrouter_action == "on":
        env_file = Path.home() / ".claude-env.sh"
        if not env_file.exists():
            print(f"Error: {env_file} not found")
            sys.exit(1)
        # Verify required vars exist in file
        text = env_file.read_text()
        if "ANTHROPIC_BASE_URL" not in text or "OPENROUTER_API_KEY" not in text:
            print(f"Warning: {env_file} may be missing ANTHROPIC_BASE_URL or OPENROUTER_API_KEY")

        semaphore.write_text("# OpenRouter mode enabled\n")
        print("OpenRouter mode: ENABLED")
        print("Restart echo-core for changes to take effect:")
        print("  systemctl --user restart echo-core")

    elif args.openrouter_action == "off":
        if semaphore.exists():
            semaphore.unlink()
            print("OpenRouter mode: DISABLED")
            print("Restart echo-core to use Anthropic API:")
            print("  systemctl --user restart echo-core")
        else:
            print("OpenRouter mode: already disabled")

    elif args.openrouter_action == "status":
        status = "ENABLED" if semaphore.exists() else "DISABLED"
        print(f"OpenRouter mode: {status}")
        if semaphore.exists():
            print(f"Semafor: {semaphore}")
            env_file = Path.home() / ".claude-env.sh"
            if env_file.exists():
                # Show which vars will be loaded
                print(f"Env file: {env_file}")
                text = env_file.read_text()
                for line in text.splitlines():
                    if line.strip().startswith("export ") and not line.strip().startswith("#"):
                        var_name = line.strip()[7:].split("=")[0] if "=" in line else "?"
                        if any(x in var_name for x in ["ANTHROPIC", "OPENROUTER"]):
                            print(f"  {var_name}=***")


def _whatsapp_status():
    """Check WhatsApp bridge connection status."""
    import urllib.request
    import urllib.error

    cfg_file = CONFIG_FILE
    bridge_url = "http://127.0.0.1:8098"
    try:
        text = cfg_file.read_text(encoding="utf-8")
        cfg = json.loads(text)
        bridge_url = cfg.get("whatsapp", {}).get("bridge_url", bridge_url)
    except (FileNotFoundError, json.JSONDecodeError, OSError):
        pass

    try:
        req = urllib.request.urlopen(f"{bridge_url}/status", timeout=5)
        data = json.loads(req.read().decode())
    except (urllib.error.URLError, OSError) as e:
        print(f"Bridge not reachable at {bridge_url}")
        print(f"  Error: {e}")
        return

    connected = data.get("connected", False)
    phone = data.get("phone", "unknown")
    has_qr = data.get("qr", False)

    if connected:
        print(f"Status:  CONNECTED")
        print(f"Phone:   {phone}")
    elif has_qr:
        print(f"Status:  WAITING FOR QR SCAN")
        print(f"Run 'echo whatsapp qr' for QR code instructions.")
    else:
        print(f"Status:  DISCONNECTED")
        print(f"Start the bridge and scan the QR code to connect.")


def _whatsapp_qr():
    """Show QR code instructions from the bridge."""
    import urllib.request
    import urllib.error

    cfg_file = CONFIG_FILE
    bridge_url = "http://127.0.0.1:8098"
    try:
        text = cfg_file.read_text(encoding="utf-8")
        cfg = json.loads(text)
        bridge_url = cfg.get("whatsapp", {}).get("bridge_url", bridge_url)
    except (FileNotFoundError, json.JSONDecodeError, OSError):
        pass

    try:
        req = urllib.request.urlopen(f"{bridge_url}/qr", timeout=5)
        data = json.loads(req.read().decode())
    except (urllib.error.URLError, OSError) as e:
        print(f"Bridge not reachable at {bridge_url}")
        print(f"  Error: {e}")
        return

    qr = data.get("qr")
    if not qr:
        if data.get("connected"):
            print("Already connected — no QR code needed.")
        else:
            print("No QR code available yet. Wait for the bridge to initialize.")
        return

    print("QR code is available at the bridge.")
    print(f"Open {bridge_url}/qr in a browser to scan,")
    print("or check the bridge terminal output for the QR code.")


def cmd_secrets(args):
    """Handle secrets subcommand."""
    if args.secrets_action == "set":
        if args.file:
            path = Path(args.file)
            if not path.exists():
                print(f"Error: file {args.file} not found")
                sys.exit(1)
            value = path.read_text().strip()
            set_secret(args.name, value)
            path.unlink()  # Delete source file after storing
            print(f"Secret '{args.name}' set from file (file deleted)")
        else:
            value = getpass.getpass(f"Enter value for '{args.name}': ")
            set_secret(args.name, value)
            print(f"Secret '{args.name}' set")

    elif args.secrets_action == "list":
        names = list_secrets()
        if not names:
            print("No secrets stored")
        else:
            for name in names:
                print(f"  - {name}")

    elif args.secrets_action == "delete":
        if delete_secret(args.name):
            print(f"Secret '{args.name}' deleted")
        else:
            print(f"Secret '{args.name}' not found")

    elif args.secrets_action == "test":
        results = check_secrets()
        for name, exists in results.items():
            print(f"  {name}: {'OK' if exists else 'MISSING'}")
        if all(results.values()):
            print("\nAll required secrets present.")
        else:
            print("\nWARNING: Some required secrets are missing!")
            sys.exit(1)


# ---------------------------------------------------------------------------
# Argument parser
# ---------------------------------------------------------------------------

def main():
    parser = argparse.ArgumentParser(prog="echo", description="Echo Core CLI")
    sub = parser.add_subparsers(dest="command")

    # status
    sub.add_parser("status", help="Show bot status")

    # doctor
    sub.add_parser("doctor", help="Run diagnostic checks")

    # restart
    restart_parser = sub.add_parser("restart", help="Restart the bot via systemctl")
    restart_parser.add_argument("--bridge", action="store_true",
                                help="Also restart WhatsApp bridge")

    # stop
    sub.add_parser("stop", help="Stop the bot via systemctl")

    # logs
    logs_parser = sub.add_parser("logs", help="Show recent log lines")
    logs_parser.add_argument("lines", nargs="?", type=int, default=20,
                             help="Number of lines (default: 20)")

    # sessions
    sessions_parser = sub.add_parser("sessions", help="Manage sessions")
    sessions_sub = sessions_parser.add_subparsers(dest="sessions_action")

    sessions_sub.add_parser("list", help="List active sessions")

    sessions_clear_p = sessions_sub.add_parser("clear", help="Clear session(s)")
    sessions_clear_p.add_argument("channel", nargs="?", default=None,
                                  help="Channel ID to clear (omit for all)")

    # channel
    channel_parser = sub.add_parser("channel", help="Manage channels")
    channel_sub = channel_parser.add_subparsers(dest="channel_action")

    channel_add_p = channel_sub.add_parser("add", help="Add a channel")
    channel_add_p.add_argument("--id", required=True, help="Discord channel ID")
    channel_add_p.add_argument("--alias", required=True, help="Channel alias")

    channel_sub.add_parser("list", help="List registered channels")

    # send
    send_parser = sub.add_parser("send", help="Send a message via router")
    send_parser.add_argument("alias", help="Channel alias")
    send_parser.add_argument("message", nargs="+", help="Message text")

    # secrets
    secrets_parser = sub.add_parser("secrets", help="Manage secrets")
    secrets_sub = secrets_parser.add_subparsers(dest="secrets_action")

    set_p = secrets_sub.add_parser("set", help="Set a secret")
    set_p.add_argument("name", help="Secret name")
    set_p.add_argument("--file", help="Read value from file (file deleted after)")

    secrets_sub.add_parser("list", help="List secret names")

    del_p = secrets_sub.add_parser("delete", help="Delete a secret")
    del_p.add_argument("name", help="Secret name")

    secrets_sub.add_parser("test", help="Check required secrets")

    # memory
    memory_parser = sub.add_parser("memory", help="Memory search commands")
    memory_sub = memory_parser.add_subparsers(dest="memory_action")

    memory_search_p = memory_sub.add_parser("search", help="Search memory files")
    memory_search_p.add_argument("query", help="Search query text")

    memory_sub.add_parser("reindex", help="Rebuild memory search index")

    # heartbeat
    sub.add_parser("heartbeat", help="Run heartbeat health checks")

    # cron
    cron_parser = sub.add_parser("cron", help="Manage scheduled jobs")
    cron_sub = cron_parser.add_subparsers(dest="cron_action")

    cron_sub.add_parser("list", help="List scheduled jobs")

    cron_run_p = cron_sub.add_parser("run", help="Force-run a job")
    cron_run_p.add_argument("name", help="Job name")

    cron_add_p = cron_sub.add_parser("add", help="Add a scheduled job")
    cron_add_p.add_argument("name", help="Job name")
    cron_add_p.add_argument("expression", help="Cron expression (e.g. '30 6 * * *')")
    cron_add_p.add_argument("--channel", required=True, help="Channel alias")
    cron_add_p.add_argument("--prompt", required=True, help="Prompt text (use '-' for stdin)")
    cron_add_p.add_argument("--model", default="sonnet", help="Model (default: sonnet)")
    cron_add_p.add_argument("--tools", default=None, help="Comma-separated allowed tools")

    cron_remove_p = cron_sub.add_parser("remove", help="Remove a job")
    cron_remove_p.add_argument("name", help="Job name")

    cron_enable_p = cron_sub.add_parser("enable", help="Enable a job")
    cron_enable_p.add_argument("name", help="Job name")

    cron_disable_p = cron_sub.add_parser("disable", help="Disable a job")
    cron_disable_p.add_argument("name", help="Job name")

    # whatsapp
    whatsapp_parser = sub.add_parser("whatsapp", help="WhatsApp bridge commands")
    whatsapp_sub = whatsapp_parser.add_subparsers(dest="whatsapp_action")

    whatsapp_sub.add_parser("status", help="Check bridge connection status")
    whatsapp_sub.add_parser("qr", help="Show QR code instructions")

    # openrouter
    openrouter_parser = sub.add_parser("openrouter", help="Toggle OpenRouter mode")
    openrouter_sub = openrouter_parser.add_subparsers(dest="openrouter_action")

    openrouter_sub.add_parser("on", help="Enable OpenRouter mode")
    openrouter_sub.add_parser("off", help="Disable OpenRouter mode")
    openrouter_sub.add_parser("status", help="Check OpenRouter status")

    # Parse and dispatch
    args = parser.parse_args()

    if args.command is None:
        parser.print_help()
        sys.exit(0)

    dispatch = {
        "status": cmd_status,
        "doctor": cmd_doctor,
        "restart": cmd_restart,
        "stop": cmd_stop,
        "logs": cmd_logs,
        "sessions": lambda a: (
            cmd_sessions(a) if a.sessions_action else (sessions_parser.print_help() or sys.exit(0))
        ),
        "channel": lambda a: (
            cmd_channel(a) if a.channel_action else (channel_parser.print_help() or sys.exit(0))
        ),
        "send": cmd_send,
        "memory": lambda a: (
            cmd_memory(a) if a.memory_action else (memory_parser.print_help() or sys.exit(0))
        ),
        "heartbeat": cmd_heartbeat,
        "cron": lambda a: (
            cmd_cron(a) if a.cron_action else (cron_parser.print_help() or sys.exit(0))
        ),
        "secrets": lambda a: (
            cmd_secrets(a) if a.secrets_action else (secrets_parser.print_help() or sys.exit(0))
        ),
        "whatsapp": lambda a: (
            cmd_whatsapp(a) if a.whatsapp_action else (whatsapp_parser.print_help() or sys.exit(0))
        ),
        "openrouter": lambda a: (
            cmd_openrouter(a) if a.openrouter_action else (openrouter_parser.print_help() or sys.exit(0))
        ),
    }

    handler = dispatch.get(args.command)
    if handler:
        handler(args)
    else:
        parser.print_help()


if __name__ == "__main__":
    main()
