"""Artifact management tools — artifact_register, artifact_list, artifact_diff, artifact_sync, artifact_install.

Tools register on the FastMCP server instance imported from server.py.
All tool handlers are wrapped with @structured_tool_handler (D-14).
"""

from __future__ import annotations

import logging
import os

from vmcluster_mcp.artifact.installer import ArtifactInstaller
from vmcluster_mcp.artifact.registry import ArtifactRegistry
from vmcluster_mcp.artifact.syncer import ArtifactSyncer
from vmcluster_mcp.exec.ssh import SSHConnectionManager
from vmcluster_mcp.models import ArtifactDiff, BuildArtifact, ErrorDetail, InstallStatus, SyncStatus, ToolResult
from vmcluster_mcp.server import config, mcp, structured_tool_handler
from vmcluster_mcp.topology.parser import load_topology

logger = logging.getLogger("vmcluster_mcp.artifact")

_registry = ArtifactRegistry(config.artifact_registry)
_registry.load()

_ssh_manager = SSHConnectionManager()
_syncer = ArtifactSyncer(_ssh_manager)
_installer = ArtifactInstaller(_ssh_manager)


@mcp.tool()
@structured_tool_handler
async def artifact_register(
    source_path: str,
    build_type: str = "raw_tree",
    kernel_version: str | None = None,
    metadata: dict[str, str] | None = None,
) -> ToolResult[BuildArtifact]:
    """Register a local build tree and receive a content-addressed artifact_id.

    Computes a sha256 hash of the entire directory tree, scans for .ko modules
    and ELF binaries, auto-detects kernel version from utsrelease.h if present,
    and persists the artifact record to the registry. Identical trees return the
    same artifact_id (deduplication).

    Args:
        source_path: Absolute path to the build output directory to register.
        build_type: One of 'raw_tree', 'make_install', 'rpm', 'deb'.
        kernel_version: Kernel version string. Auto-detected from
            include/generated/utsrelease.h if not provided.
        metadata: Optional key-value pairs to store with the artifact.

    Returns:
        ToolResult[BuildArtifact] with the registered artifact on success.
    """
    if not os.path.isdir(source_path):
        return ToolResult(
            success=False,
            error=ErrorDetail(
                code="SOURCE_NOT_FOUND",
                message=f"Source path '{source_path}' does not exist or is not a directory",
                recoverable=False,
            ),
        )

    artifact = _registry.register(source_path, build_type, kernel_version, metadata)
    return ToolResult(success=True, result=artifact)


@mcp.tool()
@structured_tool_handler
async def artifact_list() -> ToolResult[list[BuildArtifact]]:
    """List all registered artifacts with hash, source path, and timestamp.

    Returns:
        ToolResult[list[BuildArtifact]] with all registered artifacts.
    """
    artifacts = _registry.list_artifacts()
    return ToolResult(success=True, result=artifacts)


@mcp.tool()
@structured_tool_handler
async def artifact_diff(
    artifact_id_a: str,
    artifact_id_b: str,
) -> ToolResult[ArtifactDiff]:
    """Diff two artifacts to see which .ko files and binaries changed.

    Re-hashes the source trees of both artifacts and compares per-file hashes
    to identify changed .ko modules and userspace binaries.

    Args:
        artifact_id_a: artifact_id of the first (base) artifact.
        artifact_id_b: artifact_id of the second (target) artifact.

    Returns:
        ToolResult[ArtifactDiff] with changed_modules and changed_bins lists.
    """
    art_a = _registry.get(artifact_id_a)
    if art_a is None:
        return ToolResult(
            success=False,
            error=ErrorDetail(
                code="ARTIFACT_NOT_FOUND",
                message=f"Artifact '{artifact_id_a}' not found in registry",
                recoverable=False,
            ),
        )

    art_b = _registry.get(artifact_id_b)
    if art_b is None:
        return ToolResult(
            success=False,
            error=ErrorDetail(
                code="ARTIFACT_NOT_FOUND",
                message=f"Artifact '{artifact_id_b}' not found in registry",
                recoverable=False,
            ),
        )

    try:
        diff_result = _registry.diff(artifact_id_a, artifact_id_b)
    except FileNotFoundError as e:
        return ToolResult(
            success=False,
            error=ErrorDetail(
                code="SOURCE_NOT_FOUND",
                message=str(e),
                recoverable=False,
            ),
        )

    if isinstance(diff_result, ToolResult):
        return diff_result

    return ToolResult(success=True, result=diff_result)


