"""Proof evidence validator for R017 truthful evidence enforcement.

This module provides a validator that takes a job_id and vm_name and returns
a structured pass/fail result for every evidence category the proof run must
produce: job record, proof file contents, screenshots, and recording-or-gap.

The validator reuses MissingReason from demo_manifest.py for consistency.
"""

from __future__ import annotations

import re
from typing import TYPE_CHECKING

from pydantic import BaseModel, Field

from mcp_vm_blackbox.demo_manifest import MissingReason
from mcp_vm_blackbox.job_store import JobStore

if TYPE_CHECKING:
    from pathlib import Path


class EvidenceCheck(BaseModel):
    """A single evidence check result."""

    name: str = Field(description="Name of the evidence category checked")
    passed: bool = Field(description="Whether the check passed")
    detail: str = Field(description="Human-readable detail about the check result")
    missing_reason: MissingReason | None = Field(
        default=None, description="Reason why evidence is missing (only if passed=False)"
    )


class ProofEvidenceResult(BaseModel):
    """Result of validating proof run evidence."""

    valid: bool = Field(description="Whether all required evidence is present and valid")
    job_id: str = Field(description="Job ID that was validated")
    vm_name: str = Field(description="VM name that was validated")
    checks: list[EvidenceCheck] = Field(default_factory=list, description="Per-category check results")


# ISO 8601 timestamp pattern (matches YYYY-MM-DDTHH:MM:SS or similar)
_ISO_TIMESTAMP_PATTERN = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}")


async def _validate_job_record(job_store: JobStore, job_id: str, vm_name: str) -> EvidenceCheck:
    """Validate that the job record exists and is completed."""
    try:
        record = await job_store.load(job_id, vm_name)
    except FileNotFoundError:
        return EvidenceCheck(
            name="job_record",
            passed=False,
            detail=f"Job record not found for job_id={job_id}, vm_name={vm_name}",
            missing_reason=MissingReason.PATH_ABSENT,
        )

    if record.status != "completed":
        return EvidenceCheck(
            name="job_record",
            passed=False,
            detail=f"Job record exists but status is '{record.status}', not 'completed'",
            missing_reason=None,
        )

    return EvidenceCheck(
        name="job_record",
        passed=True,
        detail=f"Job record exists with status='completed', created_at={record.created_at}",
        missing_reason=None,
    )


def _validate_proof_file(proof_file_path: Path) -> EvidenceCheck:
    """Validate that the proof file exists and contains required content."""
    if not proof_file_path.exists():
        return EvidenceCheck(
            name="proof_file",
            passed=False,
            detail=f"Proof file not found at {proof_file_path}",
            missing_reason=MissingReason.PATH_ABSENT,
        )

    try:
        content = proof_file_path.read_text(encoding="utf-8")
    except OSError as e:
        return EvidenceCheck(
            name="proof_file",
            passed=False,
            detail=f"Could not read proof file: {e}",
            missing_reason=MissingReason.PATH_ABSENT,
        )

    issues: list[str] = []

    # Check for a number (calculated value)
    if not re.search(r"\d+(\.\d+)?", content):
        issues.append("missing calculated value (no number found)")

    # Check for thumbs-up emoji
    if "\U0001f44d" not in content:  # 👍
        issues.append("missing thumbs-up emoji")

    # Check for non-empty hostname line
    hostname_match = re.search(r"hostname:\s*(\S+)", content, re.IGNORECASE)
    if not hostname_match or not hostname_match.group(1):
        issues.append("missing or empty hostname line")

    # Check for non-empty username line
    username_match = re.search(r"username:\s*(\S+)", content, re.IGNORECASE)
    if not username_match or not username_match.group(1):
        issues.append("missing or empty username line")

    # Check for ISO timestamp pattern
    if not _ISO_TIMESTAMP_PATTERN.search(content):
        issues.append("missing ISO timestamp pattern")

    if issues:
        return EvidenceCheck(
            name="proof_file", passed=False, detail=f"Proof file incomplete: {'; '.join(issues)}", missing_reason=None
        )

    return EvidenceCheck(
        name="proof_file",
        passed=True,
        detail="Proof file contains all required fields (value, emoji, hostname, username, timestamp)",
        missing_reason=None,
    )


def _validate_visual_evidence(job_evidence_dir: Path, vm_name: str) -> EvidenceCheck:
    """Validate that at least one file-backed evidence item exists for the VM.

    Checks three sibling directories under job_evidence_dir:
      - screenshots/  — {vm_name}-*.png
      - terminal/     — {vm_name}-*.txt
      - tui_captures/ — {vm_name}-*.txt

    The check passes if ANY directory contains at least one matching file.

    Args:
        job_evidence_dir: Parent directory containing screenshots/, terminal/,
            and tui_captures/ subdirectories.
        vm_name: VM name prefix used to match evidence filenames.

    Returns:
        EvidenceCheck with name="visual_evidence".
    """
    screenshots_dir = job_evidence_dir / "screenshots"
    terminal_dir = job_evidence_dir / "terminal"
    tui_captures_dir = job_evidence_dir / "tui_captures"

    screenshots = list(screenshots_dir.glob(f"{vm_name}-*.png")) if screenshots_dir.exists() else []
    terminal_captures = list(terminal_dir.glob(f"{vm_name}-*.txt")) if terminal_dir.exists() else []
    tui_captures = list(tui_captures_dir.glob(f"{vm_name}-*.txt")) if tui_captures_dir.exists() else []

    total = len(screenshots) + len(terminal_captures) + len(tui_captures)

    if total == 0:
        return EvidenceCheck(
            name="visual_evidence",
            passed=False,
            detail=(
                f"No visual evidence found for '{vm_name}' in {job_evidence_dir}/{{screenshots,terminal,tui_captures}}"
            ),
            missing_reason=MissingReason.NOT_CAPTURED,
        )

    parts: list[str] = []
    if screenshots:
        parts.append(f"{len(screenshots)} screenshot(s)")
    if terminal_captures:
        parts.append(f"{len(terminal_captures)} terminal capture(s)")
    if tui_captures:
        parts.append(f"{len(tui_captures)} TUI capture(s)")

    return EvidenceCheck(name="visual_evidence", passed=True, detail=f"Found {', '.join(parts)}", missing_reason=None)


