#!/usr/bin/env python3
#
# Deploy Onion Launchpad as a GitHub repository.
#

import os
import argparse
import configparser
import time

from pathlib import Path

from github import Github
from github.GithubException import UnknownObjectException, GithubException

from git import Repo, Remote, RemoteProgress, GitCommandError

# Parameters
basepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir) + os.sep

# Defaults
defaults = {
    'import_url'          : 'https://gitlab.torproject.org/tpo/onion-services/onion-launchpad.git',
    'project_description' : 'Landing Page',
}

class OnionLaunchpadGithubDeployer():
    """
    Deployer for Onion Launchpad into GitHub repositories.
    """

    def __init__(self, args):
        # Save arguments
        self.args = args

        # Load config file
        self.config = self.load_config(args.config_file)

        # Instantiate GitHub
        assert self.instantiate_github() is not False

    def log(self, message, level='info'):
        """Log wrapper"""

        print('[' + level + '] ' + str(message))

    def load_config(self, file):
        """Load a config file"""

        if os.path.exists(file):
            if os.path.isfile(file):
                config = configparser.ConfigParser()

                self.log("Loading config file {}...".format(file))
                config.read(file)

                return config

            else:
                raise IsADirectoryError('Not a file: ' + file)

        else:
            raise FileNotFoundError('No such file ' + file)

    def instantiate_github(self):
        """Instantiate the GitHub object"""

        # Load GitHub config file
        self.github_config = self.load_config(args.github_config)

        if self.config.has_option('main', 'instance'):
            instance = self.config.get('main', 'instance')
        else:
            return False

        # Build the GitHub options object
        github_options = { k: v for k, v in self.github_config.items(instance) }

        # Cast int options
        for param in [ 'timeout', 'per_page', 'retry', 'pool_size' ]:
            if param in github_options:
                github_options[param] = int(github_options[param])

        # Cast boolean options
        for param in [ 'verify' ]:
            if param in github_options:
                github_options[param] = bool(github_options[param])

        # Instantiate
        self.github = Github(**github_options)

    def manage_organization(self):
        """Get the configured organization"""

        try:
            path         = self.config.get('main', 'organization_path')
            organization = self.github.get_organization(path)

        except UnknownObjectException as e:
            if e.status == 404:
                raise Exception('No such organization {}, please create it manually. Aborting.'.format(path))

            raise e

        self.log("Organization {} exists, updating...".format(path))

        try:

            self.organization = organization
            name              = self.config.get('main', 'organization_name')

            self.organization.edit(description=name)

        except UnknownObjectException as e:
            if e.status == 404:
                raise Exception('Could not update organization {}. '.format(path)              +
                                'Please make sure you are '.format(path)                       +
                                'part of this group and have the "admin:org" OAuth scope set ' +
                                'for the current access token. Aborting.')

            raise e

    def create_repository(self):
        """Create the repository"""

        repository_path = self.config.get('main', 'repository_path')
        repository_name = self.config.get('main', 'repository_name')
        license         = self.config.get('main', 'license', fallback='')
        params          = {
                        'name'            : repository_path,
                        'description'     : repository_name,
                        #'visibility'     : 'public', # May need a newer PyGithub
                        'private'         : False,
                        'has_issues'      : False,
                        'has_projects'    : False,
                        'has_wiki'        : False,
                        }

        try:
            self.log("Checking if repository {} exists...".format(repository_name))

            result = self.organization.get_repo(repository_path)

        except UnknownObjectException as e:
            if e.status == 404:
                self.log("Creating repository {}...".format(repository_name))

                params['auto_init']    = False
                #params['is_template'] = False, # May need a newer PyGithub

                if license != '':
                    params['license_template'] = license,

                try:
                    self.repository = self.organization.create_repo(**params)

                except GithubException as e:
                    if e.status == 403:
                        raise Exception('Could not create repository {}. '.format(repository_path) +
                                        'Please make sure you are part '                           +
                                        'of the group {} '.format(self.organization.name)          +
                                        'and have the "repo" OAuth scope set for the '             +
                                        'current access token. Aborting.')
                    raise e

                return

            else:
                raise e

        self.log("Repository {} exists.".format(repository_name))

        self.repository = result

        self.repository.edit(**params)

    def set_secrets(self):
        """Set CI/CD variables"""

        if not self.config.has_section('variables'):
            return

        for variable in self.config.items('variables'):
            (key, value) = variable
            key          = key.upper()

            self.log("Setting variable {}...".format(key))
            self.repository.create_secret(key, value)

    def populate(self):
        """Populate the repository with the Onion Launchpad codebase"""

        # This is hardcoded for now
        base_url = 'git@github.com:'
        refspec  = 'main:main'

        self.log('Setting up repository remote...')

        organization_path = self.config.get('main', 'organization_path')
        repository_path   = self.config.get('main', 'repository_path')
        remote_name       = organization_path + '_' + repository_path
        remote_url        = base_url + organization_path + '/' + repository_path + '.git'
        self.repo         = Repo(basepath)

        try:
            self.remote = self.repo.create_remote(remote_name, remote_url)

        except GitCommandError as e:
            # Maybe the remote already exists
            self.remote = Remote(self.repo, remote_name)

            assert self.remote.exists()

            self.remote.set_url(remote_url)

        assert self.remote.exists()

        self.log('Populating remote repository...')
        self.remote.push(refspec=refspec)

    def configure_pages(self):
        """Configure GitHub pages"""

        # Pages configuration is currently unsupported by PyGithub
        # So we'll do it manually.
        # See https://github.com/PyGithub/PyGithub/issues/2037
        #     https://docs.github.com/en/rest/pages?apiVersion=2022-11-28#update-information-about-a-github-pages-site
        import requests, json

        instance          = self.config.get('main', 'instance')
        organization_path = self.config.get('main', 'organization_path')
        repository_path   = self.config.get('main', 'repository_path')
        token             = self.github_config.get(instance, 'login_or_token')
        api_version       = '2022-11-28'

        url = "https://api.github.com/repos/{}/{}/pages".format(
                organization_path,
                repository_path)

        data = {
                'source': {
                    'branch': 'gh-pages',
                    'path'  : '/',
                    },
                }

        headers = {
                'Accept'               : 'application/vnd.github+json',
                'Authorization'        : 'Bearer {}'.format(token),
                'X-GitHub-Api-Version' : api_version,
                }

        self.log('Configuring GitHub Pages...')

        response = requests.put(url, data=json.dumps(data), headers=headers)

    def build(self):
        """Build Onion Launchpad"""

        # This is hardcoded for now
        ref           = 'main'
        workflow_name = 'GitHub Pages' # This is defined at ../.github/gh-pages.yml

        for workflow in self.repository.get_workflows():
            if workflow.name == workflow_name:
                self.log('Triggering a page build...')
                workflow.create_dispatch(ref)

    def launch(self):
        """Launch!"""

        try:
            self.manage_organization()
            self.create_repository()
            self.set_secrets()
            self.populate()
            self.configure_pages()
            self.build()

        except Exception as e:
            self.log(e, 'error')

            return False

