"""GitHub node-ID resolvers and generic file fetch (#83)."""
from __future__ import annotations

from .core import GitHubError, InvalidNodeIdError, _graphql


def validate_node_id(token: str, param_name: str, node_id: str) -> None:
    """Raise InvalidNodeIdError if node_id does not resolve on GitHub.

    Cheap existence query (`node(id: $id) { id }`). Transient GitHub
    failures (HTTP 5xx, network) propagate as GitHubError and remain
    retry-eligible at the tool layer.
    """
    if not node_id.strip():
        raise ValueError(f"{param_name} must not be empty.")

    query = "query($id: ID!) { node(id: $id) { id } }"
    try:
        data = _graphql(token, query, {"id": node_id})
    except GitHubError as exc:
        # GitHub rejects a syntactically-invalid ID with `errors` rather
        # than returning `data.node = null`. Treat "Could not resolve"
        # error messages as client-side (invalid ID), everything else as
        # transient.
        if "Could not resolve to a node" in str(exc):
            raise InvalidNodeIdError(param_name, node_id) from exc
        raise

    if not (data.get("node") or {}).get("id"):
        raise InvalidNodeIdError(param_name, node_id)


def get_repo_node_id(token: str, owner: str, repo: str) -> str:
    """Return the GraphQL node ID for a repository."""
    if not owner.strip() or not repo.strip():
        raise ValueError("owner and repo must not be empty.")

    query = """
    query GetRepoId($owner: String!, $repo: String!) {
      repository(owner: $owner, name: $repo) { id }
    }
    """
    data = _graphql(token, query, {"owner": owner, "repo": repo})
    repo_node = data.get("repository")
    if not repo_node:
        raise GitHubError(f"Repository {owner}/{repo} not found.")
    return repo_node["id"]


def resolve_thread_id(
    token: str,
    owner: str,
    repo: str,
    number: int,
    kind: str,
) -> str:
    """Resolve (owner, repo, number) to an Issue or Discussion node ID.

    kind must be "issue" or "discussion". Raises GitHubError if the
    thread is not found.
    """
    if not owner.strip() or not repo.strip():
        raise ValueError("owner and repo must not be empty.")
    if not isinstance(number, int) or number < 1:
        raise ValueError("number must be a positive integer.")
    if kind not in ("issue", "discussion"):
        raise ValueError(f"kind must be 'issue' or 'discussion', got: {kind!r}")

    if kind == "issue":
        query = """
        query($owner: String!, $repo: String!, $number: Int!) {
          repository(owner: $owner, name: $repo) {
            issue(number: $number) { id }
          }
        }
        """
    else:
        query = """
        query($owner: String!, $repo: String!, $number: Int!) {
          repository(owner: $owner, name: $repo) {
            discussion(number: $number) { id }
          }
        }
        """
    data = _graphql(
        token, query, {"owner": owner, "repo": repo, "number": number}
    )
    repo_node = data.get("repository") or {}
    node = repo_node.get(kind) or {}
    node_id = node.get("id")
    if not node_id:
        raise GitHubError(
            f"{kind.capitalize()} {owner}/{repo}#{number} not found."
        )
    return node_id


def get_repo_visibility(token: str, owner: str, repo: str) -> str | None:
    """Return the GraphQL `visibility` of a repo (`PUBLIC`/`PRIVATE`/`INTERNAL`).

    Returns None if the repo does not exist (distinguishes "missing" from
    transient API errors, which still raise GitHubError). Used by the
    repo-shape audit in `open_session` (#116) to evaluate whether each
    managed repo's current visibility matches §Step 3.5 compliance rules.
    """
    if not owner.strip() or not repo.strip():
        raise ValueError("owner and repo must not be empty.")

    query = """
    query($owner: String!, $repo: String!) {
      repository(owner: $owner, name: $repo) { visibility }
    }
    """
    data = _graphql(token, query, {"owner": owner, "repo": repo})
    repo_node = data.get("repository")
    if not repo_node:
        return None
    return repo_node.get("visibility")


def get_file_content(
    token: str,
    owner: str,
    repo: str,
    ref: str,
    path: str,
) -> str | None:
    """Fetch the text of a file from a repo at a given ref (#60).

    Returns the file's text content, or None if the file does not exist
    at that ref (distinguishes "upstream doesn't have the file" from
    transient API errors, which still raise GitHubError).

    Uses GraphQL's `object(expression: "ref:path")` pattern so auth is
    consistent with the rest of the server (no separate raw.github-
    usercontent.com fetch needed for private repos).
    """
    if not owner.strip() or not repo.strip():
        raise ValueError("owner and repo must not be empty.")
    if not ref.strip():
        raise ValueError("ref must not be empty.")
    if not path.strip():
        raise ValueError("path must not be empty.")

    expression = f"{ref}:{path}"
    query = """
    query($owner: String!, $repo: String!, $expr: String!) {
      repository(owner: $owner, name: $repo) {
        object(expression: $expr) {
          ... on Blob { text }
        }
      }
    }
    """
    data = _graphql(
        token, query, {"owner": owner, "repo": repo, "expr": expression}
    )
    repo_node = data.get("repository") or {}
    obj = repo_node.get("object")
    if obj is None:
        return None
    return obj.get("text")
