#!/usr/bin/env python3

import argparse
import logging.config
import re
import traceback

import sys

from gitlabform.configuration import Configuration
from gitlabform.configuration.core import ConfigFileNotFoundException
from gitlabform.gitlab import GitLab
from gitlabform.gitlab.core import TestRequestFailedException
from gitlabform.gitlab.core import NotFoundException


class GitLabForm(object):

    def main(self):
        args = self.parse_args()
        self.set_log_level(args)
        gl, c = self.initialize(args)
        projects_and_groups = self.get_projects_list(args, gl, c)
        self.run(args, gl, c, projects_and_groups)

    def parse_args(self):

        parser = argparse.ArgumentParser(description='Easy configuration as code tool for GitLab'
                                                     ' using config in plain YAML.',
                                         formatter_class=argparse.ArgumentDefaultsHelpFormatter)

        parser.add_argument('project_or_group', help='Project name in "group/project" format OR single group name '
                                                     'OR "ALL" to run for all groups in configuration')

        parser.add_argument('-c', '--config', default='./config.yml', help='Config file path and filename', )

        group_ex = parser.add_mutually_exclusive_group()

        group_ex.add_argument('-v', '--verbose', action="store_true", help='Verbose mode')

        group_ex.add_argument('-d', '--debug', action="store_true", help='Verbose mode')

        parser.add_argument('--strict', '-s', action="store_true", help='Stop on missing branches')

        parser.add_argument('--start-from', dest='start_from', default=1, type=int,
                            help='Start processing projects from the given one '
                                 '(as numbered by "[x/y] Processing: group/project" messages)')

        return parser.parse_args()

    def set_log_level(self, args):

        logging.basicConfig()
        level = logging.WARNING
        if args.verbose:
            level = logging.INFO
        elif args.debug:
            level = logging.DEBUG
        logging.getLogger().setLevel(level)

        fmt = logging.Formatter("%(message)s")
        logging.getLogger().handlers[0].setFormatter(fmt)

    def initialize(self, args):

        try:
            gl = GitLab(args.config)
            c = Configuration(args.config)
        except ConfigFileNotFoundException as e:
            logging.fatal('Aborting - config file not found at: %s', e)
            sys.exit(1)
        except TestRequestFailedException as e:
            logging.fatal("Aborting - GitLab test request failed, details: '%s'", e)
            sys.exit(2)

        return gl, c

    def get_projects_list(self, args, gl, c):

        if args.project_or_group == "ALL":
            # all groups from config
            groups = c.get_groups()
            logging.warning('>>> Processing ALL groups from config: %s', ', '.join(groups))
            projects_and_groups = []
            for group in groups:
                projects_and_groups += gl.get_projects(group)
        elif not re.match(".*/.*", args.project_or_group):
            # single group
            group = args.project_or_group
            projects_and_groups = gl.get_projects(group)
        else:
            # single project
            project_and_group = args.project_or_group
            projects_and_groups = [project_and_group]

        project_to_skip = c.get_skip_projects()
        effective_projects_and_groups = [x for x in projects_and_groups if x not in project_to_skip]

        logging.warning('*** # of projects got from GitLab: %s', str(len(projects_and_groups)))
        logging.info('*** # Projects list from GitLab: %s', str(', '.join(sorted(projects_and_groups))))

        logging.warning('*** # of projects to skip: %s', str(len(project_to_skip)))
        logging.info('*** # Projects to skip: %s', str(', '.join(project_to_skip)))

        logging.warning('*** # of projects to really process: %s', str(len(effective_projects_and_groups)))
        logging.info('*** # Projects to actually process: %s', str(', '.join(effective_projects_and_groups)))

        return effective_projects_and_groups

    def run(self, args, gl, c, projects_and_groups):

        i = 0

        for project_and_group in projects_and_groups:

            i += 1

            if i < args.start_from:
                logging.warning('$$$ [%s/%s] Skipping: %s...', i, len(projects_and_groups), project_and_group)
                continue

            logging.warning('* [%s/%s] Processing: %s', i, len(projects_and_groups), project_and_group)
            configuration = c.get_config_for_project(project_and_group)
            try:
                if 'project_settings' in configuration:
                    project_settings = configuration['project_settings']
                    logging.debug("Project settings BEFORE: %s", gl.get_project_settings(project_and_group))
                    logging.info("Setting project settings: %s", project_settings)
                    gl.put_project_settings(project_and_group, project_settings)
                    logging.debug("Project settings AFTER: %s", gl.get_project_settings(project_and_group))

                if 'deploy_keys' in configuration:
                    logging.debug("Deploy keys BEFORE: %s", gl.get_deploy_keys(project_and_group))
                    for deploy_key in sorted(configuration['deploy_keys']):
                        logging.info("Setting deploy key: %s", deploy_key)
                        gl.post_deploy_key(project_and_group, configuration['deploy_keys'][deploy_key])
                    logging.debug("Deploy keys AFTER: %s", gl.get_deploy_keys(project_and_group))

                if 'secret_variables' in configuration:
                    logging.debug("Secret variables BEFORE: %s", gl.get_secret_variables(project_and_group))
                    for secret_variable in sorted(configuration['secret_variables']):
                        logging.info("Setting secret variable: %s", secret_variable)

                        try:
                            current_value = \
                                gl.get_secret_variable(project_and_group,
                                                       configuration['secret_variables'][secret_variable]['key'])
                            if current_value != configuration['secret_variables'][secret_variable]['value']:
                                gl.put_secret_variable(project_and_group,
                                                       configuration['secret_variables'][secret_variable])
                        except NotFoundException:
                            gl.post_secret_variable(project_and_group,
                                                    configuration['secret_variables'][secret_variable])

                    logging.debug("Secret variables AFTER: %s", gl.get_secret_variables(project_and_group))

                if 'branches' in configuration:
                    logging.info("Setting branches as protected/unprotected")
                    for branch in sorted(configuration['branches']):
                        try:
                            if configuration['branches'][branch]['protected']:
                                logging.debug("Setting branch '%s' as *protected*", branch)
                                # unprotect first to reset 'allowed to merge' and 'allowed to push' fields
                                gl.unprotect_branch(project_and_group, branch)
                                gl.protect_branch(project_and_group, branch,
                                                  configuration['branches'][branch]['developers_can_push'],
                                                  configuration['branches'][branch]['developers_can_merge'])
                            else:
                                logging.debug("Setting branch '%s' as unprotected", branch)
                                gl.unprotect_branch(project_and_group, branch)
                        except NotFoundException:
                            logging.warning("! Branch '%s' not found when trying to set it as protected/unprotected",
                                            branch)
                            if args.strict:
                                exit(3)

                if 'services' in configuration:
                    logging.info("Setting services")
                    for service in sorted(configuration['services']):
                        if 'delete' in configuration['services'][service]\
                                and configuration['services'][service]['delete']:
                            logging.debug("Deleting service '%s'", service)
                            gl.delete_service(project_and_group, service)
                        else:
                            logging.debug("Setting service '%s'", service)
                            gl.set_service(project_and_group, service, configuration['services'][service])

                if 'files' in configuration:
                    logging.info("Setting files")
                    for file in sorted(configuration['files']):

                        all_branches = gl.get_branches(project_and_group)
                        if configuration['files'][file]['branches'] == 'all':
                            branches = sorted(all_branches)
                        else:
                            branches = []
                            for branch in configuration['files'][file]['branches']:
                                if branch in all_branches:
                                    branches.append(branch)
                                else:
                                    logging.warning("! Branch '%s' not found, not processing file '%s' in it", branch,
                                                    file)
                                    if args.strict:
                                        exit(3)

                        for branch in branches:

                            # unprotect protected branch temporarily for operations below
                            if 'branches' in configuration \
                                    and branch in configuration['branches'] \
                                    and configuration['branches'][branch]['protected']:
                                logging.debug("> Temporarily unprotecting the branch for managing files in it...")
                                gl.unprotect_branch(project_and_group, branch)

                            if 'ignore' in configuration['files'][file] and configuration['files'][file]['ignore']:
                                logging.debug("Ignoring file '%s' in branch '%s'", file, branch)
                            elif 'delete' in configuration['files'][file] and configuration['files'][file]['delete']:
                                try:
                                    gl.get_file(project_and_group, branch, file)
                                    logging.debug("Deleting file '%s' in branch '%s'", file, branch)
                                    gl.delete_file(project_and_group, branch, file,
                                                   "Automated delete made by gitlabform")
                                except NotFoundException:
                                    logging.debug("Not deleting file '%s' in branch '%s' (already doesn't exist)", file,
                                                  branch)
                            else:
                                try:
                                    current_content = gl.get_file(project_and_group, branch, file)
                                    if current_content != configuration['files'][file]['content']:
                                        if 'overwrite' in configuration['files'][file] \
                                                and configuration['files'][file]['overwrite']:
                                            logging.debug("Changing file '{0}' in branch '{1}'", file, branch)
                                            gl.set_file(project_and_group, branch, file,
                                                        configuration['files'][file]['content'],
                                                        "Automated change made by gitlabform")
                                        else:
                                            logging.debug("Not changing file '%s' in branch '%s' "
                                                          "(overwrite flag not set)", file, branch)
                                    else:
                                        logging.debug("Not changing file '%s' in branch '%s' (it\'s content is already"
                                                      " as provided)", file, branch)
                                except NotFoundException:
                                    logging.debug("Creating file '%s' in branch '%s'", file, branch)
                                    gl.add_file(project_and_group, branch, file,
                                                configuration['files'][file]['content'],
                                                "Automated add made by gitlabform")

                            # protect branch back after above operations
                            if 'branches' in configuration \
                                    and branch in configuration['branches'] \
                                    and configuration['branches'][branch]['protected']:
                                logging.debug("> Protecting the branch again.")
                                gl.protect_branch(project_and_group, branch,
                                                  configuration['branches'][branch]['developers_can_push'],
                                                  configuration['branches'][branch]['developers_can_merge'])

                            if 'only_first_branch' in configuration['files'][file] \
                                    and configuration['files'][file]['only_first_branch']:
                                logging.info('Skipping other branches for this file, as configured.')
                                break

                if 'hooks' in configuration:
                    logging.info("Setting hooks")
                    for hook in sorted(configuration['hooks']):

                        if 'delete' in configuration['hooks'][hook] and configuration['hooks'][hook]['delete']:
                            hook_id = gl.get_hook_id(project_and_group, hook)
                            if hook_id:
                                logging.debug("Deleting hook '%s'", hook)
                                gl.delete_hook(project_and_group, hook_id)
                            else:
                                logging.debug("Not deleting hook '%s', because it doesn't exist", hook)
                        else:
                            hook_id = gl.get_hook_id(project_and_group, hook)
                            if hook_id:
                                logging.debug("Changing existing hook '%s'", hook)
                                gl.put_hook(project_and_group, hook_id, hook, configuration['hooks'][hook])
                            else:
                                logging.debug("Creating hook '%s'", hook)
                                gl.post_hook(project_and_group, hook, configuration['hooks'][hook])

            except Exception as e:
                logging.error("+++ Error while processing '%s'", project_and_group)
                traceback.print_exc()


if __name__ == '__main__':
    GitLabForm().main()
