#!/usr/bin/env python3

import argparse
import gitlab
import logging as log
import csv
import secrets
import subprocess
import os


class Student():
    """A Gitlab user

    Students are read from the CSV file that was exported from Stud.IP.
    For each user, a dummy LDAP user is created in Gitlab.
    Upon the first login Gitlab fetches the complete user using LDAP.
    """

    def __init__(self, user, mail, name, group):
        self.user = user
        self.email = mail
        self.name = name
        self.group = group

    def from_csv(csvfile):
        """Creates an iterable containing the users"""
        reader = csv.DictReader(csvfile, delimiter=';', quotechar='"')

        for line in reader:
            yield Student(line['Nutzernamen'], line['E-Mail'], line['Vorname']
                          + ' ' + line['Nachname'], line['Gruppe'])


def create_tag(project, tag, ref):
    """Creates protected tag on ref

    The tag is used by the abgabesystem to mark the state of a solution at the
    deadline
    """

    print('Project %s. Creating tag %s' % (project.path, tag))

    project.tags.create({
        'tag_name': tag,
        'ref': ref
    })


def get_students(gl, students_csv):
    """Returns already existing GitLab users for students from provided CSV file that have an account.
    """

    for student in Student.from_csv(students_csv):
        users = gl.users.list(search=student.user)
        if len(users) > 0:
            yield users[0]


def create_user(gl, student, ldap_base, ldap_provider):
    """Creates a GitLab user account student.
    Requires admin privileges.
    """

    user = gl.users.create({
        'email': student.email,
        'username': student.user,
        'name': student.name,
        'provider': ldap_provider,
        'skip_confirmation': True,
        'extern_uid': 'uid=%s,%s' % (student.user, ldap_base),
        'password': secrets.token_urlsafe(nbytes=32)
    })
    user.customattributes.set('group', student.group)

    return user


def create_users(gl, args):
    """Creates Gitlab users from exported students list
    """
    with open(args.students, encoding='iso8859') as students_csv:
        for student in Student.from_csv(students_csv):
            try:
                create_user(gl, student, args.ldap_base, args.ldap_provider)
            except gitlab.exceptions.GitlabCreateError:
                log.warn('Failed to create user: %s' % student.user)


def fork_reference(gl, reference, namespace, deploy_key):
    """Create fork of solutions for student.
    """

    fork = reference.forks.create({
        'namespace': namespace.id
    })
    project = gl.projects.get(fork.id)
    project.visibility = 'private'
    project.container_registry_enabled = False
    project.lfs_enabled = False
    deploy_key = project.keys.create({
        'title': "Deploy Key",
        'key': deploy_key
    })
    project.keys.enable(deploy_key.id)
    project.save()

    return project


def create_project(gl, group, user, reference, deploy_key):
    """Creates a namespace (subgroup) and forks the project with
    the reference solutions into that namespace
    """

    subgroup = None

    try:
        subgroup = gl.groups.create({
            'name': user.username,
            'path': user.username,
            'parent_id': group.id
        })
    except gitlab.exceptions.GitlabError as e:
        subgroups = group.subgroups.list(search=user.username)
        if len(subgroups) > 0 and subgroup[0].name == user.username:
            subgroup = subgroups[0]
            subgroup = gl.groups.get(subgroup.id, lazy=True)
        else:
            raise(e)
    try:
        subgroup.members.create({
            'user_id': user.id,
            'access_level': gitlab.DEVELOPER_ACCESS,
        })
    except gitlab.exceptions.GitlabError:
        log.warning('Failed to add student %s to its own group' % user.username)

    try:
        fork_reference(gl, reference, subgroup, deploy_key)
    except gitlab.exceptions.GitlabCreateError as e:
        log.warning(e.error_message)


def setup_course(gl, group, students_csv, deploy_key):
    """Sets up the internal structure for the group for use with the course
    """
    solution = None
    reference_project = None

    try:
        solution = gl.groups.create({
            'name': 'solutions',
            'path': 'solutions',
            'parent_id': group.id,
            'visibility': 'internal',
        })
    except gitlab.exceptions.GitlabCreateError as e:
        log.info('Failed to create solutions group. %s' % e.error_message)
        solutions = group.subgroups.list(search='solutions')
        if len(solutions) > 0 and solutions[0].name == 'solutions':
            solution = gl.groups.get(solutions[0].id, lazy=True)
        else:
            raise(gitlab.exceptions.GitlabCreateError(error_message='Failed to setup solutions subgroup'))

    try:
        reference_project = gl.projects.create({
            'name': 'solutions',
            'namespace_id': solution.id,
            'visibility': 'internal',
        })
        reference_project.commits.create({
            'branch': 'master',
            'commit_message': 'Initial commit',
            'actions': [
                {
                    'action': 'create',
                    'file_path': 'README.md',
                    'content': 'Example solutions go here',
                },
            ]
        })
    except gitlab.exceptions.GitlabCreateError as e:
        log.info('Failed to setup group structure. %s' % e.error_message)
        projects = solution.projects.list(search='solutions')
        if len(projects) > 0 and projects[0].name == 'solutions':
            reference_project = gl.projects.get(projects[0].id)
        else:
            raise(gitlab.exceptions.GitlabCreateError(error_message='Failed to setup reference solutions'))

    if solution is None or reference_project is None:
        raise(gitlab.exceptions.GitlabCreateError(error_message='Failed to setup course'))

    for user in get_students(gl, students_csv):
        create_project(gl, solution, user, reference_project, deploy_key)


