"""
cli.py — Command-line interface for CallMe.

Commands:
  callme init    — Interactive setup wizard
  callme run     — Start the agent (foreground by default)
  callme stop    — Stop a running agent
  callme status  — Check if agent is running
  callme doctor  — Diagnose the installation
"""
from __future__ import annotations

import asyncio
import json
import os
import platform
import signal
import subprocess
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path

import click

from .config import (
    AI_PROVIDERS,
    DATA_DIR,
    LOCK_FILE,
    LOG_DIR,
    STARTUP_DELAY,
)
from .store import get_store


# ---------------------------------------------------------------------------
# Console helpers
# ---------------------------------------------------------------------------

def _step(n: int, total: int, text: str) -> None:
    click.echo(click.style(f"\n[{n}/{total}] {text}", fg="cyan", bold=True))


def _ok(text: str) -> None:
    click.echo(click.style("  ✓ " + text, fg="green"))


def _err(text: str) -> None:
    click.echo(click.style("  ✗ " + text, fg="red"))


def _warn(text: str) -> None:
    click.echo(click.style("  ⚠ " + text, fg="yellow"))


def _info(text: str) -> None:
    click.echo("  " + text)


# ---------------------------------------------------------------------------
# PID file helpers
# ---------------------------------------------------------------------------

def _pid_is_alive(pid: int) -> bool:
    """Return True if *pid* is a running, non-zombie process."""
    try:
        import psutil
        p = psutil.Process(pid)
        return p.is_running() and p.status() not in ("zombie", "dead")
    except Exception:
        # Fallback: os.kill(pid, 0) — no signal, just check existence
        try:
            os.kill(pid, 0)
            return True
        except (OSError, ProcessLookupError):
            return False


def _read_pid() -> int | None:
    try:
        text = LOCK_FILE.read_text(encoding="utf-8").strip()
        return int(text) if text.isdigit() else None
    except Exception:
        return None


def _write_pid(pid: int) -> None:
    DATA_DIR.mkdir(parents=True, exist_ok=True)
    LOCK_FILE.write_text(str(pid), encoding="utf-8")


def _remove_pid() -> None:
    try:
        LOCK_FILE.unlink(missing_ok=True)
    except Exception:
        pass


# ---------------------------------------------------------------------------
# CLI group
# ---------------------------------------------------------------------------

@click.group()
@click.version_option(version="1.0.0", prog_name="callme")
def main() -> None:
    """CallMe — AI agent that controls your computer via Telegram."""


# ---------------------------------------------------------------------------
# init
# ---------------------------------------------------------------------------

@main.command()
@click.option("--reconfigure", is_flag=True, help="Re-run setup even if already configured.")
def init(reconfigure: bool) -> None:
    """Interactive setup wizard. Run once before first use."""
    asyncio.run(_init_async(reconfigure))


