# grainyhead - Helper tools for GitHub
# Copyright © 2021,2022 Damien Goutte-Gattat
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from configparser import ConfigParser, _UNSET
from time import sleep
from random import randint
import sys
import re
import os

import click
from click_shell import shell
from ghapi.core import GhApi

from . import __version__
from .repository import Repository
from .providers import OnlineRepositoryProvider
from .caching import FileRepositoryProvider
from .util import Date, Interval
from .metrics import MetricsFormatter, MetricsReporter

prog_name = "grh"
prog_notice = f"""\
{prog_name} (GrainyHead {__version__})
Copyright © 2022 Damien Goutte-Gattat

This program is released under the GNU General Public License.
See the COPYING file or <http://www.gnu.org/licenses/gpl.html>.
"""


def die(msg):
    print(f"{prog_name}: {msg}", file=sys.stderr)
    sys.exit(1)


def _parse_github_url(url):
    m = re.match('(https?://github.com/)?([^/]+)/([^/.]+)', url)
    if not m:
        die(f"Invalid GitHub repository: {url}.")

    return m.groups()[1:]


class GrhContext(object):

    def __init__(self, config_file, section='default'):
        self._config_file = config_file
        self._name = section

        self._config = ConfigParser()
        self._has_config = len(self._config.read(config_file)) > 0

        self._repo = None
        self._cache = True

    def reset(self, section='default', config_file=None, options=None):
        self._repo = None
        self._name = section

        if config_file:
            self._config.clear()
            self._config_file = config_file
            self._has_config = len(self._config.read(config_file)) > 0
        elif options:
            self._config.clear()
            self._config.add_section(section)
            for key, value in options.items():
                self._config.set(section, key, value)
            self._has_config = True

    def get_option(self, key, fallback=_UNSET):
        return self._config.get(self._name, key, fallback=fallback)

    def disable_cache(self):
        self._cache = False

    @property
    def repository(self):
        if not self._repo:
            repo_url = self._config.get(self._name, 'repository')
            owner, repo = _parse_github_url(repo_url)
            token = self._config.get(self._name, 'token', fallback=None)
            api = GhApi(owner=owner, repo=repo, org=owner, token=token)
            backend = OnlineRepositoryProvider(api)
            if self._cache:
                backend = FileRepositoryProvider(self.cache_dir, backend)
            self._repo = Repository(api, backend)
        return self._repo

    @property
    def has_config(self):
        return self._has_config

    @property
    def config_file(self):
        return self._config_file

    @property
    def config(self):
        return self._config

    @property
    def cache_dir(self):
        xdg_data_dir = os.getenv('XDG_DATA_HOME',
                                 os.path.join(os.getenv('HOME'),
                                              '.local', 'share'))
        return os.path.join(xdg_data_dir, 'grainyhead', self._name)


@shell(context_settings={'help_option_names': ['-h', '--help']},
       prompt="grh> ")
@click.option('--config', '-c', type=click.Path(exists=False),
              default='{}/config'.format(click.get_app_dir('grainyhead')),
              help="Path to an alternative configuration file.")
@click.option('--section', '-s', default='default',
              help="Name of the configuration file section to use.")
@click.option('--no-cache', is_flag=True, default=False,
              help="Disable the file cache.")
@click.version_option(version=__version__, message=prog_notice)
@click.pass_context
def grh(ctx, config, section, no_cache):
    """Command-line tool for GitHub."""

    context = GrhContext(config, section)
    if no_cache:
        context.disable_cache()
    ctx.obj = context
    if not context.has_config:
        ctx.invoke(conf)


@grh.command(name='issues')
@click.option('--older-than', 'cutoff', default='1y', type=Date,
              help="""Only list issues that have not been updated since the
                      specified date or for the specified duration
                      (default=1y, or one year ago).""")
@click.option('--team', default='__collaborators', metavar='NAME',
              help="""The name of a GitHub team.""")
@click.pass_obj
def list_issues(grh, cutoff, team):
    """List open issues.
    
    This commands list open issues that have not been updated for a
    given amount of time.
    """

    repo = grh.repository

    issues = [i for i in repo.issues if i.updated(before=cutoff)]
    members = [m.login for m in repo.get_team(team)]

    print("| Issue | Author | Team? | Assignee(s) |")
    print("| ----- | ------ | ----- | ----------- |")

    for issue in issues:
        assignees = ', '.join(['@[{}]({})'.format(a.login, a.html_url) for a in issue.assignees])
        is_team = 'Yes' if issue.user.login in members else 'No'

        print(f"| [{issue.title}]({issue.html_url}) | @[{issue.user.login}]({issue.user.html_url}) | {is_team} | {assignees} |")


