"""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.

v1.3: Rewritten for mainstream ``openclaw`` npm package (single gateway
process, full channel support for Telegram/WhatsApp/Discord/etc.).
Replaces the old @openclaw/cli v0.1.0 stub that had zero channel support.
"""

from __future__ import annotations

import json
import logging
import os
import re
import subprocess
import time
import uuid
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, OPENCLAW_FREE_MODEL, OPENCLAW_VERSION
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],
        )


# ---------------------------------------------------------------------------
# Version resolution
# ---------------------------------------------------------------------------

def _resolve_openclaw_version() -> str:
    """Resolve the OpenClaw version to install.

    Tries to fetch the version manifest from easyclaw.help first,
    falls back to the hardcoded OPENCLAW_VERSION constant.
    """
    try:
        r = _run_cmd(
            'curl -sf --connect-timeout 5 '
            '"https://easyclaw.help/packages/version.json"',
            timeout=10,
        )
        if r.returncode == 0 and r.stdout.strip():
            data = json.loads(r.stdout)
            version = data.get("current", "")
            if version:
                return version
    except Exception:
        pass
    return OPENCLAW_VERSION


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

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

    steps = [
        _step_preflight,
        _step_nodejs,
        _step_openclaw,
        _step_config,
        _step_env,
        _step_workspace,
        _step_systemd,
        _step_start,
        _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! Gateway is running.\n"
        "\n"
        "What's next? Just ask me:\n"
        "  - 'set up Telegram' to connect a Telegram bot\n"
        "  - 'set up WhatsApp' to connect your WhatsApp\n"
        "  - 'web GUI' to 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."""
    r = _run_cmd("lsb_release -ds 2>/dev/null || head -1 /etc/os-release")
    _post_output(app, f"OS: {r.stdout.strip()}")

    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")

    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 OK")

    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 OK")


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_openclaw(
    app: App, state: InstallState, step: int, title: str,
) -> None:
    """Step 3: Install mainstream openclaw from npm."""
    version = _resolve_openclaw_version()
    _post_output(
        app,
        f"Installing openclaw@{version} from npm "
        "(~127MB, this may take 2-3 minutes)...",
    )
    r = _run_cmd(
        f"npm install -g openclaw@{version}",
        timeout=600,
    )
    _require_success(r, step, title, f"npm install openclaw@{version} 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"OpenClaw {r.stdout.strip()} installed")


def _step_config(
    app: App, state: InstallState, step: int, title: str,
) -> None:
    """Step 4: Create OpenClaw config at ~/.openclaw/openclaw.json."""
    config_dir = os.path.expanduser("~/.openclaw")
    config_path = os.path.join(config_dir, "openclaw.json")

    os.makedirs(config_dir, exist_ok=True)

    auth_token = uuid.uuid4().hex

    config = {
        "gateway": {
            "mode": "local",
            "bind": "loopback",
            "auth": {
                "token": auth_token,
            },
        },
        "agents": {
            "defaults": {
                "model": {
                    "primary": OPENCLAW_FREE_MODEL,
                },
            },
        },
    }

    if os.path.isfile(config_path):
        try:
            with open(config_path) as f:
                existing = json.load(f)
            for key in config:
                if key not in existing:
                    existing[key] = config[key]
            config = existing
            _post_output(app, "Existing config found, merging defaults")
        except (json.JSONDecodeError, OSError):
            _post_output(app, "Existing config invalid, replacing")

    with open(config_path, "w") as f:
        json.dump(config, f, indent=2)
        f.write("\n")

    _post_output(app, f"Config written to {config_path}")

    r = _run_cmd("openclaw config validate")
    if r.returncode != 0:
        _post_output(
            app,
            f"Config validation warning: {r.stderr.strip()}",
            stream="stderr",
        )
    else:
        _post_output(app, "Config validated OK")


def _step_env(
    app: App, state: InstallState, step: int, title: str,
) -> None:
    """Step 5: Set up environment (API key).

    The OPENROUTER_API_KEY is also injected into the systemd service
    in step 7. Here we persist it in /etc/environment and export it.
    """
    env_line = f"OPENROUTER_API_KEY={OPENCLAW_FREE_KEY}"
    try:
        env_path = "/etc/environment"
        env_lines = []
        if os.path.isfile(env_path):
            with open(env_path) as f:
                env_lines = [
                    ln for ln in f.readlines()
                    if not ln.strip().startswith("OPENROUTER_API_KEY=")
                ]
        env_lines.append(env_line + "\n")
        with open(env_path, "w") as f:
            f.writelines(env_lines)
    except OSError as exc:
        _post_output(
            app,
            f"Warning: could not write /etc/environment: {exc}",
            stream="stderr",
        )

    os.environ["OPENROUTER_API_KEY"] = OPENCLAW_FREE_KEY
    _post_output(app, "Environment configured with free API key")


def _step_workspace(
    app: App, state: InstallState, step: int, title: str,
) -> None:
    """Step 6: Create workspace directory."""
    workspace_dir = os.path.expanduser("~/.openclaw/workspace")
    os.makedirs(workspace_dir, exist_ok=True)

    soul_path = os.path.join(workspace_dir, "SOUL.md")
    if not os.path.isfile(soul_path):
        with open(soul_path, "w") as f:
            f.write(
                "# OpenClaw Agent\n\n"
                "You are a helpful AI assistant powered by OpenClaw.\n"
            )

    _post_output(app, f"Workspace created at {workspace_dir}")


_SYSTEMD_SERVICE = """\
[Unit]
Description=OpenClaw Gateway
After=network.target

[Service]
Type=simple
User=root
Environment=HOME=/root
Environment=OPENROUTER_API_KEY={api_key}
ExecStart=/usr/bin/openclaw gateway --bind loopback
Restart=on-failure
RestartSec=5
TimeoutStartSec=30

[Install]
WantedBy=multi-user.target
"""


def _step_systemd(
    app: App, state: InstallState, step: int, title: str,
) -> None:
    """Step 7: Create systemd service for OpenClaw gateway."""
    service_path = "/etc/systemd/system/openclaw-gateway.service"
    service_content = _SYSTEMD_SERVICE.format(api_key=OPENCLAW_FREE_KEY)

    with open(service_path, "w") as f:
        f.write(service_content)

    r = _run_cmd("systemctl daemon-reload")
    _require_success(r, step, title, "systemctl daemon-reload failed")

    _post_output(app, f"Systemd service created at {service_path}")


def _step_start(
    app: App, state: InstallState, step: int, title: str,
) -> None:
    """Step 8: Enable and start the OpenClaw gateway service."""
    r = _run_cmd("systemctl enable openclaw-gateway")
    _require_success(r, step, title, "systemctl enable failed")

    r = _run_cmd("systemctl start openclaw-gateway")
    _require_success(r, step, title, "systemctl start failed")

    _post_output(app, "Gateway starting, waiting 8s for startup...")
    time.sleep(8)

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


def _step_health(
    app: App, state: InstallState, step: int, title: str,
) -> None:
    """Step 9: Run health checks."""
    r = _run_cmd("openclaw config validate")
    if r.returncode == 0 and r.stdout.strip():
        _post_output(app, r.stdout.strip())

    r = _run_cmd("openclaw health")
    if r.returncode == 0 and r.stdout.strip():
        _post_output(app, r.stdout.strip())
    elif r.returncode != 0:
        _post_output(
            app,
            f"Health check returned exit {r.returncode} "
            "(gateway may still be starting up)",
            stream="stderr",
        )

    _post_output(app, "Health check complete")
