"""Deterministic install runner — no LLM, just subprocess.run().

Runs in a background thread via @work(thread=True). Each step is a
plain Python function that executes fixed shell commands. Output and
progress updates are posted to the Textual UI via call_from_thread().

On error: the runner stops, records the error in InstallState, and
returns. The conversational agent can then help the user diagnose.
"""

from __future__ import annotations

import logging
import re
import subprocess
import time
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from textual.app import App
    from .state import InstallState

from .constants import INSTALL_STEPS, OPENCLAW_FREE_KEY
from .messages import InstallOutput, ProgressUpdate

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Exceptions
# ---------------------------------------------------------------------------

class StepError(Exception):
    """A deterministic install step failed."""

    def __init__(
        self, step: int, title: str, message: str,
        stdout: str = "", stderr: str = "",
    ) -> None:
        self.step = step
        self.title = title
        self.stdout = stdout
        self.stderr = stderr
        super().__init__(message)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _run_cmd(
    command: str, timeout: int = 120,
) -> subprocess.CompletedProcess:
    """Run a shell command and return the result."""
    return subprocess.run(
        command, shell=True, capture_output=True,
        text=True, timeout=timeout,
    )


def _post_output(app: App, text: str, stream: str = "stdout") -> None:
    """Post a line of install output to the chat UI."""
    app.call_from_thread(app.post_message, InstallOutput(text, stream))


def _post_progress(
    app: App, state: InstallState,
    step: int, title: str, status: str,
) -> None:
    """Post a progress update to the chat UI and update shared state."""
    state.current_step = step
    if status == "complete" and title not in state.completed_steps:
        state.completed_steps.append(title)
    elif status == "error":
        state.has_error = True
    app.call_from_thread(
        app.post_message,
        ProgressUpdate(step, state.total_steps, title, status),
    )


def _require_success(
    result: subprocess.CompletedProcess,
    step: int, title: str, context: str = "",
) -> None:
    """Raise StepError if the command failed."""
    if result.returncode != 0:
        msg = context or f"Command failed (exit {result.returncode})"
        raise StepError(
            step, title, msg,
            stdout=result.stdout[:4000],
            stderr=result.stderr[:4000],
        )


# ---------------------------------------------------------------------------
# Main entry point
# ---------------------------------------------------------------------------

def run_install(app: App, state: InstallState) -> None:
    """Execute the full 10-step OpenClaw install.

    Called from a @work(thread=True) worker in app.py.
    """
    state.install_active = True

    steps = [
        _step_preflight,
        _step_nodejs,
        _step_cli,
        _step_config,
        _step_token,
        _step_env,
        _step_workspace,
        _step_gateway,
        _step_service,
        _step_health,
    ]

    for i, step_fn in enumerate(steps, start=1):
        title = INSTALL_STEPS[i - 1]
        _post_progress(app, state, i, title, "starting")

        try:
            step_fn(app, state, i, title)
            _post_progress(app, state, i, title, "complete")
            state.install_log.append(f"Step {i} ({title}): OK")
        except StepError as exc:
            state.has_error = True
            state.error_step = i
            state.error_message = f"Step {i} ({title}) failed: {exc}"
            state.error_stdout = exc.stdout
            state.error_stderr = exc.stderr
            state.install_log.append(f"Step {i} ({title}): FAILED — {exc}")
            _post_progress(app, state, i, title, "error")
            _post_output(app, f"Step {i} failed: {exc}", stream="error")
            if exc.stderr:
                _post_output(app, exc.stderr.strip(), stream="stderr")
            _post_output(
                app,
                "\nType a message to ask me for help troubleshooting.",
                stream="stdout",
            )
            state.install_active = False
            return
        except subprocess.TimeoutExpired:
            state.has_error = True
            state.error_step = i
            state.error_message = f"Step {i} ({title}): Command timed out"
            state.install_log.append(f"Step {i} ({title}): TIMEOUT")
            _post_progress(app, state, i, title, "error")
            _post_output(app, "Command timed out", stream="error")
            state.install_active = False
            return
        except Exception as exc:
            state.has_error = True
            state.error_step = i
            state.error_message = f"Step {i} ({title}): {exc}"
            state.install_log.append(f"Step {i} ({title}): EXCEPTION — {exc}")
            _post_progress(app, state, i, title, "error")
            _post_output(app, f"Unexpected error: {exc}", stream="error")
            state.install_active = False
            logger.exception("Install step %d failed", i)
            return

    # All steps complete
    state.install_active = False
    state.install_complete = True
    _post_output(
        app,
        "OpenClaw installation complete! All services are running.\n"
        "\n"
        "What's next? Just ask me:\n"
        "  • 'set up WhatsApp' — connect your WhatsApp\n"
        "  • 'set up Telegram' — connect a Telegram bot\n"
        "  • 'web GUI' — access the dashboard in your browser\n"
        "\n"
        "A free AI model is already configured — "
        "you can start using OpenClaw right away.",
        stream="success",
    )