@mcp.tool()
@structured_tool_handler
async def artifact_sync(
    cluster_name: str,
    artifact_id: str,
    nodes: list[str] | None = None,
    force: bool = False,
    dest_base: str = "/opt/vmcluster/artifacts",
) -> ToolResult[SyncStatus]:
    """Sync a registered build artifact to cluster nodes via rsync.

    Performs a per-node content-addressed dedup check before transferring.
    Nodes where the artifact directory already exists are skipped unless
    force=True. All nodes are synced concurrently; per-node failures are
    reported in SyncStatus without aborting other nodes.

    Args:
        cluster_name: Name of the topology (cluster) to target.
        artifact_id: artifact_id returned by artifact_register.
        nodes: Optional subset of node names to sync. If None, syncs all nodes.
        force: When True, skip dedup check and always run rsync.
        dest_base: Remote base directory for artifact storage (default:
            /opt/vmcluster/artifacts). The artifact lands at dest_base/artifact_id/.

    Returns:
        ToolResult[SyncStatus] with per_node SyncNodeResult map.
    """
    artifact = _registry.get(artifact_id)
    if artifact is None:
        return ToolResult(
            success=False,
            error=ErrorDetail(
                code="ARTIFACT_NOT_FOUND",
                message=f"Artifact '{artifact_id}' not found in registry",
                recoverable=False,
            ),
        )

    if not os.path.isdir(artifact.source_path):
        return ToolResult(
            success=False,
            error=ErrorDetail(
                code="SOURCE_NOT_FOUND",
                message=f"Artifact source path '{artifact.source_path}' does not exist",
                recoverable=False,
            ),
        )

    topo = load_topology(cluster_name, config.topology_dir)
    topo_node_map = {n.name: n for n in topo.nodes}

    if nodes:
        missing = [name for name in nodes if name not in topo_node_map]
        if missing:
            return ToolResult(
                success=False,
                error=ErrorDetail(
                    code="NODE_NOT_FOUND",
                    message=f"Nodes not found in cluster '{cluster_name}': {', '.join(missing)}",
                    recoverable=False,
                ),
            )
        target_nodes = [topo_node_map[name] for name in nodes]
    else:
        target_nodes = list(topo.nodes)

    sync_status = await _syncer.sync_all(
        nodes=target_nodes,
        ssh_spec=topo.ssh,
        source_path=artifact.source_path,
        dest_base=dest_base,
        artifact_id=artifact_id,
        force=force,
    )
    return ToolResult(success=True, result=sync_status)


_VALID_INSTALL_MODES = {"modprobe_path", "make_install", "rpm", "deb"}


@mcp.tool()
@structured_tool_handler
async def artifact_install(
    cluster_name: str,
    artifact_id: str,
    nodes: list[str] | None = None,
    install_mode: str = "modprobe_path",
    dest_base: str = "/opt/vmcluster/artifacts",
) -> ToolResult[InstallStatus]:
    """Install a synced artifact on cluster nodes using the specified install mode.

    Runs the install commands on all target nodes concurrently. Per-node failures
    are reported in InstallStatus without aborting other nodes (best-effort).

    Args:
        cluster_name: Name of the topology (cluster) to target.
        artifact_id: artifact_id returned by artifact_register.
        nodes: Optional subset of node names to install on. If None, installs on
            all nodes.
        install_mode: One of 'modprobe_path' (default), 'make_install', 'rpm',
            'deb'. Controls the remote install command sequence.
        dest_base: Remote base directory where artifact was synced (default:
            /opt/vmcluster/artifacts). Must match the dest_base used in
            artifact_sync.

    Returns:
        ToolResult[InstallStatus] with per_node InstallNodeResult map.
    """
    if install_mode not in _VALID_INSTALL_MODES:
        return ToolResult(
            success=False,
            error=ErrorDetail(
                code="INVALID_INSTALL_MODE",
                message=(
                    f"Invalid install_mode '{install_mode}'. "
                    f"Must be one of: {', '.join(sorted(_VALID_INSTALL_MODES))}"
                ),
                recoverable=True,
            ),
        )

    artifact = _registry.get(artifact_id)
    if artifact is None:
        return ToolResult(
            success=False,
            error=ErrorDetail(
                code="ARTIFACT_NOT_FOUND",
                message=f"Artifact '{artifact_id}' not found in registry",
                recoverable=False,
            ),
        )

    topo = load_topology(cluster_name, config.topology_dir)
    topo_node_map = {n.name: n for n in topo.nodes}

    if nodes:
        missing = [name for name in nodes if name not in topo_node_map]
        if missing:
            return ToolResult(
                success=False,
                error=ErrorDetail(
                    code="NODE_NOT_FOUND",
                    message=f"Nodes not found in cluster '{cluster_name}': {', '.join(missing)}",
                    recoverable=False,
                ),
            )
        target_nodes = [topo_node_map[name] for name in nodes]
    else:
        target_nodes = list(topo.nodes)

    install_status = await _installer.install_all(
        nodes=target_nodes,
        ssh_spec=topo.ssh,
        artifact=artifact,
        dest_base=dest_base,
        install_mode=install_mode,
    )
    return ToolResult(success=True, result=install_status)
