import argparse
import json
import operator
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from typing import Iterable, Callable, Hashable

from drafteleu.releases import find_latest_release


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


def transform_pr(pr):
    pr["labels"] = [label["name"] for label in pr.get("labels", {}).get("nodes", [])]
    return pr


def unique_by_key[T](
    iterable: Iterable[T], key: Callable[[T], Hashable]
) -> Iterable[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 seen.values()


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}"])
    response = subprocess.run(
        args,
        capture_output=True,
        text=True,
        check=False,
    )
    if response.returncode != 0:
        raise RuntimeError(f"gh command failed: {response.stderr}")

    return json.loads(response.stdout)


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

    min_closed_at = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")
    after = None
    all_prs = []

    while merged_since and merged_since < min_closed_at:
        page = 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["closedAt"] 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["closedAt"] > merged_since]

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


def has_label(pr, label):
    return label in pr["labels"]


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


def compile_changelog(prs: list[dict]) -> 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


def main(app: str, include_prereleases: bool, repo: str | None) -> None:
    latest_release = find_latest_release(
        tag_regex=rf"^{app}/", include_prereleases=include_prereleases, repo=repo
    )
    sys.stderr.write(
        f"Latest release: {latest_release['tagName']} at {latest_release['publishedAt']}\n"
    )
    prs = get_prs([app], merged_since=latest_release["publishedAt"])
    sys.stderr.write(f"Found {len(prs)} PRs since last release\n")
    changelog = compile_changelog(prs)
    sys.stdout.write(changelog + "\n")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        "Generate changelog for an app or package since its last release",
    )
    parser.add_argument(
        "app",
        type=str,
        help="The app or package name to generate the changelog for",
    )
    parser.add_argument(
        "--include-prereleases",
        "--pre",
        action="store_true",
        help="Whether to include pre-releases when determining the latest release",
    )
    parser.add_argument(
        "--repo",
        "-r",
        type=str,
        help='GitHub repository in the format "owner/repo". Defaults to the current repository.',
        default=None,
    )

    args = parser.parse_args()

    main(args.app, args.include_prereleases, args.repo)
