"""GitHub ProjectsV2 operations (#83)."""
from __future__ import annotations

from .core import GitHubError, _graphql


def list_owner_projects(token: str, owner_id: str) -> list[dict]:
    """Return the Projects V2 boards owned by an Org or User.

    Returns: list of {"id": str, "number": int, "title": str, "url": str}.

    scaffold_project v2 uses this to detect a pre-existing board with
    the target title, so a re-run of the tool is idempotent instead of
    creating a duplicate.
    """
    if not owner_id.strip():
        raise ValueError("owner_id must not be empty.")

    query = """
    query($ownerId: ID!) {
      node(id: $ownerId) {
        ... on Organization {
          projectsV2(first: 50) {
            nodes { id number title url }
          }
        }
        ... on User {
          projectsV2(first: 50) {
            nodes { id number title url }
          }
        }
      }
    }
    """
    data = _graphql(token, query, {"ownerId": owner_id})
    node = data.get("node") or {}
    nodes = ((node.get("projectsV2") or {}).get("nodes")) or []
    return [
        {
            "id": n["id"],
            "number": n["number"],
            "title": n["title"],
            "url": n["url"],
        }
        for n in nodes
        if n.get("id")
    ]


def create_projectv2(token: str, owner_id: str, title: str) -> dict:
    """Create a Projects V2 board under an Org or User.

    Returns: {"id": str, "number": int, "url": str}.

    The new board gets a default Status field with GitHub's default
    options (Todo / In Progress / Done); scaffold_project v2 then
    rewrites those options via `update_projectv2_status_options`.
    """
    if not owner_id.strip():
        raise ValueError("owner_id must not be empty.")
    if not title.strip():
        raise ValueError("title must not be empty.")

    query = """
    mutation($ownerId: ID!, $title: String!) {
      createProjectV2(input: {ownerId: $ownerId, title: $title}) {
        projectV2 { id number url }
      }
    }
    """
    data = _graphql(token, query, {"ownerId": owner_id, "title": title})
    project = (data.get("createProjectV2") or {}).get("projectV2") or {}
    if not project.get("id"):
        raise GitHubError("createProjectV2 returned no project.")
    return {
        "id": project["id"],
        "number": project["number"],
        "url": project["url"],
    }


def link_projectv2_to_repo(token: str, project_id: str, repo_id: str) -> bool:
    """Link a Projects V2 board to a repository.

    Idempotent on GitHub's side — re-linking the same repo is a no-op.
    """
    if not project_id.strip():
        raise ValueError("project_id must not be empty.")
    if not repo_id.strip():
        raise ValueError("repo_id must not be empty.")

    query = """
    mutation($projectId: ID!, $repoId: ID!) {
      linkProjectV2ToRepository(input: {
        projectId: $projectId, repositoryId: $repoId
      }) {
        repository { id }
      }
    }
    """
    _graphql(token, query, {"projectId": project_id, "repoId": repo_id})
    return True


# Standard Status options for active-claude-github projects. Listed in
# intended display order (left-to-right on the board).
STATUS_OPTIONS: tuple[str, ...] = (
    "Backlog",
    "In Progress",
    "In Review",
    "Done",
    "Parked",
)


def update_projectv2_status_options(
    token: str,
    project_id: str,
    field_id: str,
    desired_names: list[str] | tuple[str, ...] = STATUS_OPTIONS,
) -> dict:
    """Replace the Status field options with `desired_names` (read-diff-write, #6).

    Reads the current Status options, no-ops if the set already matches
    `desired_names`, otherwise issues a single `updateProjectV2Field`
    mutation.

    WARNING: the mutation replaces the full option set. Items assigned
    to options not in `desired_names` will lose their Status. Safe for
    a freshly-created board; scaffold_project v2 guards the caller
    against clobbering a non-empty pre-existing board.

    Returns: {"field_id": str, "options": {name: id, ...}, "changed": bool}.
    """
    if not project_id.strip():
        raise ValueError("project_id must not be empty.")
    if not field_id.strip():
        raise ValueError("field_id must not be empty.")
    desired = list(desired_names)
    if not desired:
        raise ValueError("desired_names must not be empty.")
    if len(set(desired)) != len(desired):
        raise ValueError("desired_names must be unique.")

    current = get_project_status_field(token, project_id)
    current_names = list(current["options"].keys())
    if current_names == desired:
        return {
            "field_id": current["field_id"],
            "options": current["options"],
            "changed": False,
        }

    options_input = [{"name": name, "color": "GRAY", "description": ""} for name in desired]
    query = """
    mutation($fieldId: ID!, $options: [ProjectV2SingleSelectFieldOptionInput!]!) {
      updateProjectV2Field(input: {
        fieldId: $fieldId,
        singleSelectOptions: $options
      }) {
        projectV2Field {
          ... on ProjectV2SingleSelectField {
            id
            options { id name }
          }
        }
      }
    }
    """
    data = _graphql(token, query, {"fieldId": field_id, "options": options_input})
    field = (
        (data.get("updateProjectV2Field") or {}).get("projectV2Field") or {}
    )
    options = field.get("options") or []
    return {
        "field_id": field.get("id", field_id),
        "options": {opt["name"]: opt["id"] for opt in options},
        "changed": True,
    }


