import json
import re
from packaging.version import Version
from drafteleu.async_utils import get_async_logger, run_process_async
from datetime import datetime

from typing import Literal, NamedTuple
from async_lru import alru_cache

logger = get_async_logger(__name__)


class Release(NamedTuple):
    is_draft: bool
    published_at: datetime
    tag_name: str


class ReleaseDetail(NamedTuple):
    body: str
    name: str
    tag_name: str


@alru_cache
async def fetch_releases(
    repo: str, *, include_prereleases: bool = True
) -> list[Release]:
    return_code, stdout, stderr = await run_process_async(
        [
            "gh",
            "release",
            "list",
            "--limit",
            "1000",
            "--json",
            "tagName,publishedAt,isDraft",
            *(["--repo", repo] if repo else []),
            *(["--exclude-pre-releases"] if not include_prereleases else []),
        ],
    )

    if return_code != 0:
        raise RuntimeError(f"gh command failed: {stderr}")
    releases = json.loads(stdout)

    return [
        Release(
            tag_name=release["tagName"],
            is_draft=release["isDraft"],
            published_at=datetime.fromisoformat(release["publishedAt"]),
        )
        for release in releases
    ]


async def get_filtered_releases(
    tag_regex: str,
    release_state: Literal["draft", "published"] = "published",
    include_prereleases=True,
    repo=None,
) -> list[Release]:
    if release_state not in ("draft", "published"):
        raise ValueError("`release_state` must be either 'draft' or 'published'")

    releases = [
        release
        for release in await fetch_releases(
            repo, include_prereleases=include_prereleases
        )
        if re.match(tag_regex, release.tag_name)
    ]

    if release_state == "draft":
        releases = [release for release in releases if release.is_draft]
    elif release_state == "published":
        releases = [release for release in releases if not release.is_draft]

    return releases


async def find_latest_release(
    tag_regex: str, include_prereleases=True, repo=None
) -> Release | None:
    relevant_releases = await get_filtered_releases(
        tag_regex,
        release_state="published",
        include_prereleases=include_prereleases,
        repo=repo,
    )

    relevant_releases.sort(
        key=lambda r: Version(re.match(tag_regex, r.tag_name).group("version")),
        reverse=True,
    )

    latest_release = relevant_releases[0] if relevant_releases else None

    return latest_release


async def find_stale_draft_releases_by_tag(tag_regex: str, repo=None) -> list[str]:
    # Retrieving releases from GitHub is done before any new release is created and
    # its response is cached. Thus, any drafts in this list are guaranteed to be
    # stale.
    releases = await get_filtered_releases(
        tag_regex, release_state="draft", include_prereleases=True, repo=repo
    )

    return [release.tag_name for release in releases]


async def cleanup_stale_drafts(stale_releases: list[str], repo: str | None) -> None:
    for tag_name in stale_releases:
        logger.info(f"Deleting stale draft release {tag_name}")
        return_code, _, stderr = await run_process_async(
            [
                "gh",
                "release",
                "delete",
                tag_name,
                "--yes",
                *(["--repo", repo] if repo else []),
            ],
        )
        if return_code != 0:
            raise RuntimeError(f"gh command failed: {stderr}")


async def fetch_release_by_tag(
    tag_name: str, repo: str | None = None
) -> ReleaseDetail | None:
    return_code, stdout, _ = await run_process_async(
        [
            "gh",
            "release",
            "view",
            tag_name,
            *(["--repo", repo] if repo else []),
            "--json",
            "tagName,name,body",
        ],
    )
    if return_code != 0:
        return None

    release = json.loads(stdout)

    return ReleaseDetail(
        tag_name=release["tagName"], name=release["name"], body=release["body"]
    )


def release_needs_update(
    release: ReleaseDetail, tag_name: str, title: str, body: str
) -> bool:
    return not all(
        (
            tag_name == release.tag_name,
            title.strip() == release.name.strip(),
            body.strip() == release.body.strip(),
        )
    )


def make_release_log_message(
    tag_name: str,
    title: str,
    body: str,
    is_update: bool,
    dry_run: bool,
    repo: str | None = None,
) -> str:
    if is_update:
        if dry_run:
            log_message = (
                f"Would update draft release {tag_name} in repo {repo or 'default'}"
            )
        else:
            log_message = f"Updating existing release {tag_name}."
    else:
        if dry_run:
            log_message = f"Would create draft release {tag_name} in repo {repo or 'default'} with title '{title}' and body:\n{body}\n"
        else:
            log_message = f"Creating new draft release {tag_name}."
    return log_message


async def gh_create_or_update_release(
    tag_name: str,
    title: str,
    body: str,
    repo: str | None,
    prerelease: bool,
    is_update: bool,
) -> None:
    command = [
        "gh",
        "release",
        *(["edit"] if is_update else ["create"]),
        tag_name,
        *(["--draft"] if not is_update else []),
        *(["--repo", repo] if repo else []),
        *("--title", title),
        *("--notes", body),
        *(["--prerelease"] if prerelease else []),
    ]
    return_code, _, stderr = await run_process_async(command)
    if return_code != 0:
        raise RuntimeError(f"gh command failed: {stderr}")


async def create_or_update_draft_release(
    tag_regex: str,
    tag_name: str,
    title: str,
    body: str,
    repo: str | None = None,
    dry_run: bool = False,
    prerelease: bool = False,
) -> None:
    release = await fetch_release_by_tag(tag_name, repo=repo)
    is_update = release is not None
    if release and not release_needs_update(release, tag_name, title, body):
        logger.info(f"No changes for release {tag_name}, skipping update.")
        return

    logger.info(
        make_release_log_message(
            tag_name,
            title,
            body,
            is_update,
            dry_run,
            repo=repo,
        )
    )

    if not dry_run:
        await gh_create_or_update_release(
            tag_name,
            title,
            body,
            repo,
            prerelease,
            is_update,
        )

    should_cleanup = not is_update and not prerelease

    if not should_cleanup or not (
        stale_tags := await find_stale_draft_releases_by_tag(tag_regex, repo=repo)
    ):
        return

    if dry_run:
        logger.info(
            f"Would delete the following stale draft releases in repo {repo or 'default'}:\n{stale_tags}"
        )
        return

    await cleanup_stale_drafts(stale_tags, repo=repo)
