"""Shared task→permission map for resolver and tool gates (#69).

``gitea_resolve_task_capability`` and issue-mutating MCP tools must agree on
which profile operation each task requires. This module is the single source
of truth; regression tests assert tool gates cannot drift from it.
"""

from __future__ import annotations

TASK_CAPABILITY_MAP: dict[str, dict[str, str]] = {
    "create_issue": {
        "permission": "gitea.issue.create",
        "role": "author",
    },
    "comment_issue": {
        "permission": "gitea.issue.comment",
        "role": "author",
    },
    "close_issue": {
        "permission": "gitea.issue.close",
        "role": "author",
    },
    "claim_issue": {
        "permission": "gitea.issue.comment",
        "role": "author",
    },
    "mark_issue": {
        "permission": "gitea.issue.comment",
        "role": "author",
    },
    "lock_issue": {
        "permission": "gitea.issue.comment",
        "role": "author",
    },
    "set_issue_labels": {
        "permission": "gitea.issue.comment",
        "role": "author",
    },
    "create_label": {
        "permission": "gitea.issue.comment",
        "role": "author",
    },
    "create_branch": {
        "permission": "gitea.branch.create",
        "role": "author",
    },
    "push_branch": {
        "permission": "gitea.branch.push",
        "role": "author",
    },
    "create_pr": {
        "permission": "gitea.pr.create",
        "role": "author",
    },
    "comment_pr": {
        "permission": "gitea.pr.comment",
        "role": "author",
    },
    "close_pr": {
        "permission": "gitea.pr.close",
        "role": "author",
    },
    # Non-closing PR metadata edits (title/body/base). Closing uses close_pr.
    "edit_pr": {
        "permission": "gitea.pr.create",
        "role": "author",
    },
    "gitea_edit_pr": {
        "permission": "gitea.pr.create",
        "role": "author",
    },
    "address_pr_change_requests": {
        "permission": "gitea.branch.push",
        "role": "author",
    },
    # PR synchronization lifecycle: assess is read-only (any role with gitea.read);
    # update-by-merge is author-only and mutates the PR head via Gitea API.
    "assess_pr_sync_status": {
        "permission": "gitea.read",
        "role": "author",
    },
    "gitea_assess_pr_sync_status": {
        "permission": "gitea.read",
        "role": "author",
    },
    "update_pr_branch_by_merge": {
        "permission": "gitea.branch.push",
        "role": "author",
    },
    "gitea_update_pr_branch_by_merge": {
        "permission": "gitea.branch.push",
        "role": "author",
    },
    "review_pr": {
        "permission": "gitea.pr.review",
        "role": "reviewer",
    },
    "submit_pr_review": {
        "permission": "gitea.pr.review",
        "role": "reviewer",
    },
    "merge_pr": {
        "permission": "gitea.pr.merge",
        "role": "merger",
    },
    "acquire_reviewer_pr_lease": {
        "permission": "gitea.pr.comment",
        "role": "reviewer",
    },
    "gitea_acquire_reviewer_pr_lease": {
        "permission": "gitea.pr.comment",
        "role": "reviewer",
    },
    # #695 AC8: controller quarantine of contaminated formal reviews.
    # Apply path posts an append-only forensic audit comment (pr.comment).
    "quarantine_contaminated_review": {
        "permission": "gitea.pr.comment",
        "role": "reconciler",
    },
    "gitea_quarantine_contaminated_review": {
        "permission": "gitea.pr.comment",
        "role": "reconciler",
    },
    "adopt_merger_pr_lease": {
        "permission": "gitea.pr.comment",
        "role": "merger",
    },
    "gitea_adopt_merger_pr_lease": {
        "permission": "gitea.pr.comment",
        "role": "merger",
    },
    "acquire_merger_pr_lease": {
        "permission": "gitea.pr.comment",
        "role": "merger",
    },
    "gitea_acquire_merger_pr_lease": {
        "permission": "gitea.pr.comment",
        "role": "merger",
    },
    # #742: owner-session terminal release/abandon of a merger-held lease when
    # the merge does not occur. Apply path posts an append-only terminal lease
    # marker (gitea.pr.comment); merger-only, never a reviewer path.
    "release_merger_pr_lease": {
        "permission": "gitea.pr.comment",
        "role": "merger",
    },
    "gitea_release_merger_pr_lease": {
        "permission": "gitea.pr.comment",
        "role": "merger",
    },
    # #691: guarded non-owner cleanup of obsolete comment-backed reviewer leases.
    # Apply path posts lease release + audit comments (gitea.pr.comment).
    "cleanup_obsolete_reviewer_comment_lease": {
        "permission": "gitea.pr.comment",
        "role": "reviewer",
    },
    "gitea_cleanup_obsolete_reviewer_comment_lease": {
        "permission": "gitea.pr.comment",
        "role": "reviewer",
    },
    # #745: post-merge moot reviewer-lease cleanup is reconciler-owned. The
    # apply path posts an append-only terminal `phase: released` lease marker
    # (gitea.pr.comment), so holding the comment permission alone must not
    # authorize it — author, reviewer and merger fail closed on the role gate
    # even though their profiles carry gitea.pr.comment. The read-only
    # `apply=false` assessment deliberately stays reachable under gitea.read
    # inside the tool (the same convention as cleanup_stale_review_decision_lock
    # below), so any namespace can diagnose a stuck lease; only apply requires
    # this task plus the reconciler role.
    "cleanup_post_merge_moot_lease": {
        "permission": "gitea.pr.comment",
        "role": "reconciler",
    },
    "gitea_cleanup_post_merge_moot_lease": {
        "permission": "gitea.pr.comment",
        "role": "reconciler",
    },
    "blind_pr_queue_review": {
        "permission": "gitea.pr.review",
        "role": "reviewer",
    },
    "pr_queue_cleanup": {
        "permission": "gitea.pr.review",
        "role": "reviewer",
    },
    "pr-queue-cleanup": {
        "permission": "gitea.pr.review",
        "role": "reviewer",
    },
    "request_changes_pr": {
        "permission": "gitea.pr.request_changes",
        "role": "reviewer",
    },
    "approve_pr": {
        "permission": "gitea.pr.approve",
        "role": "reviewer",
    },
    # #594: clear durable #332 decision lock only when last terminal PR is
    # already merged/closed (moot). Apply path requires reviewer review
    # permission; assessment itself uses gitea.read inside the tool.
    "cleanup_stale_review_decision_lock": {
        "permission": "gitea.pr.review",
        "role": "reviewer",
    },
    "gitea_cleanup_stale_review_decision_lock": {
        "permission": "gitea.pr.review",
        "role": "reviewer",
    },
    # #693: read-only classification of durable decision locks (incl. open PRs).
    "diagnose_review_decision_lock": {
        "permission": "gitea.read",
        "role": "reviewer",
    },
    "gitea_diagnose_review_decision_lock": {
        "permission": "gitea.read",
        "role": "reviewer",
    },
    "authorize_review_correction": {
        "permission": "gitea.pr.review",
        "role": "reviewer",
    },
    "gitea_authorize_review_correction": {
        "permission": "gitea.pr.review",
        "role": "reviewer",
    },
    # #709: truthful absence-of-proof recovery (server-side auth + record + consume).
    # Dedicated mutation capability — gitea.read is insufficient (review 434 F1).
    "issue_irrecoverable_provenance_authorization": {
        "permission": "gitea.decision_lock.irrecoverable_recovery",
        "role": "reconciler",
    },
    "gitea_issue_irrecoverable_provenance_authorization": {
        "permission": "gitea.decision_lock.irrecoverable_recovery",
        "role": "reconciler",
    },
    "record_irrecoverable_decision_lock_provenance": {
        "permission": "gitea.decision_lock.irrecoverable_recovery",
        "role": "reconciler",
    },
    "gitea_record_irrecoverable_decision_lock_provenance": {
        "permission": "gitea.decision_lock.irrecoverable_recovery",
        "role": "reconciler",
    },
    "consume_irrecoverable_decision_lock_provenance": {
        "permission": "gitea.pr.merge",
        "role": "merger",
    },
    "gitea_consume_irrecoverable_decision_lock_provenance": {
        "permission": "gitea.pr.merge",
        "role": "merger",
    },
    # #729: delete_branch is reconciler-owned. gitea.branch.delete is granted
    # only to the reconciler profile, so the resolver must classify this task as
    # reconciler (previously "author", which no delete-capable profile held).
    # Raw gitea_delete_branch still redirects reconciler to the guarded
    # gitea_cleanup_merged_pr_branch path (#514/#687); author/reviewer/merger
    # stay denied by both the permission gate and this role gate.
    "delete_branch": {
        "permission": "gitea.branch.delete",
        "role": "reconciler",
    },
    "cleanup_merged_pr_branch": {
        "permission": "gitea.branch.delete",
        "role": "reconciler",
    },
    "commit_files": {
        "permission": "gitea.repo.commit",
        "role": "author",
    },
    "gitea_commit_files": {
        "permission": "gitea.repo.commit",
        "role": "author",
    },
    "reconcile_merged_cleanups": {
        "permission": "gitea.read",
        "role": "reconciler",
    },
    "reconciliation_cleanup": {
        "permission": "gitea.branch.delete",
        "role": "reconciler",
    },
    "work_issue": {
        "permission": "gitea.pr.create",
        "role": "author",
    },
    "work-issue": {
        "permission": "gitea.pr.create",
        "role": "author",
    },
    # #600: controller-owned allocator — any authenticated profile may call;
    # routing enforces role match to selected work. Uses control-plane DB (#613).
    "allocate_next_work": {
        "permission": "gitea.read",
        "role": "author",
    },
    "gitea_allocate_next_work": {
        "permission": "gitea.read",
        "role": "author",
    },

    # #601 first-class lease lifecycle — inspect/list need read; mutations gate on
    # ownership in the control-plane DB (not a separate Gitea write permission).
    "list_workflow_leases": {
        "permission": "gitea.read",
        "role": "author",
    },
    "gitea_list_workflow_leases": {
        "permission": "gitea.read",
        "role": "author",
    },
    "inspect_workflow_lease": {
        "permission": "gitea.read",
        "role": "author",
    },
    "gitea_inspect_workflow_lease": {
        "permission": "gitea.read",
        "role": "author",
    },
    "adopt_workflow_lease": {
        "permission": "gitea.read",
        "role": "author",
    },
    "gitea_adopt_workflow_lease": {
        "permission": "gitea.read",
        "role": "author",
    },
    "release_workflow_lease": {
        "permission": "gitea.read",
        "role": "author",
    },
    "gitea_release_workflow_lease": {
        "permission": "gitea.read",
        "role": "author",
    },
    "expire_workflow_leases": {
        "permission": "gitea.read",
        "role": "author",
    },
    "gitea_expire_workflow_leases": {
        "permission": "gitea.read",
        "role": "author",
    },
    "abandon_workflow_lease": {
        "permission": "gitea.read",
        "role": "author",
    },
    "gitea_abandon_workflow_lease": {
        "permission": "gitea.read",
        "role": "author",
    },
    "reclaim_expired_workflow_lease": {
        "permission": "gitea.read",
        "role": "author",
    },
    "gitea_reclaim_expired_workflow_lease": {
        "permission": "gitea.read",
        "role": "author",
    },
    # #612 incident bridge — reconcile uses create_issue for apply;
    # dry-run needs read only. Tools gate apply paths themselves.
    "observability_reconcile_incident": {
        "permission": "gitea.read",
        "role": "author",
    },
    "gitea_observability_reconcile_incident": {
        "permission": "gitea.read",
        "role": "author",
    },
    "observability_list_projects": {
        "permission": "gitea.read",
        "role": "author",
    },
    "gitea_observability_list_projects": {
        "permission": "gitea.read",
        "role": "author",
    },
    "observability_link_issue": {
        "permission": "gitea.read",
        "role": "author",
    },
    "gitea_observability_link_issue": {
        "permission": "gitea.read",
        "role": "author",
    },


    "reconcile_landed_pr": {
        "permission": "gitea.read",
        "role": "author",
    },
    "reconcile-landed-pr": {
        "permission": "gitea.read",
        "role": "author",
    },
    "reconcile_already_landed_pr": {
        "permission": "gitea.pr.close",
        "role": "reconciler",
    },
    # #309: dedicated reconciler path for already-landed open PRs. Exact
    # close capabilities only — never review/approve/request_changes/merge.
    "reconcile_close_landed_pr": {
        "permission": "gitea.pr.close",
        "role": "reconciler",
    },
    "reconcile_close_landed_issue": {
        "permission": "gitea.issue.close",
        "role": "reconciler",
    },
    "reconcile_close_superseded_pr": {
        "permission": "gitea.pr.close",
        "role": "reconciler",
    },
    "reconcile_close_satisfied_issue": {
        "permission": "gitea.issue.close",
        "role": "reconciler",
    },
    "reconcile_create_followup_issue": {
        "permission": "gitea.issue.create",
        "role": "reconciler",
    },
    "post_heartbeat": {
        "permission": "gitea.issue.comment",
        "role": "author",
    },
    "reconcile_issue_claims": {
        "permission": "gitea.read",
        "role": "author",
    },
    "cleanup_stale_claims": {
        "permission": "gitea.issue.comment",
        "role": "author",
    },
}


