#!/usr/bin/env python3
"""Validate a benchmark cycle decision record meets governance gate requirements."""

from __future__ import annotations

import argparse
import re
import sys
from pathlib import Path
from typing import List, Optional


ALLOWED_OUTCOME_TYPES = {
    "measurable_gain",
    "validated_negative",
    "uncertainty_reduction",
    "uncertainty_reducing",
}


def fail(message: str, code: int = 2) -> int:
    print(f"ERROR: {message}", file=sys.stderr)
    return code


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description=(
            "Validate CYCLE_DECISION_*.md completeness for benchmark governance gating"
        )
    )
    parser.add_argument(
        "--cycle-decision-file",
        required=True,
        help="Path to cycle decision markdown (expected CYCLE_DECISION_*.md)",
    )
    parser.add_argument(
        "--baseline-run",
        required=True,
        help="Expected baseline run id",
    )
    parser.add_argument(
        "--target-run",
        required=True,
        help="Expected target run id",
    )
    return parser.parse_args()


def extract_section(lines: List[str], heading_fragment: str) -> List[str]:
    heading_fragment = heading_fragment.lower()
    start = None
    end = len(lines)
    for i, line in enumerate(lines):
        if line.startswith("##") and heading_fragment in line.lower():
            start = i + 1
            continue
        if start is not None and line.startswith("##") and i > start:
            end = i
            break
    if start is None:
        return []
    return lines[start:end]


def collect_run_ids_for_role(text: str, role: str) -> List[str]:
    # Example matches: ...runs/<RUN_ID>/summary.json with role hint in same line.
    pattern = re.compile(
        rf"{re.escape(role)}[^\n]*/runs/([^/\s`'\"\]]+)/summary\\.json",
        re.I,
    )
    matches = pattern.findall(text)
    if matches:
        return matches

    # Fallback: scan summary references regardless of role.
    return re.findall(r"/runs/([^/\\s`'\"\]]+)/summary\\.json", text)


def parse_markdown_table(section_lines: List[str]) -> int:
    """Return number of data rows in a markdown table in the provided section."""
    if not section_lines:
        return 0

    # locate a plausible header with a class column
    header_idx = None
    for idx, line in enumerate(section_lines):
        if line.strip().startswith("|") and "class" in line.lower():
            # avoid separator-like lines
            if "---" not in line:
                header_idx = idx
                break
    if header_idx is None:
        return 0

    # find separator row
    sep_idx = None
    for idx in range(header_idx + 1, len(section_lines)):
        if section_lines[idx].strip().startswith("|") and re.search(r"\|\s*-{3,}\s*\|", section_lines[idx]):
            sep_idx = idx
            break
    if sep_idx is None:
        return 0

    row_count = 0
    for idx in range(sep_idx + 1, len(section_lines)):
        line = section_lines[idx].strip()
        if not line.startswith("|"):
            break
        cells = [cell.strip() for cell in line.strip("|").split("|")]
        if len(cells) >= 2 and any(cells):
            # skip accidental horizontal separator rows
            if all(re.fullmatch(r"-+", cell) or cell == "" for cell in cells):
                continue
            row_count += 1
    return row_count


def nonempty_action(section_lines: List[str]) -> bool:
    if not section_lines:
        return False

    immediate = re.compile(
        r"-\s*\*\*Immediate next action:\*\*\s*:?:?\s*(.+?)\s*$",
        re.I,
    )
    followup_header = re.compile(
        r"-\s*\*\*Follow-up experiments:\*\*\s*:?:?\s*$",
        re.I,
    )

    for line in section_lines:
        m = immediate.search(line)
        if m:
            value = m.group(1).strip()
            if value and value not in {"-", "`"}:
                return True

    for i, line in enumerate(section_lines):
        if followup_header.search(line):
            for sub in section_lines[i + 1 :]:
                if sub.startswith("##"):
                    break
                m = re.match(r"\s*-\s*(.+?)\s*$", sub)
                if m:
                    value = m.group(1).strip()
                    if value and value not in {"-", "`", "TODO", "TBD", "To Do"}:
                        return True

    return False


def find_decision(text: str) -> Optional[str]:
    m = re.search(
        r"\*\*Decision:\*\*\s*`?(PASS|HOLD|FAIL|PARTIAL)`?",
        text,
        re.I,
    )
    if not m:
        return None
    return m.group(1).upper()


