#!/usr/bin/env python3

import os
from pathlib import Path
import tomllib
from typing import Optional
from datetime import datetime, timedelta, timezone
from time import sleep
from pprint import pprint

import click
import requests
import pytimeparse


class Client(object):
    BASE = "https://api.github.com"
    HEADERS = {"Accept": "application/vnd.github+json"}

    def __init__(self, token):
        self._token = token
        self._headers = {"Authorization": f"Bearer {token}", **self.HEADERS}

    def get_notifications(self, page=1, since: Optional[datetime] = None):
        resp = requests.get(
            f"{self.BASE}/notifications",
            headers=self._headers,
            params={
                "page": page,
                "all": "false",
                "since": since.isoformat() if since else None,
            },
        )
        resp.raise_for_status()
        return resp.json()

    def get_all_notifications(self, since: Optional[datetime] = None):
        page = 1
        while True:
            results = self.get_notifications(page=page, since=since)
            if not results:
                return
            yield from results
            page += 1

    def read(self, notif):
        return requests.patch(notif["url"], headers=self._headers).raise_for_status()

    def unsubscribe(self, notif):
        return requests.delete(
            notif["subscription_url"], headers=self._headers
        ).raise_for_status()

    def get_pr(
        self,
        url: Optional[str] = None,
        repo: Optional[str] = None,
        id: Optional[int] = None,
    ):
        url = url or f"{self.BASE}/{repo}/pulls/{id}"
        resp = requests.get(url, headers=self._headers)
        resp.raise_for_status()
        return resp.json()

    def get_pr_reviewers(self, pr):
        url = pr["url"] + "/requested_reviewers"
        resp = requests.get(url, headers=self._headers)
        resp.raise_for_status()
        return resp.json()

    def get_user(self):
        resp = requests.get(f"{self.BASE}/user", headers=self._headers)
        resp.raise_for_status()
        return resp.json()

    def get_user_teams(self):
        resp = requests.get(f"{self.BASE}/user/teams", headers=self._headers)
        resp.raise_for_status()
        return resp.json()

    def get_issue(
        self,
        url: Optional[str] = None,
        repo: Optional[str] = None,
        id: Optional[int] = None,
    ):
        url = url or f"{self.BASE}/{repo}/issues/{id}"
        resp = requests.get(url, headers=self._headers)
        resp.raise_for_status()
        return resp.json()

    def get_comments(
        self,
        url: Optional[str] = None,
        repo: Optional[str] = None,
        id: Optional[int] = None,
    ):
        url = url or f"{self.BASE}/{repo}/issues/{id}/comments"
        resp = requests.get(url, headers=self._headers)
        resp.raise_for_status()
        return resp.json()


@click.group()
def cli():
    pass


@cli.command()
@click.option(
    "--config",
    "config_path",
    type=Path,
    default=lambda: Path(os.getenv("BUILD_WORKSPACE_DIRECTORY", ""))
    / "projects/gh-unnotifier/config.toml",
)
def oneshot(config_path: Path):
    with open(config_path, "rb") as fp:
        config = tomllib.load(fp)

    client = Client(config["gh-unnotifier"]["api_key"])
    for notif in client.get_all_notifications():
        subject = notif["subject"]
        pr = None
        if "/pulls/" in subject["url"]:
            pr = client.get_pr(url=subject["url"])
            if pr["state"] == "closed":
                client.read(notif)
                client.unsubscribe(notif)

                print("Resolved", notif["id"])
            continue

        print(
            notif["id"],
            notif["subscription_url"],
            notif["reason"],
            notif["subject"],
            pr,
        )


def parse_seconds(text: str) -> timedelta:
    return timedelta(seconds=pytimeparse.parse(text))


@cli.command()
@click.option(
    "--config",
    "config_path",
    type=Path,
)
@click.option("--schedule", "schedule", default="30 seconds", type=parse_seconds)
def maintain(config_path: Path, schedule: timedelta):
    with open(config_path, "rb") as fp:
        config = tomllib.load(fp)

    client = Client(config["gh-unnotifier"]["api_key"])
    org_shitlist = config["gh-unnotifier"].get("org_shitlist", [])
    team_shitlist = config["gh-unnotifier"].get("team_shitlist", [])
    author_shitlist = config["gh-unnotifier"].get("author_shitlist", [])
    user = client.get_user()
    user_teams = {it["slug"] for it in client.get_user_teams()}
    mark = savepoint = prev = None

    def _resolve(notif, reason):
        client.read(notif)
        client.unsubscribe(notif)
        click.echo(f"Resolved {notif['id']} {reason} ({notif['subject']})")

    def _pr(subject, notif):
        pr = client.get_pr(url=subject["url"])

        if pr.get("merged", False):
            _resolve(notif, "Merged")
            return

        if pr.get("state") == "closed":
            _resolve(notif, "Closed")
            return

        if pr["user"]["login"] in author_shitlist:
            _resolve(notif, "Ignoring PR by author")
            return

        if notif["reason"] == "review_requested":
            reviewers = client.get_pr_reviewers(pr)
            if (
                any(org in subject["url"] for org in org_shitlist)
                and not any(
                    it["login"] == user["login"]
                    for it in reviewers.get("users", [])
                )
                and not any(
                    it["slug"] in user_teams
                    and it["slug"] not in team_shitlist
                    for it in reviewers.get("teams", [])
                )
            ):
                _resolve(notif, "Reviewed")
                return

        print("Oustanding PR notification")
        pprint({"subject": subject, "notif": notif, "pr": pr})

    def _mention(subject, notif):
        pr = client.get_pr(url=subject["url"])

        reviewers = client.get_pr_reviewers(pr)
        if (
            any(org in subject["url"] for org in org_shitlist)
            and not any(
                it["login"] == user["login"]
                for it in reviewers.get("users", [])
            )
            and not any(
                it["slug"] in user_teams
                and it["slug"] not in team_shitlist
                for it in reviewers.get("teams", [])
            )
        ):
            _resolve(notif, "Ignoring team mention")
            return

    def _issue(subject, notif):
        issue = client.get_issue(url=subject["url"])

        if issue["state"] == "closed":
            comments = client.get_comments(url=issue["comments_url"])
            if not any(
                it["user"]["login"] == user["login"] for it in comments
            ):
                _resolve(notif, "Unengaged issue closed")

        if issue["user"]["login"] in author_shitlist:
            _resolve(notif, "Ignoring issue by author")

    while True:
        try:
            savepoint = prev
            prev = mark
            mark = datetime.now(timezone.utc)
            tick = mark + schedule
            assert tick - schedule == mark
            for notif in client.get_all_notifications(since=prev):
                notif_timestamp = datetime.fromisoformat(notif["updated_at"])
                if (mark - notif_timestamp).days > 3:
                    _resolve(notif, "More than 3 days old")
                    continue

                subject = notif["subject"]

                match subject["type"].lower():
                    case "pullrequest":
                        _pr(subject, notif)

                    case "mention":
                        _mention(subject, notif)

                    case "issue":
                        _issue(subject, notif)

            duration = (tick - datetime.now(timezone.utc)).total_seconds()
            if duration > 0:
                sleep(duration)

        except KeyboardInterrupt:
            break

        except requests.exceptions.HTTPError as e:
            print("Encoutered exception", e, "backing off")
            prev = savepoint
            sleep(schedule.total_seconds())


if __name__ == "__main__":
    cli()
