"""Validation gates: verify the envelope's CLAIMS, never guesses.

A gate is `gate(envelope, run, baseline_commit) -> GateReport` — one check per item it looked at.
Violations are derived from the failed checks and sent back to the SAME agent
session as a correction. Every check is recorded either way, so a green gate
says WHAT it verified instead of only that it passed.

Gates check what is mechanically checkable; plan quality is a reviewer's job.
"""

from __future__ import annotations

import json
import subprocess
from pathlib import Path

from .data_types import EnvelopeBase, GateReport

TAIL_CHARS = 1000        # command output kept as evidence on a failure


def _size(path: Path) -> str:
    n = path.stat().st_size
    return f"{n}B" if n < 1024 else f"{n / 1024:.1f}KB"


def artifacts_exist(envelope: EnvelopeBase, run, baseline_commit: str) -> GateReport:
    report = GateReport()
    for a in envelope.artifacts:
        p = Path(a)
        report.check(a, p.exists(),
                     f"exists, {_size(p)}" if p.exists() else "declared artifact does not exist")
    return report


def files_non_empty(envelope: EnvelopeBase, run, baseline_commit: str) -> GateReport:
    report = GateReport()
    for a in envelope.artifacts:
        p = Path(a)
        if not (p.exists() and p.is_file()):
            continue                       # existence is artifacts_exist's job
        empty = p.stat().st_size == 0
        report.check(a, not empty, "declared artifact is empty" if empty else _size(p))
    return report


def json_parses(envelope: EnvelopeBase, run, baseline_commit: str) -> GateReport:
    report = GateReport()
    for a in envelope.artifacts:
        p = Path(a)
        if p.suffix != ".json" or not p.exists():
            continue
        try:
            parsed = json.loads(p.read_text())
            report.check(a, True, f"parses, {type(parsed).__name__}")
        except json.JSONDecodeError as e:
            report.check(a, False, f"declared JSON artifact does not parse: {e}")
    return report


def diff_matches_claims(envelope: EnvelopeBase, run, baseline_commit: str) -> GateReport:
    """Every claimed created or modified file exists; deletions are in the phase diff."""
    report = GateReport()
    missing = [f for f in getattr(envelope, "changed_files", []) if not Path(f).exists()]
    deleted = _deleted_paths(run, baseline_commit) if missing and baseline_commit else set()
    for f in getattr(envelope, "changed_files", []):
        p = Path(f)
        exists = p.exists()
        claimed = _repo_relative_path(p, run.repo_root)
        report.check(f, exists or claimed in deleted,
                     f"exists, {_size(p)}" if exists else "deleted in git diff"
                     if claimed in deleted else "trusted phase baseline unavailable"
                     if not baseline_commit else "claimed changed file does not exist")
    return report


def _deleted_paths(run, base: str) -> set[str]:
    result = subprocess.run(
        ["git", "diff", "--name-only", "--no-renames", "--diff-filter=D", "-z", base, "--"],
        cwd=run.repo_root, capture_output=True,
    )
    if result.returncode:
        return set()
    return {path for raw in result.stdout.split(b"\0") if raw
            for path in [raw.decode(errors="surrogateescape")]}


def _repo_relative_path(path: Path, repo_root: Path) -> str:
    try:
        return (repo_root / path).resolve().relative_to(repo_root.resolve()).as_posix()
    except ValueError:
        return ""


def verdict_consistent(envelope: EnvelopeBase, run, baseline_commit: str) -> GateReport:
    """A review's verdict must agree with the findings it just wrote down.

    Nothing here judges the code — that is the reviewer's job. This checks the
    envelope against itself: an approval that ships blocking items, or a
    rejection that names no problem, is a claim the harness can refute without
    reading a line of the diff.
    """
    report = GateReport()
    approved = bool(getattr(envelope, "approved", False))
    blocking = list(getattr(envelope, "blocking", []))
    unmet = [f.requirement for f in getattr(envelope, "findings", []) if not f.met]

    report.check("approved vs blocking", not (approved and blocking),
                 "no blocking items" if not blocking
                 else f"{len(blocking)} blocking item(s) while approved=true"
                 if approved else f"{len(blocking)} blocking item(s), not approved")
    report.check("approved vs findings", not (approved and unmet),
                 "every requirement met" if not unmet
                 else f"{len(unmet)} unmet requirement(s) while approved=true"
                 if approved else f"{len(unmet)} unmet requirement(s), not approved")
    report.check("rejection names a problem", approved or bool(blocking or unmet),
                 "verdict is supported" if approved or blocking or unmet
                 else "approved=false but no blocking item or unmet requirement was given")
    return report


def tests_pass(command: str):
    """Gate factory: the given shell command must exit 0."""
    def gate(envelope: EnvelopeBase, run, baseline_commit: str) -> GateReport:
        result = subprocess.run(command, shell=True, capture_output=True, text=True)
        ok = result.returncode == 0
        note = f"exit {result.returncode}"
        if not ok:
            note += "\n" + (result.stdout + result.stderr)[-TAIL_CHARS:]
        return GateReport().check(command, ok, note)
    gate.__name__ = f"tests_pass({command})"
    return gate