def find_outcome_type(text: str) -> Optional[str]:
    # Prefer inline-code value first.
    m = re.search(
        r"\*\*Outcome\\s*Type:\*\*\s*`([^`]+)`",
        text,
        re.I,
    )
    if not m:
        m = re.search(r"\*\*Outcome\\s*Type:\*\*\s*([^\n]+)", text, re.I)
    if not m:
        return None
    return m.group(1).strip().strip("`\n ").strip()


def normalize_outcome_type(raw_value: str) -> str:
    normalized = re.sub(r"[\s\\-]+", "_", raw_value.lower().strip())
    normalized = re.sub(r"[^a-z0-9_]", "", normalized)
    normalized = re.sub(r"_+", "_", normalized).strip("_")
    return normalized


def has_reference(text: str, regex: str) -> bool:
    return re.search(regex, text, re.I) is not None


def main() -> int:
    args = parse_args()
    path = Path(args.cycle_decision_file)
    if not path.exists():
        return fail(f"Cycle decision file not found: {path}")
    if not path.name.startswith("CYCLE_DECISION_") or path.suffix.lower() != ".md":
        return fail(f"Decision filename must be CYCLE_DECISION_*.md, got {path.name}")

    text = path.read_text(encoding="utf-8", errors="replace")
    lines = text.splitlines()

    if not extract_section(lines, "Artifacts"):
        return fail("Missing Artifacts section for benchmark artifact references.")

    baseline_ids = collect_run_ids_for_role(text, "baseline")
    target_ids = collect_run_ids_for_role(text, "target")

    if not baseline_ids:
        return fail("Missing baseline run reference with path containing '/runs/<baseline>/summary.json'")
    if not target_ids:
        return fail("Missing target run reference with path containing '/runs/<target>/summary.json'")

    if args.baseline_run not in baseline_ids:
        return fail(
            f"Baseline run id mismatch: expected {args.baseline_run}, "
            f"found {baseline_ids}"
        )
    if args.target_run not in target_ids:
        return fail(
            f"Target run id mismatch: expected {args.target_run}, "
            f"found {target_ids}"
        )

    section_3 = extract_section(lines, "Benchmark Impact")
    rows = parse_markdown_table(section_3)
    if rows <= 0:
        return fail(
            "Missing class-level outcome table in section 3 (Benchmark Impact) "
            "or missing data rows"
        )

    decision = find_decision(text)
    if not decision:
        return fail("Missing/invalid decision type under section 5 (expected PASS/HOLD/FAIL/PARTIAL).")

    outcome_raw = find_outcome_type(text)
    if not outcome_raw:
        return fail(
            "Missing Outcome Type under section 5 (`measurable_gain`, "
            "`validated_negative`, `uncertainty_reducing`)."
        )

    outcome = normalize_outcome_type(outcome_raw)
    if outcome not in ALLOWED_OUTCOME_TYPES:
        return fail(
            f"Invalid Outcome Type '{outcome_raw}'. Allowed values are: "
            "`measurable_gain`, `validated_negative`, `uncertainty_reducing`."
        )

    actions_section = extract_section(lines, "Next Actions")
    if not nonempty_action(actions_section):
        return fail(
            "Missing concrete next actions in section 7 (Immediate next action or Follow-up)."
        )

    required_refs = {
        "baseline summary artifact": rf"/runs/{re.escape(args.baseline_run)}/summary\\.json",
        "target summary artifact": rf"/runs/{re.escape(args.target_run)}/summary\\.json",
        "generated delta json artifact": rf"DELTA_{re.escape(args.target_run)}\\.json",
        "generated delta markdown artifact": rf"DELTA_{re.escape(args.target_run)}\\.md",
        "verification log artifact": rf"VERIFY_{re.escape(args.target_run)}\\.log",
    }
    for label, pattern in required_refs.items():
        if not has_reference(text, pattern):
            return fail(f"Missing required artifact reference for {label}.")

    print(
        f"PASS: cycle decision '{path.name}' includes "
        f"baseline={args.baseline_run}, target={args.target_run}, "
        f"decision={decision}, outcome_type={outcome}, class_rows={rows}"
    )
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
