#!/usr/bin/env python3
"""Strict loader for harness/contracts/workflow-policy.json.

Unknown fields at any depth fail validation. Python 3.10+, stdlib only.
"""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any

POLICY_REL = Path("harness") / "contracts" / "workflow-policy.json"

TOP_LEVEL_KEYS = frozenset(
    {
        "schemaVersion",
        "testExecution",
        "riskTiers",
        "skills",
        "requiredArtifacts",
        "requiredValidations",
        "validationPhases",
        "capabilityGates",
        "conditionalStages",
        "interactionWhitelist",
        "checkpointRules",
    }
)

RISK_TIER_KEYS = frozenset(
    {
        "description",
        "defaultPhases",
        "requiredValidations",
        "conditionalStages",
        "upgradeTriggers",
    }
)

SKILL_KEYS = frozenset(
    {
        "phase",
        "inputs",
        "artifacts",
        "events",
        "allowedInteractions",
        "capabilities",
    }
)

CONDITIONAL_STAGE_KEYS = frozenset({"tiers", "signals"})
CAPABILITY_GATE_KEYS = frozenset(
    {"signals", "requiredStages", "requiredValidations"}
)
CHECKPOINT_RULE_KEYS = frozenset(
    {
        "afterTasks",
        "beforeTasks",
        "blocking",
        "requiredReport",
        "reviewerTool",
    }
)
TEST_EXECUTION_KEYS = frozenset(
    {
        "runner",
        "defaultProfile",
        "maxWorkers",
        "singleInstance",
        "processTreeCleanup",
        "detachedProcessPreflight",
        "confirmationFlag",
        "confirmationRequiredProfiles",
        "resourceIntensiveModules",
        "profiles",
    }
)
TEST_EXECUTION_PROFILE_KEYS = frozenset(
    {
        "description",
        "includesResourceIntensive",
        "moduleTimeoutSeconds",
    }
)


class PolicyValidationError(ValueError):
    """Raised when workflow-policy.json contains unknown or invalid fields."""


def _reject_unknown(data: dict[str, Any], allowed: frozenset[str], path: str) -> None:
    unknown = sorted(set(data) - allowed)
    if unknown:
        raise PolicyValidationError(
            f"unknown field(s) at {path}: {', '.join(unknown)}"
        )


def _require_type(value: Any, expected: type | tuple[type, ...], path: str) -> None:
    if not isinstance(value, expected):
        names = (
            expected.__name__
            if isinstance(expected, type)
            else "|".join(t.__name__ for t in expected)
        )
        raise PolicyValidationError(f"{path} must be {names}, got {type(value).__name__}")


