# Copyright (C) 2026 GoodData Corporation

"""Run registry-defined commands for unified validation.

This is the execution half of the execution-vs-processing split:
- command execution is driven by CommandSpec from the validation registry
- processors handle output normalization separately
"""

from __future__ import annotations

import logging
from pathlib import Path
from typing import TYPE_CHECKING

from aida_mcp.tools.core.validation_registry import CommandSpec, ValidationRegistry, get_validation_registry
from aida_mcp.tools.validate.models import ExecutionResult, ResolvedCommandSpec
from aida_mcp.tools.validate.stream_progress import build_forwarder
from aida_mcp.utils.command_runner import run_command, run_command_streaming

if TYPE_CHECKING:
    from typing import Any

    from mcp.server.fastmcp import Context as MCPContext

    Context = MCPContext[Any, Any, Any]

logger = logging.getLogger(__name__)


def _format_token(token: str, values: dict[str, str]) -> str:
    """Best-effort placeholder expansion (unknown placeholders remain intact)."""
    try:
        return token.format_map(values)
    except KeyError:
        return token
    except Exception:
        return token


def _format_argv(argv: list[str], values: dict[str, str]) -> list[str]:
    return [_format_token(tok, values) for tok in argv]


def _resolve_command(registry: ValidationRegistry, command_id: str) -> CommandSpec | None:
    spec = registry.commands.get(command_id)
    return spec if isinstance(spec, CommandSpec) else None


async def run_registry_command(
    *,
    command_id: str,
    values: dict[str, str],
    workspace_root: Path,
    ctx: Context | None,
    current_step: int | None = None,
    total_steps: int | None = None,
    timeout_sec: int | None = None,
    extra_env: dict[str, str] | None = None,
) -> ExecutionResult:
    resolved = resolve_registry_command(
        command_id=command_id,
        values=values,
        workspace_root=workspace_root,
        timeout_sec=timeout_sec,
        extra_env=extra_env,
    )
    return await run_resolved_command(
        resolved=resolved,
        ctx=ctx,
        current_step=current_step,
        total_steps=total_steps,
    )


def resolve_registry_command(
    *,
    command_id: str,
    values: dict[str, str],
    workspace_root: Path,
    timeout_sec: int | None = None,
    extra_env: dict[str, str] | None = None,
) -> ResolvedCommandSpec:
    registry = get_validation_registry()
    spec = _resolve_command(registry, command_id)
    if spec is None:
        return ResolvedCommandSpec(
            command_id=command_id,
            found=False,
            argv=["<missing command_id>", command_id],
            cwd=workspace_root,
            env={},
            timeout_sec=127,
            streaming=False,
            error=f"command_id not found in registry: {command_id}",
        )

    cmd = _format_argv(spec.argv, values)
    cwd_str = _format_token(spec.cwd, values) if isinstance(spec.cwd, str) and spec.cwd else str(workspace_root)
    env = dict(spec.env)
    if extra_env:
        env.update(extra_env)
    effective_timeout = timeout_sec if timeout_sec is not None else spec.timeout_sec
    timeout = int(effective_timeout) if isinstance(effective_timeout, int) and effective_timeout > 0 else 3600
    return ResolvedCommandSpec(
        command_id=command_id,
        found=True,
        argv=cmd,
        cwd=Path(cwd_str),
        env=env,
        timeout_sec=timeout,
        streaming=spec.streaming,
        error=None,
    )


async def run_resolved_command(
    *,
    resolved: ResolvedCommandSpec,
    ctx: Context | None,
    current_step: int | None = None,
    total_steps: int | None = None,
) -> ExecutionResult:
    if not resolved.found:
        return ExecutionResult(
            argv=resolved.argv,
            cwd=resolved.cwd,
            exit_code=127,
            stdout="",
            stderr=resolved.error or f"command_id not found in registry: {resolved.command_id}",
            log_path=None,
            duration_sec=None,
        )

    if resolved.streaming:
        step_idx = int(current_step) if isinstance(current_step, int) and current_step > 0 else 1
        step_total = int(total_steps) if isinstance(total_steps, int) and total_steps > 0 else 1
        forwarder = build_forwarder(
            ctx=ctx,
            current_step=step_idx,
            total_steps=step_total,
            step_label=resolved.command_id,
            enable_tail_fallback=True,
        )
        result = await run_command_streaming(
            resolved.argv,
            cwd=resolved.cwd,
            timeout=resolved.timeout_sec,
            env=resolved.env or None,
            ctx=ctx,
            step_name=resolved.command_id,
            on_stdout_line=forwarder.on_stdout_line,
            on_stderr_line=forwarder.on_stderr_line,
        )
    else:
        result = await run_command(
            resolved.argv,
            cwd=resolved.cwd,
            timeout=resolved.timeout_sec,
            env=resolved.env or None,
            ctx=ctx,
            step_name=resolved.command_id,
        )

    return ExecutionResult(
        argv=resolved.argv,
        cwd=resolved.cwd,
        exit_code=result.exit_code,
        stdout=result.stdout,
        stderr=result.stderr,
        log_path=result.log_path,
        duration_sec=None,
    )