def _validate_recordings(recordings_dir: Path | None, vm_name: str) -> EvidenceCheck:
    """Validate recording presence or return explicit gap.

    Recording absence is acceptable but must be explicit (NOT_CAPTURED).
    """
    if recordings_dir is None or not recordings_dir.exists():
        return EvidenceCheck(
            name="recording",
            passed=True,  # Recording is optional, so we mark as passed
            detail="Recordings directory not provided or does not exist",
            missing_reason=MissingReason.NOT_CAPTURED,
        )

    # Look for webm or mp4 recordings
    recordings = list(recordings_dir.glob(f"{vm_name}-*.webm"))
    recordings.extend(recordings_dir.glob(f"{vm_name}-*.mp4"))

    if not recordings:
        return EvidenceCheck(
            name="recording",
            passed=True,  # Recording is optional, so we mark as passed
            detail=f"No recordings found matching '{vm_name}-*.webm' or '{vm_name}-*.mp4'",
            missing_reason=MissingReason.NOT_CAPTURED,
        )

    return EvidenceCheck(
        name="recording", passed=True, detail=f"Found {len(recordings)} recording(s) for {vm_name}", missing_reason=None
    )


def _validate_render_staging(render_staging_dir: Path) -> EvidenceCheck:
    """Validate that render_staging directory exists when video is enabled.

    Args:
        render_staging_dir: Path to the render_staging directory for this job.

    Returns:
        EvidenceCheck indicating whether the directory exists.
    """
    if not render_staging_dir.exists():
        return EvidenceCheck(
            name="render_staging",
            passed=False,
            detail=f"Render staging directory does not exist: {render_staging_dir}",
            missing_reason=MissingReason.PATH_ABSENT,
        )

    return EvidenceCheck(
        name="render_staging",
        passed=True,
        detail=f"Render staging directory exists at {render_staging_dir}",
        missing_reason=None,
    )


async def validate_proof_evidence(
    job_id: str,
    vm_name: str,
    jobs_root: Path,
    screenshots_dir: Path,
    proof_file_path: Path,
    recordings_dir: Path | None = None,
    video_enabled: bool = False,
) -> ProofEvidenceResult:
    """Validate that all required evidence for a proof run is present.

    Args:
        job_id: The job identifier to validate.
        vm_name: The VM name associated with the job.
        jobs_root: Root directory for job storage (passed to JobStore).
        screenshots_dir: Path to the screenshots/ subdirectory.  Its parent is
            used as the job evidence directory, which must also contain
            terminal/ and tui_captures/ siblings for full evidence coverage.
        proof_file_path: Path to the proof file.
        recordings_dir: Optional directory containing recordings.
        video_enabled: When True, validates that render_staging directory exists.
            The render_staging directory is required for video-enabled runs to
            ensure the attribution chain starts at evidence capture time.

    Returns:
        ProofEvidenceResult with per-category check results.
        The `valid` field is True if all required checks pass
        (recording is optional but must have explicit missing_reason if absent;
        render_staging is required when video_enabled=True).
        The visual_evidence check passes if ANY of screenshots/, terminal/,
        or tui_captures/ contains at least one matching file.
    """
    job_store = JobStore(root=jobs_root)

    # Derive the job evidence directory from screenshots_dir for backward compatibility.
    # Structure: evidence/{vm_name}/{job_id}/screenshots -> evidence/{vm_name}/{job_id}/
    job_evidence_dir = screenshots_dir.parent

    checks: list[EvidenceCheck] = [
        await _validate_job_record(job_store, job_id, vm_name),
        _validate_proof_file(proof_file_path),
        _validate_visual_evidence(job_evidence_dir, vm_name),
        _validate_recordings(recordings_dir, vm_name),
    ]

    # When video is enabled, validate render_staging directory
    if video_enabled:
        render_staging_dir = job_evidence_dir / "render_staging"
        checks.append(_validate_render_staging(render_staging_dir))

    # Valid if all required checks pass:
    # - recording is optional (passes with NOT_CAPTURED)
    # - render_staging is required when video_enabled=True
    required_names = {"job_record", "proof_file", "visual_evidence"}
    if video_enabled:
        required_names.add("render_staging")

    required_checks = [c for c in checks if c.name in required_names]
    valid = all(c.passed for c in required_checks)

    return ProofEvidenceResult(valid=valid, job_id=job_id, vm_name=vm_name, checks=checks)
