"""Pydantic models for MCP tool results and validated data structures."""

from __future__ import annotations

from enum import StrEnum
from typing import Literal, TypeAlias

from pydantic import BaseModel, Field, model_validator

#: Reusable type for tri-state check outcomes used by preflight results.
OverallStatus: TypeAlias = Literal["pass", "fail", "warn"]

#: Job lifecycle status persisted on :class:`JobRecord` and coordination DB.
JobStatus: TypeAlias = Literal["active", "paused", "completed", "failed"]

#: Goal state draft vs committed (see :class:`GoalState`).
GoalStateLifecycle: TypeAlias = Literal["draft", "committed"]

#: Remote execution protocol for goal-state host roster entries.
HostProtocol: TypeAlias = Literal["WinRM", "SSH", "local"]

#: Criterion kinds for goal-state verification (:class:`Criterion`).
CriterionKind: TypeAlias = Literal[
    "port_open", "service_running", "db_connect", "process_present", "env_var", "app_version", "config_path"
]

#: Maximum character length for inline evidence content (shell_output).
EVIDENCE_CONTENT_MAX_CHARS: int = 4096

# ---------------------------------------------------------------------------
# VM discovery and inspection
# ---------------------------------------------------------------------------


class VmListEntry(BaseModel):
    """A single VM entry from VBoxManage list vms."""

    name: str = Field(description="VirtualBox VM name")
    uuid: str = Field(description="VM UUID")
    running: bool = Field(description="Whether the VM is currently running")


class VmInfoResult(BaseModel):
    """VM hardware and state info (memory, CPU, VRAM, guest additions)."""

    memory_mb: str | None = Field(default=None, description="Memory in MB")
    cpus: str | None = Field(default=None, description="CPU count")
    vram_mb: str | None = Field(default=None, description="VRAM in MB")
    guest_additions: str | None = Field(default=None, description="Guest Additions version")
    state: str | None = Field(default=None, description="VM state")
    error: str | None = Field(default=None, description="Error message when VM not found")


class ScreenshotResult(BaseModel):
    """Result of a screenshot operation (vm_screenshot_api)."""

    saved_to: str = Field(description="Path where PNG was saved")
    width: int = Field(description="Image width in pixels")
    height: int = Field(description="Image height in pixels")
    error: str | None = Field(default=None, description="Error message on failure")


class ScreenshotUnchangedResult(BaseModel):
    """Result when the VM screen is unchanged from the recent baseline.

    Returned by the dedup layer when the perceptual hash of the current
    frame matches the stored hash, eliminating redundant image tokens from
    pilot context.
    """

    message: str = Field(description="Human-readable explanation of why no image was returned")
    last_capture_seconds_ago: float = Field(
        description="Seconds elapsed since the last distinct screenshot was captured"
    )
    vm_name: str = Field(description="Name of the VM whose screen was checked")
    provider: str = Field(default="dedup_layer", description="Layer responsible for producing this result")


# ---------------------------------------------------------------------------
# Viewport action (Phase 2 — vm_viewport_action)
# ---------------------------------------------------------------------------


class ElementType(StrEnum):
    """Visible UI element type identified in a VM screen analysis."""

    BUTTON = "button"
    FIELD = "field"
    CHECKBOX = "checkbox"
    DROPDOWN = "dropdown"
    LABEL = "label"
    LINK = "link"
    ICON = "icon"
    OTHER = "other"


class ElementCoordinates(BaseModel):
    """Screen coordinates of a visible UI element."""

    x: int = Field(description="Horizontal pixel coordinate")
    y: int = Field(description="Vertical pixel coordinate")


class VisibleElement(BaseModel):
    """A single visible UI element identified in the current screen state."""

    name: str = Field(description="Human-readable label for the element")
    element_type: ElementType = Field(description="Category of UI element")
    coordinates: ElementCoordinates = Field(description="Screen coordinates of the element centre")
    notes: str = Field(default="", description="Optional extra detail about the element")


class ScreenState(BaseModel):
    """Structured description of the current VM screen state."""

    description: str = Field(description="Natural-language summary of what is visible on screen")
    visible_elements: list[VisibleElement] = Field(
        default_factory=list, description="Structured list of interactive elements detected on screen"
    )
    warnings: list[str] = Field(
        default_factory=list, description="Non-fatal observations about screen state (e.g. low resolution)"
    )
    errors: list[str] = Field(default_factory=list, description="Error conditions that prevented full screen analysis")


class ViewportResponse(BaseModel):
    """Response returned by vm_viewport_action after analysing the VM screen."""

    action_taken: str = Field(description="Description of the action that was performed (or attempted)")
    screen_state: ScreenState = Field(description="Structured analysis of the VM screen at the time of the action")
    provider: str = Field(
        description=(
            "Vision provider that produced this response: "
            "'claude', 'openrouter', 'inline_fallback', or 'inline_after_parse_failure'"
        )
    )
    scale: float | None = Field(
        default=None,
        description=(
            "Uniform scale factor applied during JPEG downscale. "
            "Divide a JPEG pixel coordinate by this value to get the native VM coordinate. "
            "None when the screenshot was not downscaled."
        ),
    )
    marks_legend: SoMMarksLegend | None = Field(
        default=None,
        description=(
            "Set-of-Marks legend for the screenshot delivered to the micro-pilot. "
            "None when SoM was not applied or element detection yielded no marks."
        ),
    )