# ---------------------------------------------------------------------------
# Step implementations
# ---------------------------------------------------------------------------

def _step_preflight(
    app: App, state: InstallState, step: int, title: str,
) -> None:
    """Step 1: Verify OS, RAM, disk, root."""
    # OS check
    r = _run_cmd("lsb_release -ds 2>/dev/null || head -1 /etc/os-release")
    _post_output(app, f"OS: {r.stdout.strip()}")

    # Root check
    r = _run_cmd("whoami")
    if r.stdout.strip() != "root":
        raise StepError(step, title, "Must run as root. Use: sudo su -")
    _post_output(app, "Running as root ✓")

    # RAM check (512MB min, allow some kernel overhead)
    r = _run_cmd("free -m | awk '/Mem:/ {print $2}'")
    try:
        ram_mb = int(r.stdout.strip())
    except ValueError:
        ram_mb = 0
    if ram_mb < 450:
        raise StepError(
            step, title, f"Need ≥ 512MB RAM, found {ram_mb}MB",
        )
    _post_output(app, f"RAM: {ram_mb}MB ✓")

    # Disk check (5GB min)
    r = _run_cmd("df -BG / | awk 'NR==2 {print $4}' | tr -d 'G'")
    try:
        disk_gb = int(r.stdout.strip())
    except ValueError:
        disk_gb = 0
    if disk_gb < 5:
        raise StepError(
            step, title, f"Need ≥ 5GB free disk, found {disk_gb}GB",
        )
    _post_output(app, f"Disk: {disk_gb}GB free ✓")


def _step_nodejs(
    app: App, state: InstallState, step: int, title: str,
) -> None:
    """Step 2: Install Node.js 22 (skip if already present)."""
    r = _run_cmd("node --version 2>/dev/null")
    if r.returncode == 0:
        version = r.stdout.strip()
        match = re.match(r"v(\d+)\.", version)
        if match and int(match.group(1)) >= 22:
            _post_output(app, f"Node.js {version} already installed — skipping")
            return

    _post_output(app, "Installing Node.js 22 via NodeSource...")
    r = _run_cmd(
        "curl -fsSL https://deb.nodesource.com/setup_22.x | bash -",
        timeout=120,
    )
    _require_success(r, step, title, "NodeSource setup script failed")

    r = _run_cmd("apt-get install -y nodejs", timeout=300)
    _require_success(r, step, title, "apt-get install nodejs failed")

    r = _run_cmd("node --version")
    _require_success(r, step, title, "node not found after install")
    _post_output(app, f"Node.js {r.stdout.strip()} installed ✓")


def _step_cli(
    app: App, state: InstallState, step: int, title: str,
) -> None:
    """Step 3: Install @openclaw/cli from EasyClaw tarball."""
    _post_output(app, "Installing @openclaw/cli...")
    r = _run_cmd(
        "npm install -g https://easyclaw.help/packages/openclaw-cli-0.1.0.tgz",
        timeout=120,
    )
    _require_success(r, step, title, "npm install @openclaw/cli failed")

    r = _run_cmd("openclaw --version 2>/dev/null || which openclaw")
    _require_success(r, step, title, "openclaw CLI not found after install")
    _post_output(app, f"CLI installed: {r.stdout.strip()} ✓")