async def _init_async(reconfigure: bool) -> None:
    store = get_store()
    TOTAL = 4

    if store.exists() and not reconfigure:
        click.echo(click.style("✅ CallMe is already configured.", fg="green"))
        click.echo("  Run 'callme run' to start, or 'callme init --reconfigure' to redo setup.")
        return

    click.echo(click.style("\n📱 CallMe Setup Wizard", fg="magenta", bold=True))
    click.echo("=" * 50)

    # ------------------------------------------------------------------
    # Step 1: Dependencies
    # ------------------------------------------------------------------
    _step(1, TOTAL, "Checking dependencies")
    required = [("click", "click"), ("psutil", "psutil"), ("cryptography", "cryptography")]
    missing = []
    for name, pkg in required:
        try:
            __import__(name)
            _ok(name)
        except ImportError:
            _err(f"{name} — not installed")
            missing.append(pkg)

    if missing:
        click.echo(f"\n  Installing missing packages: {', '.join(missing)}")
        try:
            subprocess.run(
                [sys.executable, "-m", "pip", "install"] + missing,
                check=True,
            )
            _ok("All dependencies installed")
        except subprocess.CalledProcessError as exc:
            _err(f"pip install failed: {exc}")
            sys.exit(1)

    # ------------------------------------------------------------------
    # Step 2: AI provider / model / API key
    # ------------------------------------------------------------------
    _step(2, TOTAL, "AI Provider Setup")
    click.echo("\n  Available providers:\n")

    providers = list(AI_PROVIDERS.items())
    for i, (key, info) in enumerate(providers, 1):
        free_count = sum(1 for m in info["models"] if m.get("free"))
        free_note = f"  ({free_count} free model{'s' if free_count != 1 else ''})" if free_count else ""
        click.echo(f"    {i:>2}. {info['display_name']}{free_note}")

    while True:
        choice = click.prompt("\n  Select provider number", type=int)
        if 1 <= choice <= len(providers):
            provider_key, provider_info = providers[choice - 1]
            break
        _err(f"Please choose 1–{len(providers)}")

    click.echo(f"\n  Models for {provider_info['display_name']}:\n")
    for i, model in enumerate(provider_info["models"], 1):
        free_tag = "  [FREE]" if model.get("free") else ""
        click.echo(f"    {i:>2}. {model['label']}{free_tag}")

    while True:
        mchoice = click.prompt("\n  Select model number", type=int)
        if 1 <= mchoice <= len(provider_info["models"]):
            model = provider_info["models"][mchoice - 1]
            break
        _err(f"Please choose 1–{len(provider_info['models'])}")

    click.echo(f"\n  Get your API key: {click.style(provider_info['key_url'], fg='blue', underline=True)}")
    api_key = click.prompt("  Enter your API key", hide_input=True)

    if not api_key.strip():
        _err("API key cannot be empty")
        sys.exit(1)

    # Validate the key with a real request
    click.echo("  Validating API key…")
    from .ai import validate_api_key
    ok, msg = validate_api_key(provider_key, api_key.strip(), model["id"])
    if ok:
        _ok(msg)
    else:
        _warn(msg)
        if not click.confirm("  Continue anyway?", default=False):
            click.echo("  Setup cancelled.")
            sys.exit(1)

    store.set("ai_provider", provider_key)
    store.set("ai_model", model["id"])
    store.set("ai_api_key", api_key.strip())
    store.set("ai_base_url", provider_info["base_url"])

    # ------------------------------------------------------------------
    # Step 3: Telegram bot
    # ------------------------------------------------------------------
    _step(3, TOTAL, "Telegram Bot Setup")
    click.echo("""
  How to get your Telegram Bot Token:
    1. Open Telegram and search for @BotFather
    2. Send: /newbot
    3. Follow the instructions — copy the token it gives you
    """)
    telegram_token = click.prompt("  Enter your Telegram Bot Token")
    if not telegram_token.strip():
        _err("Telegram token cannot be empty")
        sys.exit(1)

    click.echo("""
  How to find your Telegram User ID:
    1. Search for @userinfobot in Telegram
    2. Send /start — it will reply with your numeric ID
    """)
    telegram_user_id = click.prompt("  Enter your Telegram User ID", type=int)

    # Test the connection by sending a message
    click.echo("  Testing Telegram connection…")
    test_url = f"https://api.telegram.org/bot{telegram_token.strip()}/sendMessage"
    test_payload = json.dumps({
        "chat_id": telegram_user_id,
        "text": "✅ *CallMe is connected!* Setup complete — run `callme run` to start.",
        "parse_mode": "Markdown",
    }).encode("utf-8")
    req = urllib.request.Request(
        test_url, data=test_payload,
        headers={"Content-Type": "application/json"}, method="POST",
    )
    try:
        with urllib.request.urlopen(req, timeout=15) as resp:
            data = json.loads(resp.read())
        if data.get("ok"):
            _ok("Test message sent! Check your Telegram.")
        else:
            _warn(f"Telegram responded: {data.get('description', '?')}")
            if not click.confirm("  Continue anyway?", default=False):
                sys.exit(1)
    except urllib.error.URLError as exc:
        _warn(f"Could not reach Telegram: {exc}")
        if not click.confirm("  Continue anyway?", default=False):
            sys.exit(1)
    except Exception as exc:
        _warn(f"Telegram test failed: {exc}")
        if not click.confirm("  Continue anyway?", default=False):
            sys.exit(1)

    store.set("telegram_token", telegram_token.strip())
    store.set("telegram_user_id", telegram_user_id)

    # ------------------------------------------------------------------
    # Step 4: Finalise
    # ------------------------------------------------------------------
    _step(4, TOTAL, "Saving configuration")
    LOG_DIR.mkdir(parents=True, exist_ok=True)
    DATA_DIR.mkdir(parents=True, exist_ok=True)
    store.set("setup_complete", True)
    _ok(f"Encrypted config saved to {DATA_DIR}")

    click.echo(click.style("""
╔══════════════════════════════════════╗
║   ✅  Setup complete!                 ║
║                                      ║
║   Start your agent:                  ║
║     callme run                       ║
║                                      ║
║   Run in background:                 ║
║     nohup callme run &               ║
╚══════════════════════════════════════╝
""", fg="green"))