# ---------------------------------------------------------------------------
# Set-of-Marks (SoM) overlay models
# ---------------------------------------------------------------------------


class ScaleInfo(BaseModel):
    """Scale metadata returned alongside a JPEG-downscaled screenshot.

    Records the ratio between the original VM frame dimensions and the
    downscaled output dimensions so callers can convert JPEG-space
    coordinates back to native VM coordinates.
    """

    scale_x: float = Field(description="Horizontal scale factor: output_width / native_width")
    scale_y: float = Field(description="Vertical scale factor: output_height / native_height")
    native_width: int = Field(description="Original screenshot width in pixels")
    native_height: int = Field(description="Original screenshot height in pixels")
    output_width: int = Field(description="Downscaled output width in pixels")
    output_height: int = Field(description="Downscaled output height in pixels")


class MarkEntry(BaseModel):
    """A single numbered mark in a Set-of-Marks legend.

    Coordinates are always stored in native VM resolution (pre-downscale)
    so they can be passed directly to vm_mouse_click without adjustment.
    """

    mark_id: int = Field(ge=1, description="1-based sequential mark identifier")
    name: str = Field(description="UI element name as reported by UIAutomation")
    control_type: str = Field(description="UI Automation control type string (e.g. 'Button', 'Edit')")
    native_x: int = Field(description="Native-resolution X coordinate of the element centre")
    native_y: int = Field(description="Native-resolution Y coordinate of the element centre")
    bounding_rect: tuple[int, int, int, int] = Field(
        description="Bounding rectangle in native resolution: (left, top, right, bottom)"
    )


class SoMMarksLegend(BaseModel):
    """Legend of all numbered marks stamped on a Set-of-Marks screenshot.

    One legend is produced per SoM-stamped screenshot and cached in the
    module-level marks cache keyed by vm_name. The pilot uses it to
    resolve mark IDs to native coordinates for vm_mouse_click.
    """

    vm_name: str = Field(description="VM whose screenshot was annotated")
    marks: list[MarkEntry] = Field(
        default_factory=list, description="Ordered list of marks (mark_id 1..N, up to SOM_MAX_ELEMENTS)"
    )
    detection_method: str = Field(
        default="uiautomation", description="Method used to detect UI elements (always 'uiautomation')"
    )
    timestamp: str = Field(description="ISO 8601 timestamp when the legend was created")


class VisionProvider(StrEnum):
    """Vision provider used to analyse the VM screen for vm_viewport_action."""

    CLAUDE_CLI = "claude"
    OPENROUTER = "openrouter"
    INLINE_FALLBACK = "inline_fallback"
    INLINE_AFTER_PARSE_FAILURE = "inline_after_parse_failure"


# ---------------------------------------------------------------------------
# Build orchestration
# ---------------------------------------------------------------------------


class WatchResult(BaseModel):
    """Result of a log-watch (build_watch or similar) operation."""

    lines_seen: int = Field(description="Total number of new lines accumulated")
    new_lines: list[str] = Field(default_factory=list, description="Last 10 lines seen (for quick inspection)")
    matched: str | None = Field(default=None, description="The break-on pattern that matched, if any")
    matched_line: str | None = Field(default=None, description="The raw log line containing the match")
    elapsed_s: float = Field(description="Elapsed seconds since watch started")
    reason: str = Field(description="Termination reason: 'pattern_match' or 'timeout'")


class BuildStartResult(BaseModel):
    """Result of build_start. Either success (pid, log_path, message) or error."""

    pid: str | None = Field(default=None, description="Build process PID")
    log_path: str | None = Field(default=None, description="Path to build log")
    message: str | None = Field(default=None, description="Success message")
    error: str | None = Field(default=None, description="Error message on failure")


class BuildStatusResult(BaseModel):
    """Result of build_status check."""

    running: bool = Field(description="Whether build appears still running")
    last_lines: list[str] = Field(description="Last N log lines")
    log_path: str = Field(description="Path to the log file")


# ---------------------------------------------------------------------------
# CI tools
# ---------------------------------------------------------------------------


class CiCheckResult(BaseModel):
    """Result of ci_check (SSH connectivity and host stats)."""

    connected: bool = Field(description="Whether SSH connection succeeded")
    disk: str | None = Field(default=None, description="df -h / output")
    memory: str | None = Field(default=None, description="free -m output")
    uptime: str | None = Field(default=None, description="uptime output")
    note: str | None = Field(default=None, description="Informational note")
    error: str | None = Field(default=None, description="Error message on failure")
    vboxmanage: str | None = Field(default=None, description="VBoxManage path (local target only)")


class CommandResult(BaseModel):
    """Result of a command execution (ci_run, podman_exec, vm_powershell)."""

    stdout: str = Field(description="Standard output")
    stderr: str = Field(description="Standard error")
    exit_code: int = Field(description="Process exit code")
    error: str | None = Field(default=None, description="Error message (e.g. timeout)")


