"""Fire-and-watch build orchestration tools."""

from __future__ import annotations

import asyncio
import os
import tempfile
import time
from typing import TYPE_CHECKING, Annotated

from anyio import Path
from fastmcp.tools import tool
from pydantic import Field

from mcp_vm_blackbox._helpers import (
    BUILD_SCRIPTS,
    BUILD_TERMINAL_MARKERS,
    LOG_POLL_INTERVAL_S,
    _resolve,
    _ssh_run,
    check_lines_for_pattern,
)
from mcp_vm_blackbox.models import BuildStartResult, BuildStatusResult, WatchResult

if TYPE_CHECKING:
    from mcp_vm_blackbox.targets import TargetConfig


async def _watch_log_local(log_path: str, watch_s: float, break_on: list[str]) -> WatchResult:
    """Tail a local log file for up to *watch_s* seconds.

    Returns early when any pattern from *break_on* is matched.

    Args:
        log_path: Absolute or relative path to the log file.
        watch_s: Maximum seconds to poll before returning with
            ``reason="timeout"``.
        break_on: Case-insensitive substrings that trigger early return.

    Returns:
        Dict with keys: ``lines_seen``, ``new_lines`` (last 10), ``matched``,
        ``matched_line``, ``elapsed_s``, ``reason``.
    """
    path = Path(log_path)
    start = time.monotonic()
    new_lines: list[str] = []

    if await path.exists():
        stat = await path.stat()
        offset = stat.st_size
    else:
        offset = 0

    while (time.monotonic() - start) < watch_s:
        if await path.exists():
            async with await path.open("rb") as fh:
                await fh.seek(offset)
                raw = await fh.read()
                offset = await fh.tell()
            if raw:
                chunk = raw.decode("utf-8", errors="replace")
                lines = chunk.splitlines()
                new_lines.extend(lines)
                hit = check_lines_for_pattern(lines, break_on)
                if hit is not None:
                    return _watch_result(new_lines, hit[0], hit[1], start, "pattern_match")
        await asyncio.sleep(LOG_POLL_INTERVAL_S)

    return _watch_result(new_lines, None, None, start, "timeout")


async def _watch_log_remote(cfg: TargetConfig, log_path: str, watch_s: float, break_on: list[str]) -> WatchResult:
    """Tail a remote log file via SSH for up to *watch_s* seconds.

    Uses byte-offset polling (``tail -c +N``) to avoid re-reading lines already
    returned.

    Args:
        cfg: Resolved target configuration providing the SSH destination.
        log_path: Path to the log file on the remote host.
        watch_s: Maximum seconds to poll.
        break_on: Case-insensitive substrings that trigger early return.

    Returns:
        Same structure as :func:`_watch_log_local`.
    """
    start = time.monotonic()
    new_lines: list[str] = []

    offset_result = await _ssh_run(cfg, ["bash", "-c", f"wc -c < {log_path} 2>/dev/null || echo 0"])
    offset = int(offset_result.stdout.strip() or "0")

    while (time.monotonic() - start) < watch_s:
        fetch_result = await _ssh_run(cfg, ["bash", "-c", f"tail -c +{offset + 1} {log_path} 2>/dev/null"])
        chunk = fetch_result.stdout

        if chunk:
            lines = chunk.splitlines()
            new_lines.extend(lines)
            offset += len(chunk.encode())
            hit = check_lines_for_pattern(lines, break_on)
            if hit is not None:
                return _watch_result(new_lines, hit[0], hit[1], start, "pattern_match")
        await asyncio.sleep(LOG_POLL_INTERVAL_S)

    return _watch_result(new_lines, None, None, start, "timeout")


def _watch_result(
    new_lines: list[str], matched: str | None, matched_line: str | None, start: float, reason: str
) -> WatchResult:
    """Build the standard log-watch result.

    Args:
        new_lines: All lines accumulated during the watch window.
        matched: The break pattern that matched, or ``None``.
        matched_line: The raw log line that contained the match, or ``None``.
        start: ``time.monotonic()`` value from the start of the watch.
        reason: ``"pattern_match"`` or ``"timeout"``.

    Returns:
        WatchResult with lines_seen, new_lines, matched,
        matched_line, elapsed_s, reason.
    """
    return WatchResult(
        lines_seen=len(new_lines),
        new_lines=new_lines[-10:],
        matched=matched,
        matched_line=matched_line,
        elapsed_s=round(time.monotonic() - start, 1),
        reason=reason,
    )


