"""MCP job lifecycle tools for multi-agent coordination.

Six tools that agents use to start, track, pause/resume, complete,
and query jobs on VMs.  All tools delegate to :mod:`job_store` and
surface errors via ``ToolError`` with structured messages.
"""

from __future__ import annotations

import json
import logging

from fastmcp.exceptions import ToolError
from fastmcp.tools import tool
from pydantic import ValidationError

from mcp_vm_blackbox.job_store import default_job_store
from mcp_vm_blackbox.models import EvidenceAttachment, IssueClassification, StepOutcome
from mcp_vm_blackbox.server import _AGENT_ID
from mcp_vm_blackbox.vm_registry import default_registry

_LOG = logging.getLogger(__name__)


@tool
async def vm_job_start(vm_name: str, scenario_name: str, step: str) -> dict:
    """Start a new job on a VM and return its ID.

    Creates an active job record with an initial step entry.

    Args:
        vm_name: Target VM name.
        scenario_name: Scenario name for this job.
        step: Description of the initial step/action.

    Returns:
        Dict with ``job_id``, ``vm_name``, and ``status``.
    """
    try:
        job_id = await default_job_store.start(vm_name, scenario_name, _AGENT_ID, step)
    except Exception as exc:
        raise ToolError(f"Failed to start job on {vm_name!r}: {exc}") from exc
    return {"job_id": job_id, "vm_name": vm_name, "status": "active"}


@tool
async def vm_job_step(
    job_id: str,
    vm_name: str,
    action: str,
    outcome: str = "",
    evidence: str = "",
    intent: str = "",
    reason: str = "",
    outcome_status: str = "",
    outcome_detail: str = "",
    issue_classification: str = "",
    resolution: str = "",
    next: str = "",  # noqa: A002
) -> dict:
    """Append a step to an existing job using the declare-execute-reflect pattern.

    Args:
        job_id: Job identifier.
        vm_name: VM name.
        action: Description of the action performed.
        outcome: Optional free-text outcome (legacy field, default empty).
        evidence: Optional JSON-encoded list of evidence attachment dicts.
            Each entry must match the ``EvidenceAttachment`` schema.
            Pass an empty string (default) to omit evidence for this step.
        intent: What the agent intends to do (declare phase).
        reason: Why this action is being taken (declare phase).
        outcome_status: Structured outcome: ``success``, ``failed``, ``blocked``,
            or ``skipped``.  Must match :class:`~mcp_vm_blackbox.models.StepOutcome`
            values.  Pass empty string to omit.
        outcome_detail: Detailed explanation of what happened (reflect phase).
        issue_classification: Issue classification if a problem was detected.
            Must match :class:`~mcp_vm_blackbox.models.IssueClassification` values
            (``user-facing``, ``harness``, ``environment``).  Pass empty string to
            omit.
        resolution: What was done about the issue, if any (reflect phase).
        next: What comes next after this step (reflect phase).

    Returns:
        Dict confirmation with ``job_id``, ``action``, ``status``,
        ``outcome_status``, and ``issue_classification``.

    Raises:
        ToolError: If ``evidence`` is non-empty but cannot be parsed as JSON or
            fails ``EvidenceAttachment`` validation, or if ``outcome_status`` /
            ``issue_classification`` contain an unrecognised enum value.
    """
    # --- Validate enum strings before touching the store ---
    validated_outcome_status: StepOutcome | None = None
    if outcome_status:
        try:
            validated_outcome_status = StepOutcome(outcome_status)
        except ValueError:
            valid = ", ".join(f"{v!r}" for v in StepOutcome)
            raise ToolError(
                f"Invalid outcome_status {outcome_status!r} for job {job_id!r}. Valid values: {valid}"
            ) from None

    validated_issue_classification: IssueClassification | None = None
    if issue_classification:
        try:
            validated_issue_classification = IssueClassification(issue_classification)
        except ValueError:
            valid = ", ".join(f"{v!r}" for v in IssueClassification)
            raise ToolError(
                f"Invalid issue_classification {issue_classification!r} for job {job_id!r}. Valid values: {valid}"
            ) from None

    # --- Parse and validate evidence JSON ---
    validated_evidence: list[EvidenceAttachment] | None = None
    if evidence:
        try:
            raw = json.loads(evidence)
        except json.JSONDecodeError as exc:
            raise ToolError(f"Invalid JSON in evidence parameter for job {job_id!r}: {exc}") from exc
        if not isinstance(raw, list):
            raise ToolError(f"evidence must be a JSON array, got {type(raw).__name__!r} for job {job_id!r}")
        try:
            validated_evidence = [EvidenceAttachment.model_validate(item) for item in raw]
        except ValidationError as exc:
            raise ToolError(f"Evidence validation failed for job {job_id!r}: {exc}") from exc

    try:
        await default_job_store.append_step(
            job_id,
            vm_name,
            _AGENT_ID,
            action,
            outcome=outcome or None,
            evidence=validated_evidence,
            intent=intent or None,
            reason=reason or None,
            outcome_status=validated_outcome_status,
            outcome_detail=outcome_detail or None,
            issue_classification=validated_issue_classification,
            resolution=resolution or None,
            next=next or None,
        )
    except FileNotFoundError as exc:
        raise ToolError(f"Job {job_id!r} not found on VM {vm_name!r}") from exc
    except Exception as exc:
        raise ToolError(f"Failed to append step to job {job_id!r}: {exc}") from exc
    return {
        "job_id": job_id,
        "action": action,
        "status": "step_appended",
        "outcome_status": outcome_status or None,
        "issue_classification": issue_classification or None,
    }