class CiPipelineStatusResult(BaseModel):
    """Result of ci_pipeline_status."""

    raw_output: str = Field(description="Formatted pipeline status output (ID, status, ref, etc.)")
    exit_code: int = Field(description="Zero on success, non-zero when GitLab API call fails")
    error: str | None = Field(default=None, description="Error message when the API call fails")


class CiPreflightResult(BaseModel):
    """Result of ci_preflight (tool versions and KVM module state)."""

    versions: dict[str, str] = Field(description="Tool name -> installed version string")
    kvm_modules_unloaded: bool = Field(description="True when no KVM modules are loaded")
    kvm_auto_unloaded: bool = Field(
        default=False,
        description=(
            "True when ci_preflight detected kvm_intel/kvm_amd at refcount=0 and "
            "successfully auto-unloaded the modules for VirtualBox compatibility."
        ),
    )
    kvm_conflict: str | None = Field(
        default=None,
        description=(
            "Actionable conflict message when a KVM vendor module (kvm_intel/kvm_amd) "
            "is loaded and blocking VirtualBox. None when no conflict exists."
        ),
    )
    issues: list[str] = Field(default_factory=list, description="List of detected issues")


# ---------------------------------------------------------------------------
# Podman tools
# ---------------------------------------------------------------------------


class PodmanContainer(BaseModel):
    """A single Podman container record from podman ps."""

    id: str = Field(description="Container ID")
    names: list[str] = Field(default_factory=list, description="Container names")
    status: str = Field(description="Container status (running, exited, etc.)")
    ports: list[str] = Field(default_factory=list, description="Port mappings")
    image: str = Field(description="Image name")


class PodmanPsError(BaseModel):
    """Error item returned by podman_ps on failure."""

    error: str = Field(description="Error message")


class PodmanLogsResult(BaseModel):
    """Result of podman_logs."""

    lines: list[str] = Field(description="Log lines")
    container: str = Field(description="Container name or ID")
    tail: int = Field(description="Number of lines requested")
    error: str | None = Field(default=None, description="Error message on failure")


class PodmanRestartResult(BaseModel):
    """Result of podman_restart."""

    container: str = Field(description="Container name or ID")
    success: bool = Field(description="Whether restart succeeded")
    error: str | None = Field(default=None, description="Error message on failure")


class PodmanServiceStatusResult(BaseModel):
    """Result of podman_service_status."""

    active: bool = Field(description="Whether service is active")
    status_output: str = Field(description="systemctl status output")
    service: str = Field(description="Service name")


# ---------------------------------------------------------------------------
# Vagrant tools
# ---------------------------------------------------------------------------


class PreflightCheck(BaseModel):
    """A single preflight check result."""

    name: str = Field(description="Check name")
    status: Literal["pass", "fail", "warn", "skip"] = Field(description="Check outcome")
    message: str = Field(description="Human-readable outcome summary")
    detail: str | None = Field(default=None, description="Extra detail or raw output")


class VagrantPreflightResult(BaseModel):
    """Result of vagrant_preflight — Vagrantfile readiness assessment."""

    vagrantfile_path: str | None = Field(default=None, description="Absolute path to the located Vagrantfile")
    overall: OverallStatus = Field(description="'fail' if any check failed, 'warn' if any warned, else 'pass'")
    checks: list[PreflightCheck] = Field(default_factory=list, description="Individual check results")
    vm_names: list[str] = Field(default_factory=list, description="vb.name values found in the Vagrantfile")
    vagrant_version: str | None = Field(default=None, description="Detected vagrant CLI version string")
    vboxmanage_version: str | None = Field(default=None, description="Detected VBoxManage version string")
    error: str | None = Field(default=None, description="Top-level error (e.g. Vagrantfile not found)")


class VagrantDetachedResult(BaseModel):
    """Result of launching a detached vagrant session."""

    detached: bool = Field(description="Always True when successful")
    session_name: str = Field(description="tmux session name")
    log_path: str = Field(description="Path to the log file")
    tail_command: str = Field(description="Command to tail the log")
    attach_command: str = Field(description="Command to attach to tmux session")
    kill_command: str = Field(description="Command to kill the tmux session")


class VagrantCommandResult(BaseModel):
    """Result of vagrant status, up, provision, destroy, winrm."""

    stdout: str | None = Field(default=None, description="Standard output")
    stderr: str | None = Field(default=None, description="Standard error")
    exit_code: int = Field(description="Process exit code")
    duration_s: float = Field(description="Elapsed seconds")
    error: str | None = Field(default=None, description="Error message (e.g. timeout)")


# ---------------------------------------------------------------------------
# VM interaction (keyboard, mouse, PowerShell)
# ---------------------------------------------------------------------------


class VmTypeResult(BaseModel):
    """Result of vm_type (keyboard emulation)."""

    status: str | None = Field(default=None, description="'ok' on success")
    chars_sent: int | None = Field(default=None, description="Characters typed")
    error: str | None = Field(default=None, description="Error message on failure")


class VmKeyResult(BaseModel):
    """Result of vm_key (special key press)."""

    status: str | None = Field(default=None, description="'ok' on success")
    key_sent: str | None = Field(default=None, description="Key that was sent")
    error: str | None = Field(default=None, description="Error message on failure")