def add_item_to_project(token: str, project_id: str, content_id: str) -> str:
    """
    Add an Issue or PR to a Projects v2 board.

    Returns the project item node ID.
    """
    if not project_id.strip():
        raise ValueError("project_id must not be empty.")
    if not content_id.strip():
        raise ValueError("content_id must not be empty.")

    query = """
    mutation AddToProject($projectId: ID!, $contentId: ID!) {
      addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) {
        item { id }
      }
    }
    """
    data = _graphql(token, query, {"projectId": project_id, "contentId": content_id})
    return data["addProjectV2ItemById"]["item"]["id"]


def get_project_status_field(token: str, project_id: str) -> dict:
    """
    Return the Status field ID and option IDs for a project board.

    Returns: {"field_id": str, "options": {"name": "id", ...}}
    """
    if not project_id.strip():
        raise ValueError("project_id must not be empty.")

    query = """
    query GetProjectFields($projectId: ID!) {
      node(id: $projectId) {
        ... on ProjectV2 {
          fields(first: 20) {
            nodes {
              ... on ProjectV2SingleSelectField {
                id
                name
                options { id name }
              }
            }
          }
        }
      }
    }
    """
    data = _graphql(token, query, {"projectId": project_id})
    fields = data["node"]["fields"]["nodes"]
    for field in fields:
        if field.get("name") == "Status":
            return {
                "field_id": field["id"],
                "options": {opt["name"]: opt["id"] for opt in field["options"]},
            }
    raise GitHubError("Status field not found on project board.")


def get_project_item_id(
    token: str,
    issue_id: str,
    project_id: str,
) -> str:
    """Resolve an Issue node ID to its ProjectV2Item ID on a given board.

    Raises GitHubError if the Issue is not on the given project board,
    or if the Issue node cannot be loaded.
    """
    if not issue_id.strip():
        raise ValueError("issue_id must not be empty.")
    if not project_id.strip():
        raise ValueError("project_id must not be empty.")

    query = """
    query ResolveProjectItem($issueId: ID!) {
      node(id: $issueId) {
        ... on Issue {
          projectItems(first: 20) {
            nodes {
              id
              project { id }
            }
          }
        }
      }
    }
    """
    data = _graphql(token, query, {"issueId": issue_id})
    node = data.get("node") or {}
    items = ((node.get("projectItems") or {}).get("nodes")) or []
    for item in items:
        if (item.get("project") or {}).get("id") == project_id:
            return item["id"]
    raise GitHubError(
        f"Issue {issue_id} is not on project {project_id}."
    )


def move_project_item(
    token: str,
    project_id: str,
    item_id: str,
    field_id: str,
    option_id: str,
) -> bool:
    """Move a project item to a specific Status column."""
    for name, value in [
        ("project_id", project_id),
        ("item_id", item_id),
        ("field_id", field_id),
        ("option_id", option_id),
    ]:
        if not value.strip():
            raise ValueError(f"{name} must not be empty.")

    query = """
    mutation MoveItem(
      $projectId: ID!,
      $itemId: ID!,
      $fieldId: ID!,
      $optionId: String!
    ) {
      updateProjectV2ItemFieldValue(input: {
        projectId: $projectId,
        itemId: $itemId,
        fieldId: $fieldId,
        value: {singleSelectOptionId: $optionId}
      }) {
        projectV2Item { id }
      }
    }
    """
    _graphql(
        token,
        query,
        {
            "projectId": project_id,
            "itemId": item_id,
            "fieldId": field_id,
            "optionId": option_id,
        },
    )
    return True