# ---------------------------------------------------------------------------
# run
# ---------------------------------------------------------------------------

@main.command()
@click.option("--foreground/--background", default=True,
              help="Run in foreground (default) or background daemon.")
def run(foreground: bool) -> None:
    """Start the CallMe agent."""
    store = get_store()
    if not store.exists():
        click.echo(click.style("❌ Not configured. Run: callme init", fg="red"))
        sys.exit(1)

    # Check for stale/running instance
    existing_pid = _read_pid()
    if existing_pid is not None:
        if _pid_is_alive(existing_pid):
            click.echo(click.style(
                f"⚠️  Agent already running (PID {existing_pid}). Use 'callme stop' first.",
                fg="yellow",
            ))
            sys.exit(1)
        else:
            _remove_pid()  # Clean up stale PID

    if not foreground:
        LOG_DIR.mkdir(parents=True, exist_ok=True)
        log_file = LOG_DIR / "agent.log"
        with open(log_file, "a") as lf:
            proc = subprocess.Popen(
                [sys.executable, "-m", "callme.cli", "run", "--foreground"],
                stdout=lf,
                stderr=lf,
                start_new_session=True,
                close_fds=True,
            )
        _write_pid(proc.pid)
        click.echo(click.style(f"✅ CallMe agent started in background (PID {proc.pid})", fg="green"))
        click.echo(f"  Logs: {log_file}")
        return

    # Foreground
    try:
        asyncio.run(_run_async())
    except KeyboardInterrupt:
        click.echo("\n⏹️  Stopped by user.")
    finally:
        _remove_pid()


async def _run_async() -> None:
    from .ai import AIEngine
    from .bot import TelegramBot
    from .logger import setup_file_logging
    from .tools import register_send_callback

    setup_file_logging()
    store = get_store()
    config = store.all()

    _write_pid(os.getpid())
    click.echo(click.style("🤖 CallMe agent running. Press Ctrl+C to stop.", fg="green"))

    engine = AIEngine(config)
    bot = TelegramBot(
        token=config["telegram_token"],
        user_id=config["telegram_user_id"],
        engine=engine,
    )

    register_send_callback(bot.send_message)

    async def _startup_msg() -> None:
        await asyncio.sleep(STARTUP_DELAY)
        import platform as _pl
        sys_info = f"{_pl.system()} {_pl.release()} ({_pl.machine()})"
        await bot.send_message(
            f"🤖 *CallMe agent started*\n"
            f"🖥️ {sys_info}\n"
            f"🧠 {config.get('ai_provider', '?')} / {config.get('ai_model', '?')}\n"
            f"💬 Send me a command or /help"
        )

    asyncio.ensure_future(_startup_msg())

    # Handle SIGTERM gracefully
    loop = asyncio.get_event_loop()
    for sig in (signal.SIGTERM, signal.SIGINT):
        try:
            loop.add_signal_handler(sig, bot.stop)
        except (NotImplementedError, RuntimeError):
            pass  # Windows doesn't support add_signal_handler for all signals

    await bot.start()


