#!/usr/bin/env bash
# Aggregate REC-72c OAuth migration regression gate.
#
# This is intentionally the final REC-72c gate. It runs the mandatory dispatch
# audit, aggregates the Phase 1-2 suites, confirms REC-72/REC-72b still pass,
# then applies function-scoped OAuth dispatch checks for the bible breakdown,
# storyboard skeleton, and Stage-1.5 enrichment paths.
set -euo pipefail

HERE="$(cd "$(dirname "$0")" && pwd)"
REPO="$(cd "$HERE/../../../.." && pwd)"
cd "$REPO"

export PYTHONPATH="${PYTHONPATH:+$PYTHONPATH:}."

echo "== REC-72c mandatory dispatch audit =="
python3 recoil/pipeline/tools/audit_dispatch.py --project tartarus --episode ep_001

echo "== REC-72c Phase 1-2 regression suites =="
python3 -m pytest -q \
  recoil/pipeline/orchestrator/tests/test_enrichment_oauth.py \
  recoil/pipeline/orchestrator/tests/test_breakdown_opus.py

echo "== REC-72 and REC-72b regression aggregate =="
bash recoil/pipeline/tools/tests/test_rec72b_oauth.sh

echo "== REC-72c exact function-scoped OAuth gates =="
python3 - <<'PY'
import ast
from pathlib import Path


def fail(message: str) -> None:
    raise SystemExit(message)


ingest_path = Path("recoil/pipeline/orchestrator/ingest_pipeline.py")
src = ingest_path.read_text(encoding="utf-8")

if "anthropic.Anthropic(" in src:
    fail(f"{ingest_path} still constructs anthropic.Anthropic")

tree = ast.parse(src, filename=str(ingest_path))


def get_function(name: str) -> ast.FunctionDef:
    matches = [
        node
        for node in ast.walk(tree)
        if isinstance(node, ast.FunctionDef) and node.name == name
    ]
    if len(matches) != 1:
        fail(f"expected exactly one {name} function, found {len(matches)}")
    return matches[0]


def node_src(node: ast.AST) -> str:
    return ast.get_source_segment(src, node) or ""


def is_call_named(node: ast.AST, name: str) -> bool:
    return isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == name


def has_constant(node: ast.AST | None, value: str) -> bool:
    if node is None:
        return False
    return any(isinstance(child, ast.Constant) and child.value == value for child in ast.walk(node))


def is_model_startswith_claude(test: ast.AST) -> bool:
    if not isinstance(test, ast.Call):
        return False
    func = test.func
    return (
        isinstance(func, ast.Attribute)
        and func.attr == "startswith"
        and isinstance(func.value, ast.Name)
        and func.value.id == "model"
        and len(test.args) == 1
        and isinstance(test.args[0], ast.Constant)
        and test.args[0].value == "claude-"
    )


def claude_branch_calls(func: ast.FunctionDef) -> list[ast.Call]:
    calls: list[ast.Call] = []
    for node in ast.walk(func):
        if isinstance(node, ast.If) and is_model_startswith_claude(node.test):
            for stmt in node.body:
                calls.extend(
                    child
                    for child in ast.walk(stmt)
                    if is_call_named(child, "call_opus_oauth")
                )
    return calls


def assert_function_scoped_call(name: str) -> tuple[ast.FunctionDef, list[ast.Call]]:
    func = get_function(name)
    if "call_opus_oauth" not in node_src(func):
        fail(f"{name} body does not contain call_opus_oauth")
    calls = claude_branch_calls(func)
    if not calls:
        fail(f"{name} has no call_opus_oauth inside a model.startswith('claude-') branch")
    return func, calls


_, breakdown_calls = assert_function_scoped_call("run_breakdown_pass")
assert_function_scoped_call("run_storyboard_pass")

for call in breakdown_calls:
    keywords = {kw.arg: kw.value for kw in call.keywords if kw.arg is not None}
    if "json_schema" not in keywords:
        fail(f"run_breakdown_pass claude call at line {call.lineno} is missing json_schema=")
    timeout = keywords.get("timeout")
    if not (isinstance(timeout, ast.Constant) and timeout.value == 2400):
        fail(f"run_breakdown_pass claude call at line {call.lineno} is missing timeout=2400")

enrichment = get_function("run_opus_enrichment")
for node in ast.walk(enrichment):
    if isinstance(node, ast.If) and has_constant(node.test, "ANTHROPIC_API_KEY"):
        fail(f"run_opus_enrichment branches on ANTHROPIC_API_KEY at line {node.lineno}")
    if isinstance(node, ast.Return) and has_constant(node.value, "no_api_key"):
        fail(f"run_opus_enrichment returns no_api_key at line {node.lineno}")

print("function-scoped OAuth dispatch gates OK")
PY

echo "== REC-72c model role resolution gates =="
python3 - <<'PY'
from recoil.core.model_profiles import get_model
from recoil.pipeline.orchestrator import ingest_pipeline as ip


def fail(message: str) -> None:
    raise SystemExit(message)


resolved = {
    "BREAKDOWN_MODEL": ip.BREAKDOWN_MODEL,
    "STORYBOARD_MODEL": ip.STORYBOARD_MODEL,
    "prose_author": get_model("prose_author", "text"),
}

if resolved["prose_author"] != ip.PROSE_AUTHOR_MODEL:
    fail(
        "PROSE_AUTHOR_MODEL does not match prose_author role: "
        f"{ip.PROSE_AUTHOR_MODEL!r} vs {resolved['prose_author']!r}"
    )

bad = {name: model for name, model in resolved.items() if not model.startswith("claude-")}
if bad:
    fail(f"expected claude-* model resolution, got {bad}")

print("model role resolution gates OK")
PY

echo "standalone non-render tools are out of scope: prompt_ab_test.py, generate_location_refs.py, visual_qc.py"
echo "REC-72c OAuth aggregate OK"