class VmKeyComboResult(BaseModel):
    """Result of vm_key_combo (modifier+key combo)."""

    status: str = Field(default="ok", description="'ok' on success")
    combo_parsed: str = Field(description="Normalized combo string that was sent")
    scancodes_sent: list[str] = Field(default_factory=list, description="Scancode sequence sent to VM")
    error: str | None = Field(default=None, description="Error message on failure")


class VmMouseClickResult(BaseModel):
    """Result of vm_mouse_click."""

    status: str = Field(description="'ok' on success")
    x: int = Field(description="X coordinate used for the click (native VM resolution)")
    y: int = Field(description="Y coordinate used for the click (native VM resolution)")
    button: str = Field(description="Button pressed (left, right, middle)")
    scale_applied: float | None = Field(
        default=None,
        description=(
            "Scale factor applied when converting JPEG coordinates to native coordinates. "
            "None when coordinates were used as-is (no scaling)."
        ),
    )
    mark_id_resolved: int | None = Field(
        default=None,
        description=(
            "Mark ID from the SoM legend that was resolved to native coordinates. None when mark_id was not provided."
        ),
    )


class VmStartResult(BaseModel):
    """Result of vm_start."""

    status: str = Field(description="'ok' on success")
    vm_name: str = Field(description="VM name that was started")
    mode: str = Field(description="Start mode: 'headless' or 'gui'")
    error: str | None = Field(default=None, description="Error message on failure")
    recording_started: bool = Field(default=False, description="Whether always-on recording was started")
    recording_path: str | None = Field(default=None, description="Path to the recording file")
    recording_warning: str | None = Field(default=None, description="Warning if recording failed to start")


class VmStopResult(BaseModel):
    """Result of vm_stop."""

    status: str = Field(description="'ok' on success")
    vm_name: str = Field(description="VM name that was stopped")
    mode: str = Field(description="Stop mode: 'acpi', 'poweroff', or 'savestate'")
    error: str | None = Field(default=None, description="Error message on failure")
    recording_stopped: bool = Field(
        default=False, description="Whether always-on recording was stopped before shutdown"
    )
    recording_path: str | None = Field(default=None, description="Path to the finalized recording file")


# ---------------------------------------------------------------------------
# Goal state and verification
# ---------------------------------------------------------------------------


class HostRosterEntry(BaseModel):
    """A single entry in the host roster for a goal state scenario."""

    name: str = Field(description="Logical host name (user-assigned label)")
    ip: str = Field(description="IP address or hostname")
    protocol: HostProtocol = Field(description="Remote execution protocol")
    port: int = Field(default=5985, description="Connection port (WinRM: 5985, SSH: 22)")
    username: str = Field(default="vagrant", description="Remote execution username")
    identity_file: str | None = Field(default=None, description="SSH identity file path (SSH protocol only)")


class Criterion(BaseModel):
    """A single verifiable criterion within a goal state."""

    criterion_id: str = Field(description="UUID for this criterion")
    host_name: str = Field(description="References HostRosterEntry.name")
    criterion_type: CriterionKind = Field(description="Type of verification check")
    params: dict[str, str] = Field(default_factory=dict, description="Type-specific parameters")


class GoalState(BaseModel):
    """Goal state definition for a VM scenario."""

    vm_name: str = Field(description="Target VM name (composite key part 1)")
    scenario_name: str = Field(description="Scenario name (composite key part 2)")
    status: GoalStateLifecycle = Field(default="committed")
    fail_fast: bool = Field(default=False)
    hosts: list[HostRosterEntry] = Field(default_factory=list)
    criteria: list[Criterion] = Field(default_factory=list)
    ticket_refs: list[str] = Field(default_factory=list)
    prior_post_mortem_path: str | None = Field(default=None)


class CriterionResult(BaseModel):
    """Result of verifying a single criterion."""

    criterion_id: str = Field(description="UUID of the criterion that was checked")
    criterion_type: str = Field(description="Type of verification check performed")
    host: str = Field(description="Host name where the check was performed")
    expected: str = Field(description="Expected value or condition")
    actual: str = Field(description="Actual observed value or condition")
    passed: bool = Field(description="Whether the criterion passed")
    stdout: str = Field(default="", description="Standard output from the check")
    stderr: str = Field(default="", description="Standard error from the check")
    exit_code: int = Field(default=0, description="Process exit code from the check")


class VerificationReport(BaseModel):
    """Report of all criterion results for a goal state verification run."""

    vm_name: str = Field(description="Target VM name")
    scenario_name: str = Field(description="Scenario name")
    passed: bool = Field(description="Whether all criteria passed")
    results: list[CriterionResult] = Field(description="Individual criterion results")
    fail_fast_triggered: bool = Field(default=False, description="Whether fail_fast halted verification early")


class PostMortem(BaseModel):
    """Post-mortem record written after a verification run."""

    vm_name: str = Field(description="Target VM name")
    scenario_name: str = Field(description="Scenario name")
    timestamp: str = Field(description="ISO 8601 timestamp of the run end")
    steps: list[dict[str, str | None]] = Field(default_factory=list, description="Steps executed with outcomes")
    verification_report: VerificationReport | None = Field(default=None)
    post_mortem_path: str = Field(description="Absolute path to the written .md file")