@grh.command(name='close')
@click.option('--older-than', 'cutoff', default='1y', type=Date,
              help="""Close issues that have not been updated since the
                      specified date or for the specified duration
                      (default=1y, or one year ago).""")
@click.option('--comment', '-c', default=None,
              help="""Text of the comment to add to the closed issues.""")
@click.option('--dry-run', '-d', is_flag=True, default=False,
              help="""List issues that would be closed without actually
                      closing them.""")
@click.option('--limit', '-l', default=-1, metavar='N',
              help="""Only close the N oldest issues.""")
@click.pass_obj
def auto_close(grh, comment, cutoff, dry_run, limit):
    """Close old issues.
    
    This command automatically closes issues that have not been updated
    for a given amount of time.
    """

    repo = grh.repository

    repo.create_label('autoclosed-unfixed', 'ff7000',
                      'This issue has been closed automatically.')

    if not comment:
        comment = grh.get_option('close.comment', fallback=None)

    issues = [i for i in reversed(repo.issues) if i.updated(before=cutoff)]
    if limit != -1:
        issues = issues[:limit]

    click.echo_via_pager(_list_closable_issues(issues))
    if comment:
        click.echo(f"Closing with comment: {comment}")
    if dry_run or not click.confirm("Proceed?"):
        return

    with click.progressbar(issues, item_show_func=_show_closing_issue) as bar:
        for issue in bar:
            repo.close_issue(issue, 'autoclosed-unfixed', comment)

            # GitHub's documentation says that requests that trigger
            # notifications (such as adding a comment to an issue)
            # should be issued "at a reasonable pace", but does not
            # elaborate on what a "reasonable pace" is.
            # We try here to wait for anything between 2 and 10 seconds
            # between requests.
            sleep(randint(2, 10))


def _list_closable_issues(issues):
    yield f"The following {len(issues)} issues are about to be closed:\n"
    for issue in issues:
        last_update, _ = issue.updated_at.split('T')
        yield f"{issue.number}: last update {last_update}: {issue.title}\n"


def _show_closing_issue(issue):
    if issue:
        return f"Closing issue #{issue.number}"
    else:
        return ""


@grh.command()
@click.option('--from', 'start', type=Date, default='6m',
              help="""Set the beginning of the reporting period.
                      Default to 6m (6 months ago).""")
@click.option('--to', 'end', type=Date, default='now',
              help="""Set the end of the reporting period.
                      Default to now.""")
@click.option('--team', default='__collaborators', metavar='NAME',
              help="""The name of a GitHub team.""")
@click.option('--selector', '-s', multiple=True, default=[], metavar='SELECTOR',
              help="""Select a subset of repository events.""")
@click.option('--format', '-f', 'fmt', default='markdown',
              type=click.Choice(['json', 'markdown', 'csv', 'tsv']),
              help="""Write output in the specified format.""")
@click.option('--period', '-p', type=Interval, default=None,
              help="""Break down the metrics per periods of the
                      specified duration.""")
@click.pass_obj
def metrics(grh, start, end, team, selector, fmt, period):
    """Get repository metrics.
    
    This command collects and prints the number of repository events
    (such as the opening and closing of issues, commits, etc.) that
    occurred over a given period of time, as well as the number of
    individual contributors that caused those events.
    """

    reporter = MetricsReporter(grh.repository)
    if len(selector) == 0:
        selector = ['all AS Total',
                    f'team:{team} AS Internal',
                    f'!team:{team} AS External']

    metrics = reporter.get_report(selector, start, end, period)

    formatter = MetricsFormatter.get_formatter(fmt)
    formatter.write(metrics, sys.stdout)


@grh.command()
@click.pass_obj
def conf(grh):
    """Edit the configuration.
    
    This command either creates a new basic configuration file for use
    with a repository or starts a text editor to edit the current
    configuration file if it already exists.
    """

    if grh.has_config:
        click.termui.edit(filename=grh.config_file)
        grh.reset(config_file=grh.config_file)
    else:
        opts = {}
        opts['repository'] = click.termui.prompt("Repository name or URL")

        click.echo("Visit https://github.com/settings/tokens to create "
                   "a personal access token.")
        opts['token'] = click.termui.prompt("Token")
        grh.reset(options=opts)

        with open(grh.config_file, 'w') as f:
            grh.config.write(f)


def refresh(grh):
    pass


try:
    from IPython import embed

    @grh.command(name='ipython')
    @click.pass_obj
    def python_shell(grh):
        """Start an interactive Python shell.
    
        This commands starts a Python shell from where the GitHub API
        can be directly manipulated through the 'api' object. This is
        intended for testing purposes.
        """

        api = grh.repository._api
        repo = grh.repository
        embed()

except ImportError:
    pass

if __name__ == '__main__':
    grh()
