"""Wrapped client for the configured GitLab bot."""

import logging
import random
import re

import gitlab

from gitlab_bot.models import GitlabConfig

log = logging.getLogger(__name__)


class GitlabBot(object):

    """Bot for doing GitLab stuff. Might be used by the IRC bot, or daemons, or whatever."""

    REVIEWS_START_FORMAT = "Starting review process."
    REVIEWS_RESET_FORMAT = "Review process reset."
    REVIEWS_REVIEW_REGISTERED_FORMAT = "Review identified."
    REVIEWS_PROGRESS_FORMAT = "{0:d} reviews of {1:d} necessary to proceed."
    REVIEWS_REVIEWS_COMPLETE = "Reviews complete."
    NEW_REVIEWER_FORMAT = "Assigning to {0:s} to review merge request."
    NEW_ACCEPTER_FORMAT = "Assigning to {0:s} to accept merge request."

    NOTE_COMMENT = [
        "The computer is your friend.",
        "Trust the computer.",
        "Quality is mandatory.",
        "Errors show your disloyalty to the computer.",
    ]

    def __init__(self):
        """Initialize the actual GitLab client."""
        config = GitlabConfig.objects.first()
        self.client = gitlab.Gitlab(config.url, private_token=config.token)
        self.client.auth()

    def random_reviews_start_message(self):
        return "{0:s} {1:s} {2:s}".format(self.REVIEWS_START_FORMAT, self.REVIEWS_PROGRESS_FORMAT,
                                          random.choice(self.NOTE_COMMENT))

    def random_reviews_reset_message(self):
        return "{0:s} {1:s} {2:s}".format(self.REVIEWS_RESET_FORMAT, self.REVIEWS_PROGRESS_FORMAT,
                                          random.choice(self.NOTE_COMMENT))

    def random_review_progress_message(self):
        return "{0:s} {1:s} {2:s}".format(self.REVIEWS_REVIEW_REGISTERED_FORMAT, self.REVIEWS_PROGRESS_FORMAT,
                                          random.choice(self.NOTE_COMMENT))

    def random_reviews_complete_message(self):
        return "{0:s} {1:s}".format(self.REVIEWS_REVIEWS_COMPLETE, random.choice(self.NOTE_COMMENT))

    def random_new_reviewer_message(self):
        return "{0:s} {1:s}".format(self.NEW_REVIEWER_FORMAT, random.choice(self.NOTE_COMMENT))

    def random_reviews_done_message(self):
        return "{0:s} {1:s}".format(self.NEW_ACCEPTER_FORMAT, random.choice(self.NOTE_COMMENT))

    def scan_project_for_reviews(self, project, merge_request_ids=None):
        project_obj = self.client.projects.get(project.project_id)
        if not project_obj:
            return

        if merge_request_ids:
            merge_requests = []
            for merge_request_id in merge_request_ids:
                merge_requests.append(project_obj.mergerequests.get(id=merge_request_id))
        else:
            merge_requests = project_obj.mergerequests.list(state='opened')

        for merge_request in merge_requests:
            log.debug("scanning merge request '%s'", merge_request.title)

            if merge_request.state in ['merged', 'closed']:
                log.info("merge request '%s' is already %s, doing nothing", merge_request.title,
                         merge_request.state)
                continue

            request_state = _MergeRequestScanningState()
            notes = sorted(self.client.project_mergerequest_notes.list(project_id=merge_request.project_id,
                                                                       merge_request_id=merge_request.id,
                                                                       all=True),
                           key=lambda x: x.id)
            for note in notes:
                if not note.system:
                    log.debug("merge request '%s', note '%s' is a normal message", merge_request.title, note.id)
                    # note that we can't ignore note.system = True + merge_request.author, that might have been
                    # a new push or something, so we only ignore normal notes from the author
                    if note.author == merge_request.author:
                        log.debug("skipping note from the merge request author")
                    elif note.author.username == self.client.user.username:
                        log.debug("saw a message from myself, i might have already sent a notice i'm sitting on")
                        if note.body.find(self.REVIEWS_RESET_FORMAT) >= 0 and request_state.unlogged_approval_reset:
                            log.debug("saw a reset message, unsetting the flag")
                            request_state.unlogged_approval_reset = False
                        elif note.body.find(self.REVIEWS_START_FORMAT) >= 0 and request_state.unlogged_review_start:
                            log.debug("saw a start message, unsetting the flag")
                            request_state.unlogged_review_start = False
                        elif note.body.find(self.REVIEWS_REVIEW_REGISTERED_FORMAT) >= 0 and request_state.unlogged_approval:
                            log.debug("saw a review log message, unsetting the flag")
                            request_state.unlogged_approval = False
                        elif note.body.find(self.REVIEWS_REVIEWS_COMPLETE) >= 0 and request_state.unlogged_review_complete:
                            log.debug("saw a review complete message, unsetting the flag")
                            request_state.unlogged_review_complete = False
                        else:
                            log.debug("nothing in particular relevant in '%s'", note.body)
                    else:
                        if note.body.find("LGTM") >= 0:
                            log.debug("merge request '%s', note '%s' has a LGTM", merge_request.title, note.id)
                            request_state.unlogged_approval = True
                            request_state.approver_list.append(note.author.username)
                            log.debug("approvers: %s", request_state.approver_list)
                            if len(request_state.approver_list) < project.code_reviews_necessary:
                                log.debug("not enough code reviews yet, setting needs_reviewer")
                                request_state.needs_reviewer = True
                                request_state.needs_accepter = False
                            else:
                                log.debug("enough code reviews, setting needs_accepter")
                                request_state.needs_accepter = True
                                request_state.needs_reviewer = False
                                request_state.unlogged_review_complete = True
                        else:
                            log.debug("merge request '%s', note '%s' does not have a LGTM", merge_request.title,
                                      note.id)
                else:
                    log.debug("merge request '%s', note '%s' is a system message", merge_request.title, note.id)
                    if re.match(r'Added \d+ commit', note.body):
                        log.debug("resetting approval list, '%s' looks like a push!", note.body)

                        # only set the unlogged approval reset flag if there's some kind of progress
                        if len(request_state.approver_list) > 0:
                            request_state.unlogged_approval_reset = True
                            request_state.needs_reviewer = True
                        request_state.approver_list.clear()
                    else:
                        log.debug("leaving the approval list as it is, i don't think '%s' is a push", note.body)

            # do some cleanup
            excluded_review_candidates = request_state.approver_list + [merge_request.author.username]
            review_candidates = [x for x in project.code_reviewers.split(',') if x not in excluded_review_candidates]
            if merge_request.assignee:
                if request_state.needs_reviewer and merge_request.assignee.username in review_candidates:
                    log.debug("unsetting the needs_reviewer flag, the request is already assigned to one")
                    request_state.needs_reviewer = False
                elif request_state.needs_reviewer and merge_request.assignee.username == merge_request.author.username:
                    log.info("unsetting the needs_reviewer flag, the request is assigned to the author")
                    log.info("in this case we are assuming that the author has work to do, and will re/unassign")
                    request_state.needs_reviewer = False

            excluded_accept_candidates = [merge_request.author.username]
            accept_candidates = [x for x in project.code_review_final_merge_assignees.split(',') if x not in excluded_accept_candidates]
            if merge_request.assignee:
                if request_state.needs_accepter and merge_request.assignee.username in accept_candidates:
                    log.debug("unsetting the needs_accepter flag, the request is already assigned to one")
                    request_state.needs_accepter = False
                elif request_state.needs_accepter and merge_request.assignee.username == merge_request.author.username:
                    log.info("unsetting the needs_accepter flag, the request is assigned to the author")
                    log.info("in this case we are assuming that the author has work to do, and will re/unassign")
                    request_state.needs_accepter = False

            log.debug("%s", request_state.__dict__)

            # status message stuff
            if request_state.unlogged_review_start:
                log.info("sending message for start of reviews")
                msg = {'body': self.random_reviews_start_message().format(len(request_state.approver_list),
                                                                          project.code_reviews_necessary)}
                self.client.project_mergerequest_notes.create(msg, project_id=project_obj.id,
                                                              merge_request_id=merge_request.id)
            if request_state.unlogged_approval_reset:
                log.info("sending message for review reset")
                msg = {'body': self.random_reviews_reset_message().format(len(request_state.approver_list),
                                                                          project.code_reviews_necessary)}
                self.client.project_mergerequest_notes.create(msg, project_id=project_obj.id,
                                                              merge_request_id=merge_request.id)
            if request_state.unlogged_approval:
                log.info("sending message for code review progress")
                msg = {'body': self.random_review_progress_message().format(len(request_state.approver_list),
                                                                            project.code_reviews_necessary)}
                self.client.project_mergerequest_notes.create(msg, project_id=project_obj.id,
                                                              merge_request_id=merge_request.id)
            if request_state.unlogged_review_complete:
                log.info("sending message for code review complete")
                msg = {'body': self.random_reviews_complete_message()}
                self.client.project_mergerequest_notes.create(msg, project_id=project_obj.id,
                                                              merge_request_id=merge_request.id)

            # if there's a reviewer necessary, assign the merge request
            if len(request_state.approver_list) < project.code_reviews_necessary and request_state.needs_reviewer:
                log.debug("%s needs a code review", merge_request.title)
                if merge_request.assignee is not None:
                    log.debug("%s currently assigned to %s", merge_request.title, merge_request.assignee.username)

                if merge_request.assignee is None or merge_request.assignee.username not in review_candidates:
                    if len(review_candidates) > 0:
                        new_reviewer = review_candidates[merge_request.iid % len(review_candidates)]
                        log.debug("%s is the new reviewer", new_reviewer)

                        # get the user object for the new reviewer
                        new_reviewer_obj = self.client.users.get_by_username(new_reviewer)

                        # create note for the update
                        msg = {'body': self.random_new_reviewer_message().format(new_reviewer_obj.name)}
                        self.client.project_mergerequest_notes.create(msg, project_id=project_obj.id,
                                                                      merge_request_id=merge_request.id)

                        # assign the merge request to the new reviewer
                        self.client.update(merge_request, assignee_id=new_reviewer_obj.id)
                    else:
                        log.warning("no reviewers left to review %s, doing nothing", merge_request.title)
                else:
                    log.debug("needs_reviewer set but the request is assigned to a reviewer, doing nothing")

            # if there's an accepter necessary, assign the merge request
            if len(request_state.approver_list) >= project.code_reviews_necessary and request_state.needs_accepter:
                log.debug("%s needs an accepter", merge_request.title)
                if merge_request.assignee is not None:
                    log.debug("%s currently assigned to %s", merge_request.title, merge_request.assignee.username)

                if merge_request.assignee is None or merge_request.assignee.username not in accept_candidates:
                    if len(accept_candidates) > 0:
                        new_accepter = accept_candidates[merge_request.iid % len(accept_candidates)]
                        log.debug("%s is the new accepter", new_accepter)

                        # get the user object for the new accepter
                        new_accepter_obj = self.client.users.get_by_username(new_accepter)

                        # create note for the update
                        msg = {'body': self.random_reviews_done_message().format(new_accepter_obj.name)}
                        self.client.project_mergerequest_notes.create(msg, project_id=project_obj.id,
                                                                      merge_request_id=merge_request.id)

                        # assign the merge request to the new reviewer
                        self.client.update(merge_request, assignee_id=new_accepter_obj.id)
                    else:
                        log.warning("no accepters left to accept %s, doing nothing", merge_request.title)


class _MergeRequestScanningState(object):

    """Track the state of a merge request as it is scanned and appropriate action identified."""

    def __init__(self):
        """Set default flags/values."""
        self.approver_list = []

        self.unlogged_review_start = True
        self.unlogged_approval_reset = False
        self.unlogged_approval = False
        self.unlogged_review_complete = False
        self.needs_reviewer = True
        self.needs_accepter = False