# ---------------------------------------------------------------------------
# Infrastructure provisioning (INFRA-01/02/03)
# ---------------------------------------------------------------------------


class InfraState(BaseModel):
    """Persisted infrastructure state for a scenario (never committed)."""

    scenario_key: str = Field(description="Composite key vm_name/scenario_name")
    db_endpoint: str = Field(description="PostgreSQL host:port")
    db_name: str = Field(description="Database name")
    db_user: str = Field(description="Database user")
    db_password: str = Field(description="Database password (local infra only)")
    network_config: dict[str, str] = Field(default_factory=dict, description="Additional network configuration")
    provisioned_at: str = Field(description="ISO 8601 timestamp when provisioned")


class InfraProvisionResult(BaseModel):
    """Result of infra_provision_postgres."""

    scenario_key: str = Field(description="Composite key vm_name/scenario_name")
    db_endpoint: str = Field(description="host:port of the provisioned database")
    db_name: str = Field(description="Database name")
    db_user: str = Field(description="Database user")
    connected: bool = Field(description="Whether connectivity was verified")
    state_path: str = Field(description="Path where infra state was persisted")
    error: str | None = Field(default=None, description="Error message on failure")


class InfraDestroyResult(BaseModel):
    """Result of infra_destroy_postgres."""

    scenario_key: str = Field(description="Composite key vm_name/scenario_name")
    destroyed: bool = Field(description="Whether the database was removed")
    state_path: str | None = Field(default=None, description="Path of the removed state file")
    error: str | None = Field(default=None, description="Error message on failure")


class InfraStatusResult(BaseModel):
    """Result of infra_status."""

    scenario_key: str = Field(description="Composite key vm_name/scenario_name")
    provisioned: bool = Field(description="Whether infra state exists on disk")
    connected: bool | None = Field(default=None, description="Connectivity check result (None if not provisioned)")
    db_endpoint: str | None = Field(default=None, description="host:port if provisioned")
    state_path: str | None = Field(default=None, description="Path to the infra state file")
    error: str | None = Field(default=None, description="Error message on failure")


# ---------------------------------------------------------------------------
# VM registry
# ---------------------------------------------------------------------------


class VMRegistryEntry(BaseModel):
    """A single VM entry persisted in the VM registry store."""

    vm_name: str = Field(description="VirtualBox VM name")
    uuid: str = Field(description="VM UUID")
    running: bool = Field(description="Whether the VM was running at last refresh")
    last_refreshed: str = Field(description="ISO 8601 timestamp of last refresh")
    recording: bool = Field(default=False, description="Whether recording is active")
    recording_path: str | None = Field(default=None, description="Path to current/last WebM")
    recording_started_at: str | None = Field(default=None, description="ISO 8601 recording start time")


# ---------------------------------------------------------------------------
# Handoff coordination
# ---------------------------------------------------------------------------


class HandoffReference(BaseModel):
    """A structured handoff record written by a pilot before pausing.

    Captures the pilot's working state at context boundary so the incoming
    pilot can resume efficiently without replaying the full step log.
    """

    handoff_id: str = Field(description="UUID4 handoff identifier")
    job_id: str = Field(description="Job identifier (FK to jobs)")
    vm_name: str = Field(description="VM name (FK to jobs)")
    todo_position: str = Field(description="TODO item or step label the pilot was working on at handoff time")
    last_completed_step_id: int | None = Field(
        default=None, description="step_id of the last successfully completed step"
    )
    next_intended_action: str = Field(description="Specific action the pilot intended to take next")
    key_observations: str = Field(description="Critical facts the incoming pilot must know (free-text, multi-line)")
    full_log_path: str = Field(description="Path to the evidence/log directory for this job")
    created_at: str = Field(description="ISO 8601 UTC timestamp of handoff creation")


# ---------------------------------------------------------------------------
# Issue register
# ---------------------------------------------------------------------------


class IssueRecord(BaseModel):
    """A structured issue entry in the per-job issue register."""

    issue_id: str = Field(description="UUID4 issue identifier")
    job_id: str = Field(description="Job identifier (FK to jobs)")
    vm_name: str = Field(description="VM name (FK to jobs)")
    classification: IssueClassification = Field(
        description="Issue classification: user-facing, harness, or environment"
    )
    severity: IssueSeverity = Field(description="Issue severity: blocker, major, minor, or observation")
    summary: str = Field(description="One-line summary of the issue")
    detail: str | None = Field(default=None, description="Detailed description of the issue")
    resolution: str | None = Field(default=None, description="What was done to resolve the issue")
    resolution_type: ResolutionType | None = Field(
        default=None, description="Resolution category: 'fixed', 'workaround', 'escalated', or 'deferred'"
    )
    evidence_paths: list[str] = Field(
        default_factory=list, description="Paths to evidence files (screenshots, logs, etc.)"
    )
    step_id: int | None = Field(
        default=None, description="Originating step_id from job_steps (nullable — issues may span steps)"
    )
    created_at: str = Field(description="ISO 8601 UTC timestamp of issue creation")
    resolved_at: str | None = Field(default=None, description="ISO 8601 UTC timestamp when issue was resolved")


# ---------------------------------------------------------------------------
# Steering coordination
# ---------------------------------------------------------------------------