# ---------------------------------------------------------------------------
# stop
# ---------------------------------------------------------------------------

@main.command()
def stop() -> None:
    """Stop the running CallMe agent."""
    pid = _read_pid()
    if pid is None:
        click.echo("⚠️  No PID file found. Is the agent running?")
        return
    if not _pid_is_alive(pid):
        _remove_pid()
        click.echo("ℹ️  Agent was not running. Removed stale PID file.")
        return
    try:
        os.kill(pid, signal.SIGTERM)
        # Wait up to 8 seconds for graceful shutdown
        for _ in range(80):
            if not _pid_is_alive(pid):
                break
            time.sleep(0.1)
        if _pid_is_alive(pid):
            os.kill(pid, signal.SIGKILL)
            click.echo(click.style(f"🔪 Force-killed PID {pid}", fg="yellow"))
        else:
            click.echo(click.style(f"✅ Agent (PID {pid}) stopped.", fg="green"))
        _remove_pid()
    except ProcessLookupError:
        _remove_pid()
        click.echo("ℹ️  Process already exited.")
    except PermissionError:
        click.echo(click.style(
            "❌ Permission denied. Run as the same user that started the agent.", fg="red"
        ))
    except AttributeError:
        # Windows: signal.SIGTERM may not be available
        try:
            subprocess.run(["taskkill", "/F", "/PID", str(pid)], check=True, capture_output=True)
            _remove_pid()
            click.echo(click.style(f"✅ Agent (PID {pid}) killed.", fg="green"))
        except Exception as exc:
            click.echo(click.style(f"❌ Could not stop agent: {exc}", fg="red"))


# ---------------------------------------------------------------------------
# status
# ---------------------------------------------------------------------------

@main.command()
def status() -> None:
    """Show whether the CallMe agent is running and its configuration."""
    pid = _read_pid()
    store = get_store()

    click.echo("\n🤖 CallMe Status")
    click.echo("─" * 35)

    if pid and _pid_is_alive(pid):
        click.echo(click.style(f"  Agent:  ✅ Running (PID {pid})", fg="green"))
        try:
            import psutil
            p = psutil.Process(pid)
            cpu = p.cpu_percent(interval=0.3)
            mem = p.memory_info().rss / 1024 / 1024
            uptime_secs = time.time() - p.create_time()
            h, rem = divmod(int(uptime_secs), 3600)
            m = rem // 60
            click.echo(f"  Uptime: {h}h {m}m  |  CPU {cpu:.1f}%  |  RAM {mem:.1f} MB")
        except Exception:
            pass
    elif pid:
        click.echo(click.style(f"  Agent:  ⚠️  Stale PID {pid} (process not alive)", fg="yellow"))
        click.echo("  Run: callme run")
    else:
        click.echo(click.style("  Agent:  ❌ Not running", fg="red"))
        click.echo("  Run: callme run")

    if store.exists():
        cfg = store.all()
        click.echo(click.style("  Config: ✅ Configured", fg="green"))
        click.echo(f"  AI:     {cfg.get('ai_provider', '?')} / {cfg.get('ai_model', '?')}")
        click.echo(f"  User:   Telegram ID {cfg.get('telegram_user_id', '?')}")
        click.echo(f"  Logs:   {LOG_DIR}")
    else:
        click.echo(click.style("  Config: ❌ Not configured — run: callme init", fg="red"))

    click.echo()


# ---------------------------------------------------------------------------
# doctor
# ---------------------------------------------------------------------------