# ---------------------------------------------------------------------------
# Tools
# ---------------------------------------------------------------------------


@tool
async def build_start(
    build: Annotated[str, Field(description='Build alias: "windows-vm" or "postgres-vm".')],
    target: Annotated[str, Field(description='Named target or raw SSH string. Defaults to "ci".')] = "ci",
    log_dir: Annotated[str, Field(description="Directory on the target host where the build log is written.")] = "",
) -> BuildStartResult:
    """Start a VM build in the background on the target host.

    Args:
        build: One of ``"windows-vm"`` or ``"postgres-vm"``.
        target: Named target or raw SSH string.  Defaults to ``"ci"``.
        log_dir: Directory on the target host where the build log is written.
            Defaults to the system temporary directory.

    Returns:
        BuildStartResult with pid, log_path, message on success, or error on failure.
    """
    if build not in BUILD_SCRIPTS:
        return BuildStartResult(error=f"Unknown build '{build}'. Valid: {', '.join(BUILD_SCRIPTS)}")

    effective_log_dir = log_dir or tempfile.gettempdir()
    script = BUILD_SCRIPTS[build]
    log_path = f"{effective_log_dir}/packer-{build}-{os.getpid()}-build.log"
    cfg = _resolve(target)
    launch_cmd_str = f"nohup bash {script} > {log_path} 2>&1 & echo $!"

    result = await _ssh_run(cfg, ["bash", "-c", launch_cmd_str])

    if result.returncode != 0:
        return BuildStartResult(error=f"Failed to start build: {result.stderr}")

    pid = result.stdout.strip()
    return BuildStartResult(
        pid=pid, log_path=log_path, message=f"Build started. Use build_watch('{log_path}') to monitor."
    )


@tool
async def build_watch(
    log_path: Annotated[str, Field(description="Path to the log file (as returned by build_start).")],
    target: Annotated[str, Field(description='Named target or raw SSH string. Defaults to "ci".')] = "ci",
    watch_s: Annotated[int, Field(description="Maximum seconds to watch before returning.", ge=1)] = 60,
    break_on: Annotated[
        list[str] | None,
        Field(
            description='Case-insensitive substrings that trigger early return. Defaults to ["Build successful", "Error", "FAILED", "failed"].'
        ),
    ] = None,
) -> WatchResult:
    """Watch a build log for up to *watch_s* seconds, returning early on a pattern match.

    Call repeatedly to continue watching after a timeout.

    Args:
        log_path: Path to the log file (as returned by :func:`build_start`).
        target: Named target or raw SSH string.
        watch_s: Maximum seconds to watch before returning with
            ``reason="timeout"``.
        break_on: Case-insensitive substrings that trigger early return.
            Defaults to ``["Build successful", "Error", "FAILED", "failed"]``.

    Returns:
        WatchResult with lines_seen, new_lines (last 10), matched,
        matched_line, elapsed_s, reason.
    """
    if break_on is None:
        break_on = ["Build successful", "Error", "FAILED", "failed"]

    cfg = _resolve(target)
    if cfg.is_remote:
        return await _watch_log_remote(cfg, log_path, watch_s=float(watch_s), break_on=break_on)
    return await _watch_log_local(log_path, watch_s=float(watch_s), break_on=break_on)


@tool
async def build_status(
    log_path: Annotated[str, Field(description="Path to the log file.")],
    target: Annotated[str, Field(description='Named target or raw SSH string. Defaults to "ci".')] = "ci",
    tail_lines: Annotated[int, Field(description="Number of trailing log lines to return.", ge=1)] = 20,
) -> BuildStatusResult:
    """Check whether a build is still running and return the last N log lines.

    Args:
        log_path: Path to the log file.
        target: Named target or raw SSH string.
        tail_lines: Number of trailing log lines to return.  Defaults to 20.

    Returns:
        BuildStatusResult with running, last_lines, log_path.
    """
    cfg = _resolve(target)
    check_cmd = f"tail -{tail_lines} {log_path} 2>/dev/null"
    result = await _ssh_run(cfg, ["bash", "-c", check_cmd])

    last_lines = result.stdout.strip().splitlines() if result.returncode == 0 else []
    last_text = "\n".join(last_lines)
    running = not any(marker in last_text for marker in BUILD_TERMINAL_MARKERS)

    return BuildStatusResult(running=running, last_lines=last_lines, log_path=log_path)
