"""Fail-closed subagent delegation gates (#266).

Subagents can bypass or lose context for worktree, capability, mutation,
retry, and reporting rules. These helpers make delegation an explicit,
provable decision instead of a default: deterministic write tasks stay
inline unless the parent session records why a subagent is needed and
hands the subagent the full gate context it must operate under.

Like ``author_proofs``/``review_proofs``, the helpers are pure (no git,
no API calls): the parent workflow gathers the facts and passes them in,
so the same logic works from prompts, harness assertions, and tests.
Nothing here weakens the review/merge/permission gates — a delegated
subagent is subject to the same gates as its parent.
"""

# AC1: deterministic write workflows a subagent must never run by default.
DETERMINISTIC_WRITE_TASKS = frozenset({
    "claim_issue",
    "create_branch",
    "edit_code",
    "commit",
    "push_branch",
    "create_pr",
    "review_pr",
    "merge_pr",
    "cleanup_branch",
    "close_issue",
})

# Read-only delegation that needs no explicit authorization.
READ_ONLY_TASKS = frozenset({
    "read_files",
    "code_search",
    "inventory_prs",
    "summarize_issue",
    "explore_codebase",
})

# AC3: context a subagent must inherit from the parent session before any
# authorized write delegation may proceed.
REQUIRED_INHERITED_CONTEXT = (
    "issue_lock",
    "branch",
    "worktree_path",
    "identity_profile",
    "allowed_tool_class",
    "command_deny_list",
    "validation_ledger_requirement",
    "final_report_schema",
)

# AC4: proof fields a subagent final report must carry — the same fields a
# parent workflow's final report requires.
REQUIRED_SUBAGENT_REPORT_FIELDS = (
    "identity_profile",
    "worktree_path",
    "branch",
    "changed_files",
    "validation_results",
    "workspace_mutations",
)


def _clean(value):
    return (value or "").strip() if isinstance(value, str) else value


def _classify_task(task_type):
    task = _clean(task_type)
    if not task:
        return "", "unknown"
    if task in DETERMINISTIC_WRITE_TASKS:
        return task, "deterministic_write"
    if task in READ_ONLY_TASKS:
        return task, "read_only"
    return task, "unknown"


def _missing_context_fields(inherited_context):
    context = inherited_context or {}
    missing = []
    for field in REQUIRED_INHERITED_CONTEXT:
        value = context.get(field)
        if not isinstance(value, str) or not value.strip():
            missing.append(field)
    return missing


def assess_subagent_delegation(task_type, *, explicitly_allowed=False,
                               justification=None, inherited_context=None):
    """Decide whether delegating *task_type* to a subagent may proceed.

    Fail closed: unknown tasks are blocked; deterministic write tasks are
    blocked unless explicitly allowed (AC1) with a recorded justification
    (AC2) and the full inherited gate context (AC3). Read-only delegation
    is allowed without explicit authorization.

    Returns {'block', 'allowed', 'task_type', 'task_class', 'reasons',
    'missing_context'}.
    """
    task, task_class = _classify_task(task_type)
    reasons = []
    missing_context = []

    if task_class == "unknown":
        reasons.append(
            f"task type '{task}' is not a recognized delegation class; "
            "run it inline in the parent session (fail closed)"
        )
    elif task_class == "deterministic_write":
        if not explicitly_allowed:
            reasons.append(
                f"deterministic write task '{task}' must run inline unless "
                "subagent use is explicitly allowed by the parent session"
            )
        if not _clean(justification):
            reasons.append(
                "no recorded justification for why a subagent is needed; "
                "the parent session must record one before delegating"
            )
        missing_context = _missing_context_fields(inherited_context)
        if missing_context:
            reasons.append(
                "subagent would not inherit required gate context: "
                + ", ".join(missing_context)
            )

    block = bool(reasons)
    return {
        "block": block,
        "allowed": not block,
        "task_type": task,
        "task_class": task_class,
        "reasons": reasons,
        "missing_context": missing_context,
    }


def validate_subagent_report(report_fields):
    """AC4: accept subagent output only with the parent-grade proof fields.

    *report_fields* maps field name -> reported value. Missing or blank
    proof fields make the report invalid (fail closed).

    Returns {'valid', 'block', 'reasons', 'missing_fields'}.
    """
    reasons = []
    report = report_fields or {}
    missing = []
    for field in REQUIRED_SUBAGENT_REPORT_FIELDS:
        value = report.get(field)
        if not isinstance(value, str) or not value.strip():
            missing.append(field)
    if missing:
        reasons.append(
            "subagent final report missing required proof fields: "
            + ", ".join(missing)
        )

    valid = not reasons
    return {
        "valid": valid,
        "block": not valid,
        "reasons": reasons,
        "missing_fields": missing,
    }
