import json
import operator
from datetime import datetime
from pathlib import Path
from drafteleu.async_utils import run_process_async
from typing import Hashable, NamedTuple
from collections.abc import Iterable, Callable
from datetime import UTC


PATH_TO_GRAPHQL_QUERY = Path(__file__).parent.joinpath("queries")


class PullRequest(NamedTuple):
    author: dict
    closed_at: datetime
    labels: list[str]
    number: int
    title: str


def transform_pr(pr: dict) -> PullRequest:
    return PullRequest(
        number=pr["number"],
        author=pr["author"],
        title=pr["title"],
        labels=[label["name"] for label in pr.get("labels", {}).get("nodes", [])],
        closed_at=datetime.fromisoformat(pr["closedAt"]),
    )


def unique_by_key[T](iterable: Iterable[T], key: Callable[[T], Hashable]) -> list[T]:
    """
    Remove duplicates from an iterable based on a key function.
    """
    seen = {}
    for item in iterable:
        if (k := key(item)) not in seen:
            seen[k] = item
    return list(seen.values())


async def graphql_query(query_name: str, **kwargs):
    args = [
        "gh",
        "api",
        "graphql",
        "-F",
        f"query=@{PATH_TO_GRAPHQL_QUERY.joinpath(f'{query_name}.graphql').as_posix()}",
    ]
    for k, v in kwargs.items():
        if isinstance(v, list):
            for item in v:
                args.extend(["-F", f"{k}[]={item}"])
        elif v is not None:
            args.extend(["-F", f"{k}={v}"])

    return_code, stdout, stderr = await run_process_async(args)
    if return_code != 0:
        raise RuntimeError(f"gh command failed: {stderr}")

    return json.loads(stdout)


async def get_prs(
    labels: list[str], *, merged_since: datetime | None = None, repo: str
) -> list[PullRequest]:
    owner, repo_name = repo.split("/")

    min_closed_at = datetime.now(UTC)
    after = None
    all_prs = []

    while merged_since and merged_since < min_closed_at:
        page = await graphql_query(
            "get_prs",
            repository=repo_name,
            owner=owner,
            labels=labels,
            after=after,
        )
        prs = [
            transform_pr(item["node"])
            for item in page["data"]["repository"]["pullRequests"]["edges"]
            if item["node"].get("closedAt")
        ]

        min_closed_at = min((pr.closed_at for pr in prs), default=min_closed_at)

        all_prs.extend(prs)
        page_info = page["data"]["repository"]["pullRequests"]["pageInfo"]
        if page_info["hasNextPage"]:
            after = page_info["endCursor"]
        else:
            break

    if merged_since:
        all_prs = [pr for pr in all_prs if pr.closed_at > merged_since]

    return unique_by_key(all_prs, key=operator.attrgetter("number"))


def has_label(pr: PullRequest, label: str) -> bool:
    return label in pr.labels


def format_author(author):
    return author["login"].removeprefix("app/")


def compile_changelog(prs: list[PullRequest]) -> str:
    changelog = "## What’s Changed\n\n"
    for pr in filter(lambda pr: not has_label(pr, "dependencies"), prs):
        changelog += f"* {pr.title} (#{pr.number}) @{format_author(pr.author)}\n"

    dependencies = [pr for pr in prs if has_label(pr, "dependencies")]
    if dependencies:
        changelog += "\n## :arrow_up: Dependencies\n\n"
        for pr in dependencies:
            changelog += f"* {pr.title} (#{pr.number}) @{format_author(pr.author)}\n"
    return changelog