class SteeringPriority(StrEnum):
    """Priority level for a steering message."""

    NORMAL = "normal"
    URGENT = "urgent"


class SteeringSender(StrEnum):
    """Source actor that sent the steering message."""

    ORCHESTRATOR = "orchestrator"
    USER = "user"
    SCHEDULED_TASK = "scheduled-task"


class SteeringDelivery(StrEnum):
    """Delivery mechanism for a steering message."""

    CHANNEL = "channel"
    HOOK = "hook"


class SteeringMessage(BaseModel):
    """A steering instruction queued for delivery to a running pilot."""

    message_id: str = Field(description="UUID4 message identifier")
    target_pilot: str | None = Field(
        default=None, description="Target pilot agent ID (null = broadcast to all pilots on the job)"
    )
    job_id: str = Field(description="Job identifier (FK to jobs)")
    vm_name: str = Field(description="VM name (FK to jobs)")
    sender: SteeringSender = Field(description="Source actor: orchestrator, user, or scheduled-task")
    content: str = Field(description="Steering instruction text")
    priority: SteeringPriority = Field(description="Message priority: normal or urgent")
    created_at: str = Field(description="ISO 8601 UTC timestamp of message creation")
    acknowledged_at: str | None = Field(
        default=None, description="ISO 8601 UTC timestamp when pilot acknowledged the message"
    )
    acted_on: bool = Field(default=False, description="Whether the pilot has acted on this message")
    delivery: SteeringDelivery = Field(description="Delivery mechanism: channel (push) or hook (poll)")
    channel_meta: str | None = Field(
        default=None, description="Delivery-specific metadata (e.g. channel ID for push delivery)"
    )


# ---------------------------------------------------------------------------
# Job coordination
# ---------------------------------------------------------------------------


class JobSummary(BaseModel):
    """Compact job summary for fleet status responses."""

    job_id: str = Field(description="Unique job identifier")
    scenario_name: str = Field(description="Scenario name")
    status: JobStatus = Field(description="Current job status")
    last_agent_id: str = Field(description="Last agent that touched the job")
    last_step: str | None = Field(default=None, description="Description of the last step, or None if no steps")
    updated_at: str = Field(description="ISO 8601 timestamp of last update")
    pilot_context_usage: float | None = Field(
        default=None, description="Fraction of pilot context window consumed (0.0-1.0), or null if unknown"
    )


class FleetEntry(BaseModel):
    """A single VM with its running state and associated jobs."""

    vm_name: str = Field(description="VirtualBox VM name")
    uuid: str = Field(description="VM UUID")
    running: bool = Field(description="Whether the VM is currently running")
    recording: bool = Field(default=False, description="Whether recording is active")
    recording_path: str | None = Field(default=None, description="Path to current/last WebM")
    recording_started_at: str | None = Field(default=None, description="ISO 8601 recording start time")
    jobs: list[JobSummary] = Field(default_factory=list, description="Jobs associated with this VM")


class StepOutcome(StrEnum):
    """Structured outcome status for a job step."""

    SUCCESS = "success"
    FAILED = "failed"
    BLOCKED = "blocked"
    SKIPPED = "skipped"


class IssueClassification(StrEnum):
    """Classification of an issue detected during a step."""

    USER_FACING = "user-facing"
    HARNESS = "harness"
    ENVIRONMENT = "environment"


class IssueSeverity(StrEnum):
    """Severity rating for an issue in the issue register."""

    BLOCKER = "blocker"
    MAJOR = "major"
    MINOR = "minor"
    OBSERVATION = "observation"


class ResolutionType(StrEnum):
    """Resolution category applied when an issue is resolved or deferred."""

    FIXED = "fixed"
    WORKAROUND = "workaround"
    ESCALATED = "escalated"
    DEFERRED = "deferred"


class EvidenceAttachment(BaseModel):
    """A single evidence attachment on a job step."""

    type: Literal["screenshot", "terminal", "shell_output", "tui_capture", "recording_timestamp"] = Field(
        description="Evidence type"
    )
    path: str | None = Field(
        default=None, description="Relative path to evidence file (for screenshot, terminal, tui_capture)"
    )
    content: str | None = Field(default=None, description="Inline content (for shell_output -- max 4096 chars)")
    metadata: dict[str, str] = Field(default_factory=dict, description="Optional metadata (encoding, dimensions, etc.)")

    @model_validator(mode="after")
    def _validate_type_constraints(self) -> EvidenceAttachment:
        """Enforce type-specific field requirements.

        Rules:
            - screenshot, terminal, tui_capture require ``path`` (not ``content``).
            - shell_output requires ``content`` (not ``path``).
            - content is truncated to 4096 characters.
        """
        path_types = {"screenshot", "terminal", "tui_capture"}
        if self.type in path_types:
            if not self.path:
                msg = f"Evidence type {self.type!r} requires 'path'"
                raise ValueError(msg)
        elif self.type == "shell_output":
            if not self.content:
                msg = "Evidence type 'shell_output' requires 'content'"
                raise ValueError(msg)
            if len(self.content) > EVIDENCE_CONTENT_MAX_CHARS:
                self.content = self.content[:EVIDENCE_CONTENT_MAX_CHARS]
        elif self.type == "recording_timestamp" and not self.content:
            msg = "Evidence type 'recording_timestamp' requires 'content' (ISO-8601 timestamp)"
            raise ValueError(msg)
        return self


