"""Cross-repo mutation preflight (#98).

Gates mutation tools that take an explicit `(owner, repo)` target
against the project's declared `related_repos` graph. Enforces the
§Cross-repo work protocol from active-claude-github.md §Multi-repo
relations (shipped via #97):

  subsystem        → allow        (lifecycle-coupled with parent)
  publish_target   → allow        (downstream one-way distribution)
  sibling          → allow        (same-org skill-managed peer)
  cousin           → BLOCK        (cross-org — explicit user permission)
  neighbor         → allow        (user-authorized non-skill repo)
  foreign          → BLOCK        (no write access — treat as read-only)

Unknown targets (not in related_repos) default to allow — the preflight
does not invent policy for repos the user hasn't declared. The skill's
primary/secondary issue discipline catches unknown-target work at the
protocol layer.

The block path raises `CrossRepoMutationBlocked` carrying a structured
`manual_step` message; dispatch in `server/build.py` translates the
exception into an `OperationError(manual_step=...)` response so the
caller sees a non-buffered failure with an actionable instruction.
"""
from __future__ import annotations

from dataclasses import dataclass

from ..models import GithubConfig
from ..parsers import parse_owner_repo


@dataclass(frozen=True)
class CrossRepoMutationBlocked(Exception):
    """Raised when a mutation targets a repo the protocol forbids
    without explicit user permission (`cousin`) or at all (`foreign`)."""

    target_repo: str
    relationship: str
    manual_step: str

    def __str__(self) -> str:  # pragma: no cover — trivial
        return self.manual_step


# Relationships that require no gate (allowed automatically).
_ALLOWED_RELATIONSHIPS = frozenset(
    {"subsystem", "publish_target", "sibling", "neighbor", "contribution_target"}
)

# Relationships that require a gate (blocked without explicit permission).
_BLOCKED_RELATIONSHIPS = frozenset({"cousin", "foreign"})


def check_cross_repo_mutation(
    config: GithubConfig, owner: str, repo: str
) -> None:
    """Raise CrossRepoMutationBlocked if (owner, repo) is a gated target.

    `subsystem`, `publish_target`, `sibling`, `neighbor`, and the
    deprecated `contribution_target` pass through silently. The
    project's own public/private repos pass through by short-circuit
    (they are never in `related_repos`). `cousin` and `foreign` raise
    with an actionable `manual_step`.
    """
    target_fullname = f"{owner}/{repo}"

    # Own repos (public/private pair) are always allowed.
    own_public_owner, own_public = parse_owner_repo(config.repos.public)
    own_private_owner, own_private = parse_owner_repo(config.repos.private)
    if (
        (owner, repo) == (own_public_owner, own_public)
        or (owner, repo) == (own_private_owner, own_private)
    ):
        return

    entry = config.find_related_repo(target_fullname)
    if entry is None:
        # Unknown target — allow. The skill protocol handles discovery.
        return

    if entry.relationship in _ALLOWED_RELATIONSHIPS:
        return

    if entry.relationship == "cousin":
        raise CrossRepoMutationBlocked(
            target_repo=target_fullname,
            relationship="cousin",
            manual_step=(
                f"Cross-org mutation on '{target_fullname}' requires "
                "explicit user permission per §Cross-repo work protocol "
                "(active-claude-github.md §Multi-repo relations, #97). "
                "The target is declared as a 'cousin' in related_repos. "
                "Pause and ask the user before re-running."
            ),
        )

    if entry.relationship == "foreign":
        raise CrossRepoMutationBlocked(
            target_repo=target_fullname,
            relationship="foreign",
            manual_step=(
                f"Cannot mutate '{target_fullname}' — declared as "
                "'foreign' in related_repos, meaning no write access. "
                "Record the dependency in the primary Issue's "
                "## Related issues section instead."
            ),
        )

    # Defensive — every type in RelationshipType should be covered above.
    raise CrossRepoMutationBlocked(
        target_repo=target_fullname,
        relationship=entry.relationship,
        manual_step=(
            f"Unhandled relationship type {entry.relationship!r} for "
            f"'{target_fullname}'. Treat as blocked pending protocol "
            "clarification."
        ),
    )
