"""
Use the GitHub API to show the status of current GitHub Action runs on a
specific branch.

It tries to show enough runs to see the latest status of each workflow.  Runs
older than a week are not considered.

"""

import contextlib
import io
import os.path
import re
import signal
import sys
import urllib.parse

import click
import exceptiongroup
import rich.console

from .data_core import draw_runs
from .git_help import git_repo_urls, git_branch
from .http_help import get_data
from .utils import Interval, WatchGhaError


console = rich.console.Console(highlight=False)
error_console = rich.console.Console(stderr=True, highlight=False)


def fatal(msg, status=2):
    error_console.print(msg)
    sys.exit(status)


@contextlib.contextmanager
def handle_resize(handler):
    original_sigwinch_handler = None
    try:
        # Save the original handler.
        original_sigwinch_handler = signal.getsignal(signal.SIGWINCH)
        # Call the given handler on sigwinch.
        signal.signal(signal.SIGWINCH, lambda _, __: handler())
    except AttributeError:
        # This system is probably windows, and doesn't have SIGWINCH.
        # Unfortunately there's no signal for window resize on windows and I
        # don't know how to properly handle it. Just swallow the error.
        pass

    # yield control to the with block, and re-raise any exceptions
    try:
        yield
    except:
        raise
    finally:
        # clean up our sigwinch handler if we set one
        if original_sigwinch_handler:
            signal.signal(signal.SIGWINCH, original_sigwinch_handler)


@click.command()
@click.option("--sha", help="The commit SHA to use. Must be a full SHA.")
@click.option(
    "--poll",
    help="How many seconds between refreshes.",
    type=int,
    default=15,
    show_default=True,
)
@click.option("--wait-for-start", is_flag=True, help="Wait for jobs to start.")
@click.option(
    "--only",
    help=(
        "Words to limit the workflows shown. "
        + "Only workflows with these comma separated case insensitive substrings "
        + "in their names will be shown."
    ),
)
@click.argument("repo", default=".")
@click.argument("branch", required=False)
def main(sha, poll, wait_for_start, only, repo, branch):
    """
    Watch GitHub Action runs.

    Repeatedly gets the latest status and redraws the screen, until all of the
    jobs are complete.

    REPO is a local directory or GitHub URL, defaulting to ".".

    BRANCH is defaulted from the git repo.

    """
    if only is not None:
        only_words = [w.strip() for w in only.split(",")]
    else:
        only_words = None

    watcher = GhaWatcher(
        urls=gha_urls(repo, branch, sha),
        get_data_fn=get_data,
        only_words=only_words,
    )

    watcher.watch(wait_for_start, poll, console)


def gha_urls(repo, branch, sha):
    """Figure out the GHA api URLs to use for `repo`, `branch`, and `sha`."""
    if os.path.isdir(repo):
        repo_urls = list(git_repo_urls(repo))
        if branch is None:
            branch = git_branch(repo)
    elif ":" in repo:
        repo_urls = [repo]
        if branch is None:
            fatal(f"Branch is required for URL repo")
    else:
        fatal(f"Don't understand repo {repo!r}")

    params = {"per_page": "40"}
    if sha:
        params["head_sha"] = sha
    else:
        assert branch is not None
        params["branch"] = branch
    url_args = urllib.parse.urlencode(params)

    github_urls = []
    for repo_url in repo_urls:
        # repo_url = "https://github.com/owner/repo.git"
        # repo_url = "git@github.com:someorg/somerepo.git"
        # see also https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables
        server_url = os.getenv("GITHUB_SERVER_URL", "https://github.com")
        repo_match = re.fullmatch(
            rf"(?:{re.escape(server_url)}/|git@github.com:)([^/]+/[^/]+?)(?:\.git|/)?",
            repo_url,
        )
        if repo_match is None:
            continue

        api_url = os.getenv("GITHUB_API_URL", "https://api.github.com")
        url = f"{api_url}/repos/{repo_match[1]}/actions/runs?{url_args}"
        github_urls.append(url)

    if not github_urls:
        fatal(f"Couldn't find GitHub repo from remote urls: {repo_urls!r}")

    return github_urls


class GhaWatcher:
    def __init__(self, urls, get_data_fn, only_words):
        self.urls = urls
        self.get_data_fn = get_data_fn
        self.only_words = only_words
        self.status = 0
        self.error = None

    def watch(self, wait_for_start, poll, console):
        self.done = False
        self.succeeded = False
        self.interrupted = False

        self.watch_gha_errors = []
        interval = Interval(poll)

        output = ""

        with exceptiongroup.catch(
            {
                WatchGhaError: self.handle_watchghaerror,
                KeyboardInterrupt: self.handle_keyboardinterrupt,
            }
        ):
            while True:
                output = self.get_gha_display()
                if wait_for_start:
                    if not self.done:
                        break
                else:
                    break

            if not self.done:
                with console.screen() as screen:
                    with handle_resize(lambda: screen.update(output)):
                        while not self.done:
                            screen.update(output)
                            interval.wait()
                            output = self.get_gha_display()

        if self.watch_gha_errors:
            fatal(self.watch_gha_errors[0])
        console.print(output, end="")
        if self.interrupted:
            fatal("** interrupted **", status=2)
        sys.exit(0 if self.succeeded else 1)

    def handle_watchghaerror(self, excgroup):
        self.watch_gha_errors.extend(excgroup.exceptions)

    def handle_keyboardinterrupt(self, excgroup):
        self.interrupted = True

    def get_gha_display(self):
        stream = io.StringIO()

        self.done, self.succeeded = draw_runs(
            self.urls,
            only_words=self.only_words,
            datafn=self.get_data_fn,
            outfn=lambda s: print(s, file=stream),
        )
        return stream.getvalue()