def _step_config(
    app: App, state: InstallState, step: int, title: str,
) -> None:
    """Step 4: Config init, write, validate."""
    for cmd, desc in [
        ("openclaw config init --non-interactive", "config init"),
        ("openclaw config write", "config write"),
        ("openclaw config validate", "config validate"),
    ]:
        _post_output(app, f"Running {desc}...")
        r = _run_cmd(cmd)
        _require_success(r, step, title, f"{desc} failed")
        if r.stdout.strip():
            _post_output(app, r.stdout.strip())
    _post_output(app, "Configuration validated ✓")


def _step_token(
    app: App, state: InstallState, step: int, title: str,
) -> None:
    """Step 5: Generate auth token."""
    r = _run_cmd("openclaw token generate")
    _require_success(r, step, title, "token generate failed")
    _post_output(app, "Auth token generated ✓")


def _step_env(
    app: App, state: InstallState, step: int, title: str,
) -> None:
    """Step 6: Environment setup + inject free API key."""
    r = _run_cmd("openclaw env setup")
    _require_success(r, step, title, "env setup failed")

    # Read current env file, inject/update OPENROUTER_API_KEY
    env_path = "/etc/openclaw/env"
    try:
        with open(env_path, "r") as f:
            lines = f.readlines()
    except FileNotFoundError:
        lines = []

    # Remove any existing OPENROUTER_API_KEY lines, then add ours
    lines = [
        ln for ln in lines
        if not ln.strip().startswith("OPENROUTER_API_KEY=")
    ]
    lines.append(f"OPENROUTER_API_KEY={OPENCLAW_FREE_KEY}\n")

    with open(env_path, "w") as f:
        f.writelines(lines)

    _post_output(app, "Environment configured with free API key ✓")


def _step_workspace(
    app: App, state: InstallState, step: int, title: str,
) -> None:
    """Step 7: Workspace init and validate."""
    r = _run_cmd("openclaw workspace init")
    _require_success(r, step, title, "workspace init failed")

    r = _run_cmd("openclaw workspace validate")
    _require_success(r, step, title, "workspace validate failed")
    _post_output(app, "Workspace initialized ✓")


def _step_gateway(
    app: App, state: InstallState, step: int, title: str,
) -> None:
    """Step 8: Gateway install (config dir) and configure."""
    r = _run_cmd("openclaw gateway install")
    _require_success(r, step, title, "gateway install failed")

    r = _run_cmd("openclaw gateway configure")
    _require_success(r, step, title, "gateway configure failed")
    _post_output(app, "Gateway installed and configured ✓")


def _step_service(
    app: App, state: InstallState, step: int, title: str,
) -> None:
    """Step 9: Install systemd services, wait, verify."""
    r = _run_cmd("openclaw service install")
    _require_success(r, step, title, "service install failed")

    _post_output(app, "Services installed, waiting 5s for startup...")
    time.sleep(5)

    r = _run_cmd("systemctl is-active openclaw openclaw-gateway")
    output = r.stdout.strip()
    if "inactive" in output or "failed" in output:
        # Grab journal logs for diagnosis
        journal = _run_cmd(
            "journalctl -u openclaw -u openclaw-gateway -n 30 --no-pager"
        )
        raise StepError(
            step, title,
            f"Services not active: {output}",
            stderr=journal.stdout[:4000],
        )
    _post_output(app, f"Services running ✓")


def _step_health(
    app: App, state: InstallState, step: int, title: str,
) -> None:
    """Step 10: Full health check."""
    r = _run_cmd("openclaw health --full")
    _require_success(r, step, title, "Health check failed")
    if r.stdout.strip():
        _post_output(app, r.stdout.strip())
    _post_output(app, "Health check passed ✓")
