"""Tool definitions and synchronous executors for Agent Johnny 5.

All executors run inside the agent's background thread — they use
subprocess.run() and open() (blocking), NOT async equivalents.
This is safe because the agent thread is isolated from the main
Textual event loop.

The restart_step and resume_pipeline tools use
asyncio.run_coroutine_threadsafe() to schedule async work on the
main Textual event loop from the agent's sync thread.
"""

from __future__ import annotations

import asyncio
import json
import logging
import os
import signal
import subprocess
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from textual.app import App

    from ..state import InstallState

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# OpenAI-compatible tool definitions (sent to Kimi K2.5)
# ---------------------------------------------------------------------------

TOOL_DEFINITIONS: list[dict] = [
    {
        "type": "function",
        "function": {
            "name": "run_shell_command",
            "description": (
                "Execute a shell command on the server. "
                "Returns stdout, stderr, and return code. "
                "Use for diagnostics, checking services, reading configs, etc."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "command": {
                        "type": "string",
                        "description": "The shell command to execute",
                    },
                    "timeout": {
                        "type": "integer",
                        "description": "Timeout in seconds (default 30)",
                    },
                },
                "required": ["command"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "read_file",
            "description": "Read the full contents of a file on the server.",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {
                        "type": "string",
                        "description": "Absolute file path to read",
                    },
                },
                "required": ["path"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "write_file",
            "description": (
                "Write content to a file on the server. "
                "Creates parent directories if needed. "
                "Overwrites existing content."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {
                        "type": "string",
                        "description": "Absolute file path to write",
                    },
                    "content": {
                        "type": "string",
                        "description": "Content to write to the file",
                    },
                },
                "required": ["path", "content"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "interrupt_active_step",
            "description": (
                "Kill the currently running installation subprocess. "
                "Use when a step appears hung or stuck (no output for 60+ seconds). "
                "Sends SIGTERM by default, SIGKILL if SIGTERM fails."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "signal": {
                        "type": "string",
                        "enum": ["SIGTERM", "SIGKILL"],
                        "description": "Signal to send (default SIGTERM)",
                    },
                },
                "required": [],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "restart_step",
            "description": (
                "Re-run a specific installation step by name. "
                "Use after diagnosing and fixing the root cause of a failure. "
                "The step runs asynchronously and this tool blocks until it completes. "
                "Returns the step result (success/failure)."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "step_name": {
                        "type": "string",
                        "description": (
                            "Name of the step to restart "
                            "(e.g. 'OpenClaw CLI', 'Preflight', 'Node.js')"
                        ),
                    },
                },
                "required": ["step_name"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "resume_pipeline",
            "description": (
                "Resume the installation pipeline from a specific step number. "
                "All steps from that number onward will execute sequentially. "
                "Returns immediately — the pipeline runs asynchronously."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "from_step": {
                        "type": "integer",
                        "description": "Step number to resume from (1-16)",
                    },
                },
                "required": ["from_step"],
            },
        },
    },
]


# ---------------------------------------------------------------------------
# Synchronous tool executors (run in agent's background thread)
# ---------------------------------------------------------------------------

# Commands that must never be run
_BLOCKED_COMMANDS = frozenset([
    "rm -rf /",
    "rm -rf /*",
    "mkfs",
    "dd if=/dev/zero",
    ":(){:|:&};:",
])


def execute_tool(
    name: str, args: dict, state: InstallState, app: App
) -> str:
    """Execute a tool by name and return the result as a JSON string.

    Called synchronously from the agent's background thread.
    The `app` parameter is needed by restart_step and resume_pipeline
    to schedule async work on the main event loop.
    """
    if name == "run_shell_command":
        return _run_shell_command(args)
    elif name == "read_file":
        return _read_file(args)
    elif name == "write_file":
        return _write_file(args)
    elif name == "interrupt_active_step":
        return _interrupt_active_step(args, state)
    elif name == "restart_step":
        return _restart_step(args, state, app)
    elif name == "resume_pipeline":
        return _resume_pipeline(args, state, app)
    else:
        return json.dumps({"error": f"Unknown tool: {name}"})


def _run_shell_command(args: dict) -> str:
    """Execute a shell command via subprocess.run (blocking)."""
    command = args.get("command", "")
    timeout = args.get("timeout", 30)

    # Safety check
    cmd_lower = command.strip().lower()
    for blocked in _BLOCKED_COMMANDS:
        if blocked in cmd_lower:
            return json.dumps({
                "error": f"Blocked: '{command}' matches safety blocklist"
            })

    try:
        result = subprocess.run(
            command,
            shell=True,
            capture_output=True,
            timeout=timeout,
            text=True,
        )
        return json.dumps({
            "stdout": result.stdout[:8000],  # cap output size
            "stderr": result.stderr[:4000],
            "returncode": result.returncode,
        })
    except subprocess.TimeoutExpired:
        return json.dumps({"error": f"Command timed out after {timeout}s"})
    except Exception as exc:
        return json.dumps({"error": str(exc)})


def _read_file(args: dict) -> str:
    """Read file contents (blocking I/O)."""
    path = args.get("path", "")
    try:
        with open(path, "r", errors="replace") as f:
            content = f.read(100_000)  # cap at 100KB
        return json.dumps({"content": content})
    except Exception as exc:
        return json.dumps({"error": str(exc)})


def _write_file(args: dict) -> str:
    """Write content to a file (blocking I/O)."""
    path = args.get("path", "")
    content = args.get("content", "")
    try:
        os.makedirs(os.path.dirname(path), exist_ok=True)
        with open(path, "w") as f:
            f.write(content)
        return json.dumps({"success": True, "path": path})
    except Exception as exc:
        return json.dumps({"error": str(exc)})


def _interrupt_active_step(args: dict, state: InstallState) -> str:
    """Send a signal to the active installation subprocess.

    The pid_lock is asyncio-based, but we're in a sync thread.
    We access the process reference directly — the lock protects
    writes, and we only read here. The worst case is a stale read
    that says 'no process' right after one spawned, which is harmless.
    """
    signal_name = args.get("signal", "SIGTERM")
    proc = state.active_process

    if proc is None:
        return json.dumps({"error": "No active subprocess to interrupt"})

    sig = signal.SIGKILL if signal_name == "SIGKILL" else signal.SIGTERM

    try:
        proc.send_signal(sig)
        return json.dumps({
            "success": True,
            "pid": state.active_pid,
            "signal": signal_name,
        })
    except ProcessLookupError:
        return json.dumps({"error": "Process already terminated"})
    except Exception as exc:
        return json.dumps({"error": str(exc)})


def _restart_step(args: dict, state: InstallState, app: App) -> str:
    """Re-run a specific installation step by name.

    Finds the step in STEP_REGISTRY, schedules its execute() coroutine
    on the main event loop via asyncio.run_coroutine_threadsafe(), and
    blocks the agent thread until completion.
    """
    from ..installer.steps import STEP_REGISTRY
    from ..messages import LogLine, StepCompleted, StepStarted

    step_name = args.get("step_name", "")
    if not step_name:
        return json.dumps({"error": "step_name is required"})

    # Find step by name (case-insensitive)
    target_step = None
    step_index = 0
    for i, step in enumerate(STEP_REGISTRY, 1):
        if step.name.lower() == step_name.lower():
            target_step = step
            step_index = i
            break

    if target_step is None:
        available = ", ".join(f"'{s.name}'" for s in STEP_REGISTRY)
        return json.dumps({
            "error": f"Unknown step: '{step_name}'. Available: {available}"
        })

    if state.main_loop is None:
        return json.dumps({"error": "Main event loop not available"})

    if state.is_running:
        return json.dumps({
            "error": "Another step is currently running. Wait for it to finish."
        })

    async def _run_single_step() -> bool:
        """Execute one step on the main event loop."""
        state.is_running = True
        state.has_error = False
        state.current_step_index = step_index
        state.current_step_name = target_step.name

        app.post_message(
            StepStarted(step_index, target_step.name, state.total_steps)
        )
        app.post_message(
            LogLine(
                f"\n{'=' * 50}\n"
                f"  Restarting Step {step_index}/{state.total_steps}: "
                f"{target_step.name}\n"
                f"{'=' * 50}"
            )
        )

        # Precondition check
        ok, reason = target_step.validate_preconditions()
        if not ok:
            app.post_message(
                LogLine(
                    f"[SKIP] Precondition failed: {reason}",
                    is_stderr=True,
                )
            )
            app.post_message(
                StepCompleted(
                    step_index, target_step.name,
                    success=False, error=reason,
                )
            )
            state.has_error = True
            state.is_running = False
            return False

        success = await target_step.execute(state, app)

        if success:
            app.post_message(
                LogLine(f"  [OK] {target_step.name} completed successfully.")
            )
        else:
            app.post_message(
                LogLine(
                    f"  [FAIL] {target_step.name} failed on restart.",
                    is_stderr=True,
                )
            )

        app.post_message(
            StepCompleted(
                step_index, target_step.name,
                success=success,
                error="" if success else "Failed on restart",
            )
        )
        state.is_running = False
        if not success:
            state.has_error = True
        return success

    # Schedule on main loop and block this thread until done
    future = asyncio.run_coroutine_threadsafe(
        _run_single_step(), state.main_loop
    )

    try:
        success = future.result(timeout=target_step.timeout + 30)
        return json.dumps({
            "success": success,
            "step_name": target_step.name,
            "step_index": step_index,
        })
    except TimeoutError:
        return json.dumps({
            "error": f"Step '{target_step.name}' timed out during restart"
        })
    except Exception as exc:
        logger.exception("restart_step error")
        return json.dumps({"error": str(exc)})


def _resume_pipeline(args: dict, state: InstallState, app: App) -> str:
    """Resume the installation pipeline from a specific step.

    Schedules the StepRunner.resume_from() coroutine on the main loop.
    Returns immediately — the pipeline runs asynchronously.
    """
    from ..installer.steps import STEP_REGISTRY

    from_step = args.get("from_step", 0)
    total = len(STEP_REGISTRY)

    if not (1 <= from_step <= total):
        return json.dumps({
            "error": f"from_step must be between 1 and {total}, got {from_step}"
        })

    if state.is_running:
        return json.dumps({
            "error": "Pipeline is already running. Wait for it to finish."
        })

    # Schedule async resume on the main loop (non-blocking)
    app.call_from_thread(app.run_resume_pipeline, from_step)

    step_name = STEP_REGISTRY[from_step - 1].name
    return json.dumps({
        "status": "pipeline_resumed",
        "from_step": from_step,
        "step_name": step_name,
        "remaining_steps": total - from_step + 1,
    })