class JobStep(BaseModel):
    """A single step logged within a job record."""

    agent_id: str = Field(description="Agent that performed this step")
    action: str = Field(description="What was done")
    timestamp: str = Field(description="ISO 8601 timestamp of the step")
    outcome: str | None = Field(default=None, description="Outcome or result of the step (free-text, legacy)")
    evidence: list[EvidenceAttachment] | None = Field(
        default=None, description="Evidence attachments captured during this step"
    )
    # --- DER fields (all optional for backward compatibility) ---
    intent: str | None = Field(default=None, description="What the agent intends to do (declare phase)")
    reason: str | None = Field(default=None, description="Why this action is being taken (declare phase)")
    outcome_status: StepOutcome | None = Field(
        default=None, description="Structured outcome: success/failed/blocked/skipped"
    )
    outcome_detail: str | None = Field(
        default=None, description="Detailed explanation of what happened (reflect phase)"
    )
    issue_classification: IssueClassification | None = Field(
        default=None, description="Issue classification if a problem was detected"
    )
    resolution: str | None = Field(default=None, description="What was done about the issue, if any (reflect phase)")
    next: str | None = Field(default=None, description="What comes next after this step (reflect phase)")


class JobRecord(BaseModel):
    """A job record representing a multi-step VM operation."""

    job_id: str = Field(description="Unique job identifier (hex[:12])")
    vm_name: str = Field(description="Target VM name")
    scenario_name: str = Field(description="Scenario name")
    created_by: str = Field(description="Agent that created the job")
    last_agent_id: str = Field(description="Last agent that touched the job")
    status: JobStatus = Field(description="Current job status")
    steps: list[JobStep] = Field(default_factory=list, description="Append-only step log")
    created_at: str = Field(description="ISO 8601 timestamp of job creation")
    updated_at: str = Field(description="ISO 8601 timestamp of last update")
    pilot_context_usage: float | None = Field(default=None, description="Pilot context window usage fraction (0.0-1.0)")


# ---------------------------------------------------------------------------
# Packer image management (PLAT-03)
# ---------------------------------------------------------------------------


class PackerCacheEntry(BaseModel):
    """A single entry in the Packer image cache manifest."""

    image_name: str = Field(description="Human-readable image name")
    image_path: str = Field(description="Absolute path to the built image/box")
    platform: str = Field(description="Target platform: windows, macos, linux")
    built_at: str = Field(description="ISO 8601 build timestamp")
    inputs_hash: str = Field(description="SHA-256 of the Packer template inputs")
    template_path: str = Field(description="Path to the Packer template file used")


class PackerBuildResult(BaseModel):
    """Dispatch receipt for packer_build_image (background task)."""

    image_name: str = Field(description="Requested image name")
    template_path: str = Field(description="Packer template path")
    cache_dir: str = Field(description="Cache directory path")
    status: str = Field(description="'dispatched' — build runs as background task")


class PackerImageStatusResult(BaseModel):
    """Result of packer_image_status."""

    image_name: str = Field(description="Requested image name")
    cached: bool = Field(description="Whether a valid cached image exists")
    stale: bool = Field(description="Whether the cached image is stale (inputs changed)")
    entry: PackerCacheEntry | None = Field(default=None, description="Cache entry if cached")
    error: str | None = Field(default=None, description="Error message on failure")


class PackerListResult(BaseModel):
    """Result of packer_list_images."""

    images: list[PackerCacheEntry] = Field(default_factory=list, description="All cached images")
    cache_dir: str = Field(description="Cache directory path")
    count: int = Field(description="Number of cached images")


# ---------------------------------------------------------------------------
# Platform detection (PLAT-01/02)
# ---------------------------------------------------------------------------


class PlatformDetectionResult(BaseModel):
    """Result of platform detection for a VM via VirtualBox Python API."""

    vm_name: str = Field(description="VirtualBox VM name")
    platform: Literal["windows", "macos", "linux"] = Field(description="Detected platform")
    os_type_id: str = Field(description="Raw VirtualBox OS type ID")


class PlatformRunResult(BaseModel):
    """Result of running an installer command on a macOS or Linux VM via SSH."""

    vm_name: str = Field(description="Target VM name")
    scenario_name: str = Field(description="Scenario name")
    platform: str = Field(description="Detected platform")
    installer_cmd: str = Field(description="Command that was executed")
    stdout: str = Field(description="Standard output from the installer")
    stderr: str = Field(description="Standard error from the installer")
    exit_code: int = Field(description="Exit code from the installer")
    error: str | None = Field(default=None, description="Error message on failure")


# ---------------------------------------------------------------------------
# KVM module management (S02-01)
# ---------------------------------------------------------------------------


class KvmUnloadResult(BaseModel):
    """Result of kvm_unload (unload KVM kernel modules to resolve VirtualBox conflict)."""

    status: str = Field(description="'ok' on success, 'blocked' when users active, 'error' on failure")
    modules_before: list[str] = Field(default_factory=list, description="KVM modules loaded before unload attempt")
    modules_after: list[str] = Field(
        default_factory=list, description="KVM modules loaded after unload (empty on success)"
    )
    cpu_vendor: str = Field(description="Detected CPU vendor: 'intel', 'amd', or 'unknown'")
    dry_run: bool = Field(description="Whether this was a dry-run (no changes made)")
    error: str | None = Field(default=None, description="Error message on failure or blocked state")