@tool
async def vm_job_pause(job_id: str, vm_name: str) -> dict:
    """Pause an active job.

    Args:
        job_id: Job identifier.
        vm_name: VM name.

    Returns:
        Dict confirmation with ``job_id`` and ``status``.
    """
    try:
        await default_job_store.set_status(job_id, vm_name, "paused")
    except FileNotFoundError as exc:
        raise ToolError(f"Job {job_id!r} not found on VM {vm_name!r}") from exc
    except ValueError as exc:
        raise ToolError(f"Cannot pause job {job_id!r}: {exc}") from exc
    except Exception as exc:
        raise ToolError(f"Failed to pause job {job_id!r}: {exc}") from exc
    return {"job_id": job_id, "vm_name": vm_name, "status": "paused"}


@tool
async def vm_job_resume(job_id: str, vm_name: str) -> dict:
    """Resume a paused job.

    Args:
        job_id: Job identifier.
        vm_name: VM name.

    Returns:
        Dict confirmation with ``job_id`` and ``status``.
    """
    try:
        await default_job_store.set_status(job_id, vm_name, "active")
    except FileNotFoundError as exc:
        raise ToolError(f"Job {job_id!r} not found on VM {vm_name!r}") from exc
    except ValueError as exc:
        raise ToolError(f"Cannot resume job {job_id!r}: {exc}") from exc
    except Exception as exc:
        raise ToolError(f"Failed to resume job {job_id!r}: {exc}") from exc
    return {"job_id": job_id, "vm_name": vm_name, "status": "active"}


@tool
async def vm_job_complete(job_id: str, vm_name: str) -> dict:
    """Mark a job as completed and return a video generation hint.

    After setting the job status to ``"completed"``, the tool checks the VM
    registry for an active or recent recording.  The ``video_hint`` field in
    the response tells the orchestrator whether a summary video can be
    generated and which tool to call.

    Args:
        job_id: Job identifier.
        vm_name: VM name.

    Returns:
        Dict with ``job_id``, ``vm_name``, ``status``, and ``video_hint``.
        ``video_hint`` contains:

        - ``ready`` (bool): ``True`` when a recording path is available.
        - ``suggested_tool`` (str): ``"vm_build_summary_video"``.
        - ``evidence_root`` (str): Default evidence root directory.
        - ``recording_path`` (str | None): Path to the WebM recording, or
          ``None`` when no recording was found in the registry.
    """
    try:
        await default_job_store.set_status(job_id, vm_name, "completed")
    except FileNotFoundError as exc:
        raise ToolError(f"Job {job_id!r} not found on VM {vm_name!r}") from exc
    except ValueError as exc:
        raise ToolError(f"Cannot complete job {job_id!r}: {exc}") from exc
    except Exception as exc:
        raise ToolError(f"Failed to complete job {job_id!r}: {exc}") from exc

    registry_entry = await default_registry.get(vm_name)
    recording_path: str | None = None
    if registry_entry is not None:
        recording_path = registry_entry.recording_path

    video_hint: dict = {
        "ready": recording_path is not None,
        "suggested_tool": "vm_build_summary_video",
        "evidence_root": ".mcp-vm-blackbox/evidence",
        "recording_path": recording_path,
    }
    return {"job_id": job_id, "vm_name": vm_name, "status": "completed", "video_hint": video_hint}


@tool
async def vm_job_list(vm_name: str = "", status: str = "") -> list[dict]:
    """List jobs, optionally filtered by VM name and/or status.

    Args:
        vm_name: Filter by VM name (optional).
        status: Filter by status (optional).

    Returns:
        List of job summary dicts with ``job_id``, ``vm_name``,
        ``scenario_name``, ``status``, and ``step_count``.
    """
    try:
        if vm_name and status:
            all_for_vm = await default_job_store.list_by_vm(vm_name)
            records = [r for r in all_for_vm if r.status == status]
        elif vm_name:
            records = await default_job_store.list_by_vm(vm_name)
        elif status:
            records = await default_job_store.list_by_status(status)
        else:
            records = await default_job_store.list_all()
    except Exception as exc:
        raise ToolError(f"Failed to list jobs: {exc}") from exc

    return [
        {
            "job_id": r.job_id,
            "vm_name": r.vm_name,
            "scenario_name": r.scenario_name,
            "status": r.status,
            "step_count": len(r.steps),
        }
        for r in records
    ]