@main.command()
def doctor() -> None:
    """Diagnose the CallMe installation and configuration."""
    click.echo("\n🩺 CallMe Doctor")
    click.echo("═" * 40)
    passed = 0
    failed = 0
    warned = 0

    def check(label: str, ok: bool | None, detail: str = "", warning: bool = False) -> None:
        nonlocal passed, failed, warned
        if ok is True:
            passed += 1
            suffix = f": {detail}" if detail else ""
            _ok(f"{label}{suffix}")
        elif ok is False:
            failed += 1
            suffix = f": {detail}" if detail else ""
            _err(f"{label}{suffix}")
        else:
            warned += 1
            suffix = f": {detail}" if detail else ""
            _warn(f"{label}{suffix}")

    # Python version
    v = sys.version_info
    check("Python ≥ 3.10", v >= (3, 10), f"{v.major}.{v.minor}.{v.micro}")

    # Operating system
    _info(f"OS: {platform.system()} {platform.release()} ({platform.machine()})")

    # Dependencies
    for mod, name, required in [
        ("click", "click", True),
        ("psutil", "psutil", True),
        ("cryptography", "cryptography", True),
    ]:
        try:
            m = __import__(mod)
            ver = getattr(m, "__version__", "?")
            check(f"Package: {name}", True, ver)
        except ImportError:
            check(f"Package: {name}", False, f"not installed — pip install {name}")

    # Optional screenshot tools
    click.echo("\n  Screenshot tools (need at least one on Linux):")
    for tool in ["scrot", "gnome-screenshot", "import", "maim"]:
        import shutil
        found = bool(shutil.which(tool))
        if found:
            check(f"  Screenshot: {tool}", True, "found")
            break
    else:
        if platform.system() == "Linux":
            check("  Screenshot tool", None, "none found — sudo apt install scrot")

    # Config
    store = get_store()
    check("Setup complete", store.exists(), "" if store.exists() else "run: callme init")

    if store.exists():
        cfg = store.all()
        check("Telegram token", bool(cfg.get("telegram_token")))
        check("Telegram user_id", bool(cfg.get("telegram_user_id")))
        check("AI provider", bool(cfg.get("ai_provider")), cfg.get("ai_provider", "?"))
        check("AI model", bool(cfg.get("ai_model")), cfg.get("ai_model", "?"))
        check("AI API key", bool(cfg.get("ai_api_key")), "present" if cfg.get("ai_api_key") else "missing")

    # Encryption key file
    from .config import ENCRYPTION_KEY_FILE
    check("Encryption key file", ENCRYPTION_KEY_FILE.exists(), str(ENCRYPTION_KEY_FILE) if ENCRYPTION_KEY_FILE.exists() else "missing")

    # Internet
    click.echo("\n  Testing connectivity:")
    for host, url in [
        ("Telegram API",  "https://api.telegram.org"),
        ("open-meteo",    "https://api.open-meteo.com"),
        ("OpenAI",        "https://api.openai.com"),
    ]:
        try:
            req = urllib.request.Request(url, method="HEAD", headers={"User-Agent": "CallMe/1.0"})
            with urllib.request.urlopen(req, timeout=6):
                pass
            check(f"  Reach {host}", True)
        except Exception as exc:
            check(f"  Reach {host}", None, str(exc)[:60])

    # Running agent?
    pid = _read_pid()
    if pid and _pid_is_alive(pid):
        check("Agent running", True, f"PID {pid}")
    else:
        _warn("Agent: not running — start with: callme run")

    # Summary
    click.echo(f"\n{'─' * 40}")
    total = passed + failed + warned
    color = "green" if failed == 0 else ("yellow" if failed <= 2 else "red")
    click.echo(click.style(f"  {passed}/{total} checks passed  ({warned} warnings)", fg=color, bold=True))
    if failed > 0:
        click.echo("  Fix failures, then re-run 'callme doctor' or 'callme init'.")
    click.echo()


# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    main()