def validate_policy(data: Any) -> dict[str, Any]:
    if not isinstance(data, dict):
        raise PolicyValidationError("workflow policy root must be an object")
    _reject_unknown(data, TOP_LEVEL_KEYS, "root")
    _require_type(data.get("schemaVersion"), int, "schemaVersion")
    if data["schemaVersion"] != 1:
        raise PolicyValidationError("unsupported schemaVersion")

    test_execution = data["testExecution"]
    _require_type(test_execution, dict, "testExecution")
    _reject_unknown(test_execution, TEST_EXECUTION_KEYS, "testExecution")
    for key in ("runner", "defaultProfile", "confirmationFlag"):
        _require_type(test_execution[key], str, f"testExecution.{key}")
    max_workers = test_execution["maxWorkers"]
    _require_type(max_workers, int, "testExecution.maxWorkers")
    if isinstance(max_workers, bool) or max_workers < 1 or max_workers > 2:
        raise PolicyValidationError("testExecution.maxWorkers must be 1 or 2")
    for key in (
        "singleInstance",
        "processTreeCleanup",
        "detachedProcessPreflight",
    ):
        _require_type(test_execution[key], bool, f"testExecution.{key}")
        if not test_execution[key]:
            raise PolicyValidationError(f"testExecution.{key} must be true")
    for key in ("confirmationRequiredProfiles", "resourceIntensiveModules"):
        values = test_execution[key]
        _require_type(values, list, f"testExecution.{key}")
        if any(not isinstance(item, str) or not item for item in values):
            raise PolicyValidationError(
                f"testExecution.{key} must contain non-empty strings"
            )

    execution_profiles = test_execution["profiles"]
    _require_type(execution_profiles, dict, "testExecution.profiles")
    required_profile_names = {"safe", "system", "full"}
    if set(execution_profiles) != required_profile_names:
        raise PolicyValidationError(
            "testExecution.profiles must define exactly safe, system, and full"
        )
    if test_execution["defaultProfile"] != "safe":
        raise PolicyValidationError("testExecution.defaultProfile must be safe")
    confirmation_profiles = set(test_execution["confirmationRequiredProfiles"])
    if not {"system", "full"}.issubset(confirmation_profiles):
        raise PolicyValidationError(
            "testExecution confirmation must cover system and full"
        )
    for profile_name, profile in execution_profiles.items():
        path = f"testExecution.profiles.{profile_name}"
        _require_type(profile, dict, path)
        _reject_unknown(profile, TEST_EXECUTION_PROFILE_KEYS, path)
        _require_type(profile["description"], str, f"{path}.description")
        _require_type(
            profile["includesResourceIntensive"],
            bool,
            f"{path}.includesResourceIntensive",
        )
        timeout = profile["moduleTimeoutSeconds"]
        _require_type(timeout, int, f"{path}.moduleTimeoutSeconds")
        if isinstance(timeout, bool) or timeout <= 0:
            raise PolicyValidationError(
                f"{path}.moduleTimeoutSeconds must be a positive integer"
            )
    if execution_profiles["safe"]["includesResourceIntensive"]:
        raise PolicyValidationError(
            "testExecution.profiles.safe must exclude resource-intensive tests"
        )
    for profile_name in ("system", "full"):
        if not execution_profiles[profile_name]["includesResourceIntensive"]:
            raise PolicyValidationError(
                f"testExecution.profiles.{profile_name} must include resource-intensive tests"
            )

    risk_tiers = data["riskTiers"]
    _require_type(risk_tiers, dict, "riskTiers")
    for tier_name, tier in risk_tiers.items():
        path = f"riskTiers.{tier_name}"
        _require_type(tier, dict, path)
        _reject_unknown(tier, RISK_TIER_KEYS, path)
        _require_type(tier["description"], str, f"{path}.description")
        _require_type(tier["defaultPhases"], list, f"{path}.defaultPhases")
        _require_type(tier["requiredValidations"], list, f"{path}.requiredValidations")
        _require_type(tier["conditionalStages"], list, f"{path}.conditionalStages")
        _require_type(tier["upgradeTriggers"], list, f"{path}.upgradeTriggers")

    skills = data["skills"]
    _require_type(skills, dict, "skills")
    for skill_name, skill in skills.items():
        path = f"skills.{skill_name}"
        _require_type(skill, dict, path)
        _reject_unknown(skill, SKILL_KEYS, path)
        _require_type(skill["phase"], str, f"{path}.phase")
        for key in ("inputs", "artifacts", "events", "allowedInteractions", "capabilities"):
            _require_type(skill[key], list, f"{path}.{key}")

    for section in ("requiredArtifacts", "requiredValidations"):
        section_data = data[section]
        _require_type(section_data, dict, section)
        for phase, items in section_data.items():
            _require_type(items, list, f"{section}.{phase}")

    validation_phases = data["validationPhases"]
    _require_type(validation_phases, dict, "validationPhases")
    for verification, phase in validation_phases.items():
        _require_type(phase, str, f"validationPhases.{verification}")

    capability_gates = data["capabilityGates"]
    _require_type(capability_gates, dict, "capabilityGates")
    for capability, gate in capability_gates.items():
        path = f"capabilityGates.{capability}"
        _require_type(gate, dict, path)
        _reject_unknown(gate, CAPABILITY_GATE_KEYS, path)
        for key in CAPABILITY_GATE_KEYS:
            _require_type(gate[key], list, f"{path}.{key}")

    conditional = data["conditionalStages"]
    _require_type(conditional, dict, "conditionalStages")
    for stage_name, stage in conditional.items():
        path = f"conditionalStages.{stage_name}"
        _require_type(stage, dict, path)
        _reject_unknown(stage, CONDITIONAL_STAGE_KEYS, path)
        _require_type(stage["tiers"], list, f"{path}.tiers")
        _require_type(stage["signals"], list, f"{path}.signals")

    interaction = data["interactionWhitelist"]
    _require_type(interaction, dict, "interactionWhitelist")
    for key, items in interaction.items():
        _require_type(items, list, f"interactionWhitelist.{key}")

    checkpoints = data["checkpointRules"]
    _require_type(checkpoints, dict, "checkpointRules")
    for checkpoint_id, rule in checkpoints.items():
        path = f"checkpointRules.{checkpoint_id}"
        _require_type(rule, dict, path)
        _reject_unknown(rule, CHECKPOINT_RULE_KEYS, path)
        _require_type(rule["afterTasks"], list, f"{path}.afterTasks")
        _require_type(rule["beforeTasks"], list, f"{path}.beforeTasks")
        _require_type(rule["blocking"], bool, f"{path}.blocking")

    return data


def policy_path_for_repo(repo_root: Path) -> Path:
    return repo_root / POLICY_REL


def load_policy(repo_root: Path) -> dict[str, Any]:
    path = policy_path_for_repo(repo_root)
    if not path.is_file():
        raise FileNotFoundError(f"workflow policy missing: {path}")
    data = json.loads(path.read_text(encoding="utf-8-sig"))
    return validate_policy(data)