# A reviewer lease is the first mutation in the canonical ``review_pr``
# workflow, so the already-resolved review capability is valid for that one
# narrower transition. Keep this directed and explicit: lease acquisition
# does not authorize a review verdict, and reviewer proof never authorizes a
# merger lease (#763).
_PREFLIGHT_TASK_TRANSITIONS = frozenset({
    ("review_pr", "acquire_reviewer_pr_lease"),
})


def _canonical_preflight_task(task: str | None) -> str:
    """Normalize only declared ``gitea_`` aliases for preflight comparison."""
    value = (task or "").strip()
    if value.startswith("gitea_") and value[6:] in TASK_CAPABILITY_MAP:
        return value[6:]
    return value


def preflight_task_matches(
    resolved_task: str | None,
    mutation_task: str | None,
) -> bool:
    """Return whether capability proof authorizes this mutation transition."""
    resolved = _canonical_preflight_task(resolved_task)
    mutation = _canonical_preflight_task(mutation_task)
    if not resolved or not mutation:
        return False
    return resolved == mutation or (resolved, mutation) in _PREFLIGHT_TASK_TRANSITIONS


# Tasks for which permission alone is insufficient: the active/configured
# profile's declared role must also match the task role.  This is the complete
# resolver set from master at the #723 reconstruction point, shared with
# runtime reporting so those two authorities cannot drift again.
ROLE_EXCLUSIVE_TASKS: frozenset[str] = frozenset(
    {
        "acquire_reviewer_pr_lease",
        "gitea_acquire_reviewer_pr_lease",
        "review_pr",
        "approve_pr",
        "request_changes_pr",
        "blind_pr_queue_review",
        "pr_queue_cleanup",
        "pr-queue-cleanup",
        "merge_pr",
        "acquire_merger_pr_lease",
        "gitea_acquire_merger_pr_lease",
        "adopt_merger_pr_lease",
        "gitea_adopt_merger_pr_lease",
        "release_merger_pr_lease",
        "gitea_release_merger_pr_lease",
        "create_branch",
        "push_branch",
        "create_pr",
        "commit_files",
        "gitea_commit_files",
        "address_pr_change_requests",
        "update_pr_branch_by_merge",
        "gitea_update_pr_branch_by_merge",
        "delete_branch",
        "cleanup_merged_pr_branch",
        "reconciliation_cleanup",
        "work_issue",
        "work-issue",
    }
)

# Issue-mutating MCP tools and their resolver task keys.
ISSUE_MUTATION_TOOL_TASKS: dict[str, str] = {
    "gitea_create_issue": "create_issue",
    "gitea_close_issue": "close_issue",
    "gitea_create_issue_comment": "comment_issue",
    "gitea_mark_issue": "mark_issue",
    "gitea_set_issue_labels": "set_issue_labels",
    "gitea_create_label": "create_label",
    "gitea_commit_files": "commit_files",
}


def required_permission(task: str) -> str:
    """Return the canonical operation a *task* requires (fail closed)."""
    try:
        return TASK_CAPABILITY_MAP[task]["permission"]
    except KeyError as exc:
        raise KeyError(f"Unknown task/action: {task!r} (fail closed)") from exc


def required_role(task: str) -> str:
    """Return author/reviewer role kind for *task* (fail closed)."""
    try:
        return TASK_CAPABILITY_MAP[task]["role"]
    except KeyError as exc:
        raise KeyError(f"Unknown task/action: {task!r} (fail closed)") from exc


def tool_required_permission(tool_name: str) -> str:
    """Return the operation an issue-mutating tool must gate on."""
    return required_permission(ISSUE_MUTATION_TOOL_TASKS[tool_name])
