#!/usr/bin/env python3
"""Create and publish Gitea releases for tagged IronStorage builds."""

from __future__ import annotations

import json
import os
import re
import subprocess
import sys
import unittest
from urllib.error import HTTPError
from urllib.parse import quote
from urllib.request import Request, urlopen


def issue_numbers(message: str) -> list[int]:
    return sorted({int(number) for number in re.findall(r"#(\d+)", message)})


def markdown(text: str) -> str:
    return text.replace("\\", "\\\\").replace("[", "\\[").replace("]", "\\]")


def release_body(issues: list[dict[str, object]], server: str, repository: str, sha: str) -> str:
    groups = {"Fixes": [], "Improvements": [], "Other changes": []}
    for issue in issues:
        labels = {label["name"] for label in issue.get("labels", [])}  # type: ignore[index]
        group = "Fixes" if "bug" in labels else "Improvements" if "enhancement" in labels else "Other changes"
        title = " ".join(str(issue["title"]).split())
        groups[group].append(f"- [#{issue['number']} {markdown(title)}]({issue['html_url']})")

    sections = [f"## {name}\n" + "\n".join(items) for name, items in groups.items() if items]
    if not sections:
        sections.append("No closed issues were linked from commits in this release.")
    sections.append(f"Built from [{sha[:12]}]({server}/{repository}/commit/{sha}).")
    return "\n\n".join(sections)


class Gitea:
    def __init__(self) -> None:
        self.api_url = os.environ["GITEA_API_URL"].rstrip("/")
        self.repository = os.environ["GITEA_REPOSITORY"]
        self.token = os.environ["GITEA_TOKEN"]

    def request(self, method: str, path: str, payload: object | None = None) -> object:
        data = None if payload is None else json.dumps(payload).encode()
        request = Request(
            f"{self.api_url}{path}",
            data=data,
            method=method,
            headers={"Authorization": f"token {self.token}", "Content-Type": "application/json"},
        )
        with urlopen(request) as response:
            content = response.read()
        return None if not content else json.loads(content)

    def issue(self, number: int) -> dict[str, object]:
        return self.request("GET", f"/repos/{self.repository}/issues/{number}")  # type: ignore[return-value]


def commit_messages(sha: str) -> str:
    try:
        previous = subprocess.check_output(
            ["git", "describe", "--tags", "--abbrev=0", f"{sha}^"],
            stderr=subprocess.DEVNULL,
            text=True,
        ).strip()
    except subprocess.CalledProcessError:
        previous = ""
    revision = f"{previous}..{sha}" if previous else sha
    return subprocess.check_output(["git", "log", "--format=%s%n%b", revision], text=True)


def prepare() -> None:
    gitea = Gitea()
    tag = os.environ["GITEA_REF_NAME"]
    sha = os.environ["GITEA_SHA"]
    server = os.environ["GITEA_SERVER_URL"].rstrip("/")

    issues = [gitea.issue(number) for number in issue_numbers(commit_messages(sha))]
    closed = [issue for issue in issues if issue["state"] == "closed"]
    payload = {
        "tag_name": tag,
        "target_commitish": sha,
        "name": tag,
        "body": release_body(closed, server, gitea.repository, sha),
        "draft": True,
        "prerelease": False,
    }

    try:
        release = gitea.request("GET", f"/repos/{gitea.repository}/releases/tags/{quote(tag, safe='')}")
    except HTTPError as error:
        if error.code != 404:
            raise
        release = gitea.request("POST", f"/repos/{gitea.repository}/releases", payload)
    else:
        if not release["draft"]:  # type: ignore[index]
            raise RuntimeError(f"release {tag} is already published")
        release = gitea.request("PATCH", f"/repos/{gitea.repository}/releases/{release['id']}", payload)  # type: ignore[index]
        for asset in release["assets"] or []:  # type: ignore[index]
            gitea.request(
                "DELETE",
                f"/repos/{gitea.repository}/releases/{release['id']}/assets/{asset['id']}",  # type: ignore[index]
            )

    with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
        output.write(f"release_id={release['id']}\n")  # type: ignore[index]


def publish() -> None:
    gitea = Gitea()
    release_id = int(os.environ["RELEASE_ID"])
    gitea.request("PATCH", f"/repos/{gitea.repository}/releases/{release_id}", {"draft": False})


class ReleaseTests(unittest.TestCase):
    def test_notes_are_unique_sorted_grouped_and_linked(self) -> None:
        self.assertEqual(issue_numbers("Fix #12, refs #3 and #12"), [3, 12])
        body = release_body(
            [
                {
                    "number": 12,
                    "title": "Fix [unlock]",
                    "html_url": "https://example.test/issues/12",
                    "labels": [{"name": "bug"}],
                },
                {
                    "number": 3,
                    "title": "Add search",
                    "html_url": "https://example.test/issues/3",
                    "labels": [{"name": "enhancement"}],
                },
            ],
            "https://example.test",
            "owner/repo",
            "1234567890abcdef",
        )
        self.assertIn("## Fixes\n- [#12 Fix \\[unlock\\]](https://example.test/issues/12)", body)
        self.assertIn("## Improvements\n- [#3 Add search](https://example.test/issues/3)", body)
        self.assertIn("[1234567890ab](https://example.test/owner/repo/commit/1234567890abcdef)", body)


if __name__ == "__main__":
    command = sys.argv[1] if len(sys.argv) == 2 else ""
    if command == "prepare":
        prepare()
    elif command == "publish":
        publish()
    elif command == "test":
        unittest.main(argv=[sys.argv[0]])
    else:
        raise SystemExit("usage: release.py prepare|publish|test")