def cmdline():
    """Process from CLI"""

    epilog = """Invocation examples:

      {basename} myproject.ini
      {basename} --github-config ~/.pygithub.cfg myproject.ini
    """

    epilog += """\nGitHub configuration example (INI file format):

        # See https://pygithub.readthedocs.io/en/latest/github.html
        [mygithub]
        # Mandatory parameters
        login_or_token = MY-LOGIN-OR-ACCESS-TOKEN

        # Optional endpoint parameters
        password       = PASSWORD
        base_url       = https://api.github.com

        # Optional client parameters
        timeout        = 15
        user_agent     = 'PyGithub/Python'
        per_page       = 30
        verify         = True

        # Optional connection parameters
        # See https://requests.readthedocs.io/en/latest/api/#requests.adapters.HTTPAdapter
        retry          = 10
        pool_size      = 10 # Needs a newer PyGithub
    """

    epilog += """\nConfiguration example (INI file format):

        # Basic group and project information
        [main]
        instance          = mygithub
        organization_name = My Group
        organization_path = my-group
        repository_name   = Landing Page
        repository_path   = my-group.gitlab.io

        # Deployment secrets
        [variables]
        LEKTOR_FOREGROUND_COLOR      = FF0000
        LEKTOR_BACKGROUND_COLOR      = FF7700
        LEKTOR_BUTTON_COLOR          = 3377FF
        LEKTOR_ONION_URL_LOCK_FILTER = FF0000
        LEKTOR_ONION_URL             = https://abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz2345.onion
        LEKTOR_SERVICE_NAME          = Service Name
        LEKTOR_SERVICE_URL           = https://service-url.website
        LEKTOR_FAVICON               = /favicon-default-32.png
        LEKTOR_LOGO_PATH             = /onion-launchpad-logo-default-1.svg
        LEKTOR_DEFAULT_LANGUAGE      = en
        LEKTOR_AVAILABLE_LANGUAGES   = en es
    """

    description = 'Deploy Onion Launchpad as a GitHub repository'
    parser      = argparse.ArgumentParser(
                    description=description,
                    epilog=epilog,
                    formatter_class=argparse.RawDescriptionHelpFormatter,
                  )

    parser.add_argument(
            '-c',
            '--github-config',
            dest='github_config',
            default=os.path.join(Path.home(), '.pygithub.cfg'),
            help='Configuration file with GitHub authentication. Default: %(default)s',
            )

    parser.add_argument(
            'config_file',
            help='Config file with deployment options',
            )

    args = parser.parse_args()

    return args

if __name__ == "__main__":
    args   = cmdline()
    pad    = OnionLaunchpadGithubDeployer(args)
    status = pad.launch()

    exit(int(not status))
