#!/usr/bin/env python3
import argparse
import json
import logging
import os
import sys

import gitlab

logger = logging.getLogger(os.path.basename(__file__))

DRY_RUN = os.getenv('DRY_RUN', False)
GITLAB_TOKEN = os.getenv('GITLAB_TOKEN')
GITLAB_URL = os.getenv('REPO_URL', 'https://repo.lcloud.pl')


def init_logger(log_level=None, o_format=None):
    """
    Initializes the logger configuration.
    :param log_level: Override default log level
    :param o_format: Output format of the logs
    """
    if not log_level:
        log_level = 'INFO'

    # get the numerical value for the log level specified as string:
    # valid string are: DEBUG, INFO, WARNING, ERROR, CRITICAL
    numeric_level = getattr(logging, log_level.upper(), None)
    if not isinstance(numeric_level, int):
        logger.error(f'[!!] Invalid log level: {log_level}, setting log level to 10')
        numeric_level = 10

    if not o_format or o_format == 'stream':
        o_format = '[%(asctime)s.%(msecs)03d] %(levelname)s :: %(message)s'
    elif o_format == 'json':
        o_format = "{'time':'%(asctime)s.%(msecs)03d'," \
                   " 'name': '%(name)s'," \
                   " 'level': '%(levelname)s'," \
                   " 'message': '%(message)s'}"

    logging.basicConfig(level=numeric_level, format=o_format, datefmt='%H:%M:%S')
    # override github logging configuration
    logging.getLogger('requests').setLevel(logging.WARNING)
    logging.getLogger('requests').propagate = False
    logging.getLogger('urllib3').setLevel(logging.WARNING)
    logging.getLogger('urllib3').propagate = False


def gitlab_client(token):
    if token:
        try:
            gl = gitlab.Gitlab(url=GITLAB_URL, private_token=token)
            # Test if token is valid.
            projects = gl.projects.list(all=False)
            return gl
        except gitlab.exceptions.GitlabAuthenticationError:
            logger.error('Verify private token!')
            sys.exit(1)


def get_variables(client=None, resource_type=None, resource_id=None, file=None):
    try:
        if client and resource_type and resource_id:
            variables = []
            resource = None
            resource_path = ''

            if resource_type == 'project':
                resource = client.projects.get(resource_id)
                resource_path = resource.path_with_namespace
            if resource_type == 'group':
                resource = client.groups.get(resource_id)
                resource_path = resource.full_path

            logger.info(f'Getting variables for {resource_type} id {resource_id} ({resource_path})')
            for gv in resource.variables.list():
                variables.append({
                    'variable_type': gv.variable_type,
                    'key': gv.key,
                    'value': gv.value,
                    'protected': gv.protected,
                    'masked': gv.masked,
                    'environment_scope': gv.environment_scope
                })

            if file:
                with open(f'{file}.json', 'w') as f:
                    f.write(json.dumps(variables, indent=4))
            else:
                return variables

    except gitlab.exceptions.GitlabHttpError as e:
        logger.error(f" {e}")
        if "403" in str(e):
            logger.error(f"You may not have enough permissions for selected project/group or it doesn't exists.")
        sys.exit(1)
    except gitlab.exceptions.GitlabUpdateError as e:
        logger.error(f" {e}")
        if "403" in str(e):
            logger.error(f"You may not have enough permissions for selected project/group or it doesn't exists.")
        sys.exit(1)
    except Exception as e:
        logger.error(f" {e}")
        sys.exit(1)
    else:
        logger.info('All variables have been fetched.')


def remove_variables(client=None, resource_type=None, resource_id=None):
    try:
        if client and resource_type and resource_id:
            resource = None
            resource_path = ''

            if resource_type == 'project':
                resource = client.projects.get(resource_id)
                resource_path = resource.path_with_namespace
            if resource_type == 'group':
                resource = client.groups.get(resource_id)
                resource_path = resource.full_path

            logger.info(f'Removing variables for {resource_type} id {resource_id} ({resource_path})')
            if resource:
                for gv in resource.variables.list():
                    if not DRY_RUN:
                        resource.variables.delete(
                            gv.key,
                            filter={
                                "environment_scope": gv.environment_scope
                            }
                        )
    except gitlab.exceptions.GitlabHttpError as e:
        logger.error(f" {e}")
        if "403" in str(e):
            logger.error(f"You may not have enough permissions for selected project/group or it doesn't exists.")
        sys.exit(1)
    except gitlab.exceptions.GitlabUpdateError as e:
        logger.error(f" {e}")
        if "403" in str(e):
            logger.error(f"You may not have enough permissions for selected project/group or it doesn't exists.")
        sys.exit(1)
    except Exception as e:
        logger.error(f" {e}")
        sys.exit(1)
    else:
        logger.info('All variables have been removed.')