class KvmReloadResult(BaseModel):
    """Result of kvm_reload (reload KVM kernel modules after VirtualBox use)."""

    status: str = Field(description="'ok' on success, 'error' on failure")
    modules_before: list[str] = Field(default_factory=list, description="KVM modules loaded before reload attempt")
    modules_after: list[str] = Field(
        default_factory=list, description="KVM modules loaded after reload (kvm + vendor module)"
    )
    cpu_vendor: str = Field(description="Detected CPU vendor: 'intel', 'amd', or 'unknown'")
    error: str | None = Field(default=None, description="Error message on failure")


# ---------------------------------------------------------------------------
# VM readiness (S02-02)
# ---------------------------------------------------------------------------


class VmReadyResult(BaseModel):
    """Result of vm_wait_ready (polls GuestProperty until VM is truly booted)."""

    status: str = Field(description="'ok' when ready, 'error' on timeout or VM not running")
    vm_name: str = Field(description="VirtualBox VM name")
    elapsed_s: float = Field(description="Elapsed seconds until ready or timeout")
    strategy: str = Field(description="Readiness strategy used: 'guest_additions_service' or 'guest_info_ip'")
    properties: dict[str, str] = Field(default_factory=dict, description="GuestProperties found at ready state")
    error: str | None = Field(default=None, description="Error message on failure")
    blocking_reason: str | None = Field(
        default=None,
        description="Categorized failure reason: 'timeout', 'vm_not_running', 'vm_not_found', 'guest_additions_missing', 'desktop_not_usable'",
    )
    os_product: str | None = Field(
        default=None, description="OS product detected from GuestProperties (e.g., 'Windows', 'Linux')"
    )
    desktop_settle_s: float = Field(default=0.0, description="Post-readiness settling time applied for GUI automation")


# ---------------------------------------------------------------------------
# Guest file operations (S02-01)
# ---------------------------------------------------------------------------


class VmCopyResult(BaseModel):
    """Result of vm_copy_from_guest (copy file from guest VM to host)."""

    success: bool = Field(description="Whether the copy operation succeeded")
    guest_path: str = Field(description="Source path inside the guest VM")
    host_path: str = Field(description="Destination path on the host")
    error: str | None = Field(default=None, description="Error message on failure (VBoxManage stderr)")


# ---------------------------------------------------------------------------
# Recording and evidence (always-on screen capture + summary video)
# ---------------------------------------------------------------------------


class RecordingStartResult(BaseModel):
    """Result of vm_recording_start."""

    status: str = Field(description="'ok' on success, 'error' on failure")
    vm_name: str = Field(description="VM name")
    recording_path: str | None = Field(default=None, description="Path to WebM file")
    started_at: str | None = Field(default=None, description="ISO 8601 timestamp")
    fps: int = Field(default=5, description="Recording FPS")
    video_rate_kbps: int = Field(default=256, description="Video bitrate")
    error: str | None = Field(default=None, description="Error message on failure")


class RecordingStopResult(BaseModel):
    """Result of vm_recording_stop."""

    status: str = Field(description="'ok' on success, 'error' on failure")
    vm_name: str = Field(description="VM name")
    recording_path: str | None = Field(default=None, description="Path to finalized WebM")
    duration_s: float | None = Field(default=None, description="Recording duration in seconds")
    file_size_bytes: int | None = Field(default=None, description="WebM file size")
    error: str | None = Field(default=None, description="Error message on failure")


class RecordingStatusResult(BaseModel):
    """Result of vm_recording_status."""

    vm_name: str = Field(description="VM name")
    recording: bool = Field(description="Whether recording is active")
    recording_path: str | None = Field(default=None, description="Current/last recording path")
    recording_started_at: str | None = Field(default=None, description="When recording started")
    elapsed_s: float | None = Field(default=None, description="Seconds since recording started")
    file_size_bytes: int | None = Field(default=None, description="Current file size on disk")


class FrameExtractionResult(BaseModel):
    """Result of vm_extract_frames."""

    vm_name: str = Field(description="VM name")
    frames: list[str] = Field(default_factory=list, description="Paths to extracted PNG frames")
    frame_count: int = Field(default=0, description="Number of frames extracted")
    source_recording: str | None = Field(default=None, description="WebM file frames were extracted from")
    time_range: str | None = Field(default=None, description="Time range extracted (start--end)")
    error: str | None = Field(default=None, description="Error message on failure")


class SummaryVideoResult(BaseModel):
    """Result of vm_build_summary_video."""

    job_id: str = Field(description="Job identifier")
    vm_name: str = Field(description="VM name")
    video_path: str | None = Field(default=None, description="Path to rendered MP4")
    duration_s: float | None = Field(default=None, description="Video duration")
    step_count: int = Field(default=0, description="Number of job steps in the video")
    evidence_count: int = Field(default=0, description="Number of evidence items included")
    error: str | None = Field(default=None, description="Error message on failure")