def projects(gl, args):
    """Creates the projects for all course participants
    """
    groups = gl.groups.list(search=args.course)
    if len(groups) == 0 and groups[0].name == args.course:
        log.warn('This group does not exist')
    else:
        group = groups[0]
        with open(args.deploy_key, 'r') as key, open(args.students, encoding='iso8859') as students_csv:
            key = key.read()
            setup_course(gl, group, students_csv, key)


def deadline(gl, args):
    """Checks deadlines for course and triggers deadline if it is reached"""

    deadline_name = args.tag_name
    try:
        reference = gl.projects.get(args.reference, lazy=True)

        for fork in reference.forks.list():
            project = gl.projects.get(fork.id, lazy=False)
            try:
                create_tag(project, deadline_name, 'master')
            except gitlab.exceptions.GitlabCreateError as e:
                print(e.error_message)

    except gitlab.exceptions.GitlabGetError as e:
        print(e.error_message)


def plagiates(gl, args):
    """Runs the plagiarism checker (JPlag) for the solutions with a certain tag
    """

    tag = args.tag_name
    reference = gl.projects.get(args.reference, lazy=True)
    try:
        os.mkdir('solutions')
    except os.FileExistsError as e:
        print(e)
    os.chdir('solutions')

    for fork in reference.forks.list():
        project = gl.projects.get(fork.id, lazy=True)
        try:
            subprocess.run(
                ['git', 'clone', '--branch', tag, project.ssh_url_to_repo, project.path_with_namespace])
            os.chdir('..')
        except:
            print(e.error_message)

    subprocess.run(
        ['java', '-jar', args.jplag_jar, '-s', 'solutions', '-p', 'java', '-r', 'results', '-bc', args.reference, '-l', 'java17'])


def course(gl, args):
    """Creates the group for the course
    """
    try:
        group = gl.groups.create({
            'name': args.course,
            'path': args.course,
            'visibility': 'internal',
        })
        log.info('Created group %s' % args.course)
    except gitlab.exceptions.GitlabCreateError as e:
        log.warning('Failed to create group %s. %s' % (args.course, e.error_message))


if __name__ == '__main__':

    gl = gitlab.Gitlab.from_config()
    gl.auth()
    log.info('authenticated')

    parser = argparse.ArgumentParser()
    subparsers = parser.add_subparsers(title='subcommands')

    user_parser = subparsers.add_parser(
        'users',
        help='Creates users from LDAP')
    user_parser.set_defaults(func=create_users)
    user_parser.add_argument('-s', '--students', dest='students')
    user_parser.add_argument('-b', '--ldap-base', dest='ldap_base')
    user_parser.add_argument('-p', '--ldap-provider', dest='ldap_provider')

    course_parser = subparsers.add_parser(
        'courses',
        help='Creates a new course')
    course_parser.set_defaults(func=course)
    course_parser.add_argument('-c', '--course', dest='course')

    projects_parser = subparsers.add_parser(
        'projects',
        help='Sets up the projects and groups for a course')
    projects_parser.set_defaults(func=projects)
    projects_parser.add_argument('-c', '--course', dest='course')
    projects_parser.add_argument('-d', '--deploy-key', dest='deploy_key')
    projects_parser.add_argument('-s', '--students', dest='students')

    deadline_parser = subparsers.add_parser(
        'deadline',
        description='Sets the tags at a deadline to permanently mark it in the version history')
    deadline_parser.set_defaults(func=deadline)
    deadline_parser.add_argument('-t', '--tag-name', dest='tag_name')
    deadline_parser.add_argument('-r', '--reference', dest='reference')

    plagiates_parser = subparsers.add_parser(
        'plagiates',
        description='Runs the plagiarism checker on all solutions using a reference project as the baseline')
    plagiates_parser.set_defaults(func=plagiates)
    plagiates_parser.add_argument('-t', '--tag-name', dest='tag_name')
    plagiates_parser.add_argument('-r', '--reference', dest='reference')
    plagiates_parser.add_argument('-j', '--jplag-jar', dest='jplag_jar')

    args = parser.parse_args()

    log.basicConfig(filename='example.log', filemode='w', level=log.DEBUG)

    if 'func' in args:
        args.func(gl, args)
    else:
        parser.print_help()