def set_variables(client=None, resource_type=None, resource_id=None, file=None, variables=None, force=False):
    try:
        if client and resource_type and resource_id:
            resource = None
            resource_path = ''

            if resource_type == 'project':
                resource = client.projects.get(resource_id)
                resource_path = resource.path_with_namespace
            if resource_type == 'group':
                resource = client.groups.get(resource_id)
                resource_path = resource.full_path

            logger.info(f'Setting variables for {resource_type} id {resource_id} ({resource_path})')

            if file:
                with open(file, 'r') as f:
                    variables = json.load(f)

            if variables:
                for v in variables:
                    if resource_type == 'group':
                        if v.get('environment_scope') and v.get('environment_scope') != '*':
                            if force:
                                logger.warning(f'Variable {v.get("key")} is not global scoped ({v.get("environment_scope")}), but force is enabled - converting to global scoped in group')
                            else:
                                logger.warning(f'Variable {v.get("key")} is not global scoped ({v.get("environment_scope")}), omitting...')
                                continue
                    logger.debug(f'Adding {v.get("key")} variable...')
                    try:
                        if not DRY_RUN:
                            resource.variables.create(v)
                    except gitlab.exceptions.GitlabCreateError:
                        if force:
                            key = v.pop('key')
                            if not DRY_RUN:
                                resource.variables.update(key, v)
                        else:
                            logger.warning(f'Variable {v.get("key")} already exists. Delete it first or overwrite forcefully.')
    except gitlab.exceptions.GitlabHttpError as e:
        logger.error(f" {e}")
        if "403" in str(e):
            logger.error(f"You may not have enough permissions for selected project/group or it doesn't exists.")
        sys.exit(1)
    except gitlab.exceptions.GitlabUpdateError as e:
        logger.error(f" {e}")
        if "403" in str(e):
            logger.error(f"You may not have enough permissions for selected project/group or it doesn't exists.")
        sys.exit(1)
    except Exception as e:
        logger.error(f" {e}")
        sys.exit(1)
    else:
        logger.info('All variables have been added or updated.')


def parse_args(raw_args):
    """
    Parses script parameters.
    :param raw_args: Script parameters.
    :return: Parsed parameters.
    """
    p = argparse.ArgumentParser(
        prog="variables.py",
        formatter_class=lambda prog: (argparse.RawDescriptionHelpFormatter(prog, max_help_position=60, width=120)))

    p.add_argument('-s', '--source', required=False,
                   help='Source of env variables (eg. p:748 or project:748 or g:748 or group:748)')
    p.add_argument('-d', '--dest', required=False,
                   help='Destination of env variables (eg. p:748 or project:748 or g:748 or group:748)')
    p.add_argument('--clear', required=False, action='store_true',
                   help='Clear all env variables in source and/or destination')
    p.add_argument('--force', required=False, action='store_true',
                   help='Force overwriting of existing variables.')

    p.add_argument('--dry-run', required=False, action='store_true',
                   help='Does not save the results to a local CSV file.')
    p.add_argument('-l', '--level', required=False,
                   help='Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL).')
    p.add_argument('-t', '--token', required=False,
                   help='Personal Access Tokens; or pass it with GITLAB_TOKEN variable.')

    if len(sys.argv) == 1:
        p.print_help()
        sys.exit(1)

    return p.parse_args(raw_args), p


def main():
    args, parser = parse_args(sys.argv[1:])

    init_logger(args.level)

    if args.dry_run:
        global DRY_RUN
        DRY_RUN = args.dry_run

    if not (args.token or GITLAB_TOKEN):
        logger.error('You need to provide GitLab Token using -t | --token parameter or GITLAB_TOKEN variable.')
        sys.exit(2)

    token = args.token or GITLAB_TOKEN

    gl_client = gitlab_client(token)
    args.source = args.source or ""
    args.dest = args.dest or ""
    s_type = any(res in args.source for res in ["p:", "project:"]) and "project" or any(res in args.source for res in ["g:", "group:"]) and "group" or any(res in args.source for res in ["f:", "file:"]) and "file"
    d_type = any(res in args.dest for res in ["p:", "project:"]) and "project" or any(res in args.dest for res in ["g:", "group:"]) and "group" or any(res in args.dest for res in ["f:", "file:"]) and "file"
    if args.source and args.dest:
        if not s_type:
            logger.error(f'Undefined resource type: {args.source}')
            sys.exit(2)
        if not d_type:
            logger.error(f'Undefined resource type: {args.dest}')
            sys.exit(2)

        if args.clear:
            if s_type == "file":
                with open(args.source.split(":")[-1], 'w') as f:
                    f.write(json.dumps([]))
            else:
                remove_variables(gl_client, s_type, args.source.split(":")[-1])

            if d_type == "file":
                with open(args.dest.split(":")[-1], 'w') as f:
                    f.write(json.dumps([]))
            else:
                remove_variables(gl_client, d_type, args.dest.split(":")[-1])
        else:
            if s_type == "file":
                with open(args.source.split(":")[-1], 'r') as f:
                    s_variables = json.load(f)
                if d_type == "file":
                    with open(args.dest.split(":")[-1], 'w') as f:
                        f.write(json.dumps(s_variables))
                else:
                    set_variables(gl_client, d_type, args.dest.split(":")[-1], None, s_variables, args.force)
            else:
                s_variables = get_variables(gl_client, s_type, args.source.split(":")[-1])
                if d_type == "file":
                    with open(args.dest.split(":")[-1], 'w') as f:
                        f.write(json.dumps(s_variables))
                else:
                    set_variables(gl_client, d_type, args.dest.split(":")[-1], None, s_variables, args.force)

    elif args.source:
        if args.clear:
            if s_type == "file":
                with open(args.source.split(":")[-1], 'w') as f:
                    f.write(json.dumps([]))
            else:
                remove_variables(gl_client, s_type, args.source.split(":")[-1])

    elif args.dest:
        if args.clear:
            if d_type == "file":
                with open(args.dest.split(":")[-1], 'w') as f:
                    f.write(json.dumps([]))
            else:
                remove_variables(gl_client, d_type, args.dest.split(":")[-1])
    else:
        parser.print_help()
        sys.exit(1)


if __name__ == '__main__':
    sys.exit(main())
