"""
Margebot: A Errbot Plugin for Gitlab MR reminders
"""
from datetime import datetime, timezone
from dateutil import parser
from dateutil.tz import tzutc
from dateutil.relativedelta import relativedelta
from errbot import BotPlugin, botcmd, arg_botcmd, re_botcmd, webhook
from errbot.templating import tenv
from errcron.bot import CrontabMixin
import gitlab


def deltastr(any_delta):
    """
    Output a datetime delta in the format "x days, y hours, z minutes ago"
    """
    l_delta = []
    days = any_delta.days
    hours = any_delta.seconds // 3600
    mins = (any_delta.seconds // 60) % 60

    for (key, val) in [("day", days), ("hour", hours), ("minute", mins)]:
        if val == 1:
            l_delta.append("1 " + key)
        elif val > 1:
            l_delta.append("{} {}s".format(val, key))

    if l_delta == []:
        retval = "now"
    else:
        retval = ", ".join(l_delta) + " ago"
    return retval


class Marge(BotPlugin, CrontabMixin):
    """
    I remind you about merge requests

    Use:
       In gitlab:
          Add a merge request webook of the form
             'https://webookserver/margeboot/<rooms>'
          to the projects you want tracked.  <rooms> should be a
          comma-separated list of short room names (anything before the '@')
          that you want notified.
       In errbot:
          Add <roomname>@domain to the CHATROOM_PRESENCE list in config.py for
          rooms margebot should join
    """

    CRONTAB = [
        # Set in config now: '0 11,17 * * * .crontab_hook'    # 7:00AM and 1:00PM EST warnings
    ]

    def __init__(self, *args, **kwargs):
        self.git_host = None
        self.chatroom_host = None
        self.gitlab = None
        self.soak_delta = None
        self.webhook_url = None
        super().__init__(*args, **kwargs)

    def get_configuration_template(self):
        """
        GITLAB_HOST:      Host name of your gitlab server
        GITLAB_ADMIN_TOKEN: PAT from an admin's https://${GIT_HOST}/profile/personal_access_tokens.
        CHATROOM_HOST: Chatroom host.  Usually 'chatroom' + FQDN of Jabber server
        CRONTAB: Schedule of automated merge request checks in '%M %H %d %m %w' format
        VERIFY_SSL : True, False, or path to CA cert to verify cert
        CRONTAB_SOAK_HOURS : Don't send out reminders about MRs opened less than this many hours
        WEBHOOK_URL :  URL to use for defining MR integration in gitlab
        """
        return {'GITLAB_HOST': 'gitlab.example.com',
                'GITLAB_ADMIN_TOKEN': 'gitlab-admin-user-private-token',
                'CHATROOM_HOST': 'conference.jabber.example.com',
                'CRONTAB': '0 11,17 * * *',
                'VERIFY_SSL': True,
                'CRONTAB_SOAK_HOURS': 1,
                'WEBHOOK_URL': 'https://webhooks.example.com:3142/margebot/'}

    def check_configuration(self, configuration):
        """
        Check that the plugin has been configured properly
        """
        super().check_configuration(configuration)

    def activate(self):
        """
        Initialization done when the plugin is activated
        """
        if not self.config:
            self.log.info('Margebot is not configured. Forbid activation')
            return
        self.git_host = self.config['GITLAB_HOST']
        self.chatroom_host = self.config['CHATROOM_HOST']
        Marge.CRONTAB = ['{} .crontab_hook'.format(self.config['CRONTAB'])]
        gitlab_auth_token = self.config['GITLAB_ADMIN_TOKEN']
        verify_ssl = self.config['VERIFY_SSL']
        self.gitlab = gitlab.Gitlab('https://' + self.git_host, gitlab_auth_token, ssl_verify=verify_ssl)
        self.activate_crontab()

        self.soak_delta = relativedelta(hours=self.config['CRONTAB_SOAK_HOURS'])
        self.webhook_url = self.config['WEBHOOK_URL']
        if self.webhook_url[-1] != '/':
            self.webhook_url += '/'
        super().activate()

    def deactivate(self):
        """
        Anything that needs to be tore down when the plugin is deactivated goes here.
        """
        super().deactivate()

    @webhook('/margebot/<rooms>/')
    def gitlab_hook(self, request, rooms):
        """
        Webhook that listens on http://<server>:<port>/gitlab
        """

        self.log.info("webhook: request: {}, rooms: {}".format(request, rooms))
        # self.log.info("state: {}".format(request['object_attributes']['state']))

        # verify it's a merge request
        if request['event_type'] != 'merge_request':
            self.log.error('unexpecting event_type: {}'.format(request['event_type']))
        elif 'opened' in request['object_attributes']['state']:

            if request['object_attributes']['work_in_progress']:
                wip = "WIP "
            else:
                wip = ""

            url = request['project']['homepage']
            title = request['object_attributes']['title']
            author_id = request['object_attributes']['author_id']  # map this to user name ...
            try:
                author = self.gitlab.users.get(author_id)
                author_name = author.attributes['username']
            except Exception as exp:
                self.log.info("unexpected author_id {}, exp={}".format(author_id, exp))
                author_name = author_id

            target_project_id = request['object_attributes']['target_project_id']
            iid = request['object_attributes']['iid']

            # If the MR is tagged 'never-close' or 'abandoned' ignore it
            if 'labels' in request:
                for a_label in request['labels']:
                    if a_label['title'] in ['never-close', 'abandoned']:
                        self.log.info("Skipping {} notice for {} MR".format(a_label['title'], url))
                        return "OK"

            msg_template = "Hi there ! {} has opened a new {}MR: \"{}\"\n{}/merge_requests/{}"
            msg = msg_template.format(author_name, wip, title, url, iid)

            if 'OPEN_MRS' not in self.keys():
                empty_dict = {}
                self['OPEN_MRS'] = empty_dict

            open_mrs = self['OPEN_MRS']

            if (target_project_id, iid, rooms) not in open_mrs:
                for a_room in rooms.split(','):
                    if self.config:
                        self.send(self.build_identifier(a_room + '@' + self.chatroom_host), msg)

                self.log.info("webhook: Saving ({}, {}, {})".format(target_project_id, iid, rooms))
                open_mrs[(target_project_id, iid, rooms)] = True
                self['OPEN_MRS'] = open_mrs

        # TODO:  Add check if an MR has toggled the WIP indicator
        # (trigger on updates (what's that look like in request['object_attributes']['state'])
        # Then check in request['changes']['title']['previous'] starts with 'WIP:'
        # but not request['changes']['title']['current'], and vice versa
        # See https://gitlab.com/gitlab-org/gitlab-ce/issues/53529

        return "OK"

    def mr_status_msg(self, a_mr, author=None):
        """
        Create the merge request status message
        """
        self.log.info("mr_status_msg: a_mr: {}".format(a_mr))
        mr_attrs = a_mr.attributes

        # Only weed out MRs less than the soak time for the crontab output (where author==None)
        now = datetime.now(timezone.utc)
        creation_time = parser.parse(mr_attrs['created_at'], tzinfos=[tzutc()]).astimezone(timezone.utc)
        if not author:
            self.log.info("times: {}, {}, {}".format(creation_time, self.soak_delta, now))
            if creation_time + self.soak_delta > now:
                project_id = mr_attrs['target_project_id']
                mr_id = mr_attrs['id']
                soak_hours = self.config['CRONTAB_SOAK_HOURS']
                info_template = "skipping: MR <{},{}> was opened less than {} hours ago"
                info_msg = info_template.format(project_id, mr_id, soak_hours)
                self.log.info(info_msg)
                return None

        str_open_since = deltastr(now - creation_time)

        warning = ""
        if mr_attrs['work_in_progress']:
            warning = "still WIP"
        elif mr_attrs['merge_status'] != 'can_be_merged':
            warning = "there are merge conflicts"

        if author:
            authored = (mr_attrs['author']['id'] == author)
        else:
            authored = False

        msg = "{} (opened by {} {})".format(mr_attrs['web_url'], mr_attrs['author']['username'], str_open_since)
        upvotes = mr_attrs['upvotes']
        if upvotes >= 2:
            msg += ": Has 2+ upvotes and could be merged in now"
            if warning != "":
                msg += " except {}.".format(warning)
            else:
                msg += "."

        elif upvotes == 1:
            self.log.error("pre-award a_mr: {}".format(dir(a_mr.awardemojis)))
            award_emoji = a_mr.awardemojis.list()
            self.log.info("award_emoji: {}".format(award_emoji))
            already_approved = "someone"
            for an_emoji in award_emoji:
                emoji_attr = an_emoji.attributes
                if emoji_attr["name"] == "thumbsup":
                    already_approved = emoji_attr["user"]["username"]

            if authored:
                msg += ": Your MR has been approved by " + already_approved + " and is waiting for another upvote"
            else:
                msg += ": Approved by " + already_approved + " and waiting for another upvote"
            if warning != "":
                msg += " but {}.".format(warning)
            else:
                msg += "."

        else:
            if authored:
                msg += ": Your MR has no upvotes"
            else:
                msg += ": No upvotes, please review"
            if warning != "":
                msg += " but {}.".format(warning)
            else:
                msg += "."

        return {'creation_time': creation_time, 'msg': msg}

    def crontab_hook(self, polled_time):
        """
        Send a scheduled message to the rooms margebot is watching
        about open MRs the room cares about.
        """

        self.log.info("crontab_hook triggered at {}".format(polled_time))

        reminder_msg = {}  # Map of reminder_msg['roomname@domain'] = [(created_at,msg)]

        # initialize the reminders
        rooms = self.rooms()
        for a_room in rooms:
            self.log.info("poller: a_room.node: {}".format(a_room.node))
            reminder_msg[a_room.node] = []

        still_open_mrs = {}

        # Let's walk through the MRs we've seen already:
        open_mrs = self['OPEN_MRS']

        for (project_id, mr_id, notify_rooms) in open_mrs:

            a_project = self.gitlab.projects.get(project_id)
            if not a_project:
                self.log.debug("Couldn't find project: {}".format(project_id))
                continue

            # Lookup the MR from the project/id
            a_mr = a_project.mergerequests.get(mr_id)
            mr_attrs = a_mr.attributes
            if not a_mr:
                self.log.debug("Couldn't find project: {}, id: {}".format(project_id, mr_id))
                continue

            # If the MR is tagged 'never-close' or 'abandoned', ignore it
            if 'labels' in mr_attrs and ('never-close' in mr_attrs['labels'] or 'abandoned' in mr_attrs['labels']):
                continue

            self.log.info("a_mr: {} {} {} {}".format(project_id, mr_id, notify_rooms, mr_attrs['state']))

            # If the MR is no longer open, skip to the next MR,
            # and don't include this MR in the next check
            if 'opened' not in mr_attrs['state']:
                continue
            else:
                still_open_mrs[(project_id, mr_id, notify_rooms)] = True

            msg_dict = self.mr_status_msg(a_mr)
            if msg_dict is None:
                continue

            for a_room in notify_rooms.split(','):
                if a_room in reminder_msg:
                    reminder_msg[a_room].append(msg_dict)
                else:
                    self.log.error("{} not in reminder_msg (project_id={}, mr_id={})".format(a_room, project_id, mr_id))

        self['OPEN_MRS'] = open_mrs

        # Remind each of the rooms about open MRs
        for a_room, room_msg_list in reminder_msg.items():
            if room_msg_list != []:
                if self.config:
                    to_room = a_room + '@' + self.config['CHATROOM_HOST']
                    msg = tenv().get_template('reviews.md').render(msg_list=room_msg_list)
                    self.send(self.build_identifier(to_room), msg)

        self['OPEN_MRS'] = still_open_mrs

    @botcmd(template="reviews")
    def reviews(self, msg, args):
        """
        Returns a list of MRs that are waiting for some luv.
        Also returns a list of MRs that have had enough luv but aren't merged in yet.
        """
        # Sending directly to Margbot:  sender in the form sender@....
        # Sending to a chatroom:  snder in the form room@rooms/sender

        log_template = 'reviews: msg: {}, args: {}, \nmsg.frm: {}, \nmsg.msg.frm: {}, chatroom_host: {}'
        self.log.info(log_template.format(msg, args, msg.frm.__dict__, dir(msg.frm), self.config['CHATROOM_HOST']))
        self.log.info('reviews: bot mode: {}'.format(self._bot.mode))

        if self._bot.mode == "xmpp":
            if msg.frm.domain == self.config['CHATROOM_HOST']:
                sender = msg.frm.resource
            else:
                sender = msg.frm.node
        else:
            sender = str(msg.frm).split('@')[0]

        keys = self.keys()
        if 'OPEN_MRS' not in keys:
            self.log.error('OPEN_MRS not in {}'.format(keys))
            return "No MRs to review"

        sender_gitlab_id = None
        try:
            sender_users = self.gitlab.users.list(username=sender)
            sender_gitlab_id = sender_users[0].attributes['id']
        except Exception as exp:
            self.log.error('problem mapping {} to a gitlab user, exp={}'.format(sender, exp))
            sender_gitlab_id = None

        # Walk through the MRs we've seen already:
        msg_list = []
        msg = ""
        still_open_mrs = {}
        open_mrs = self['OPEN_MRS']
        self.log.info('open_mrs: {}'.format(open_mrs))
        for (project, mr_id, notify_rooms) in open_mrs:

            try:
                a_project = self.gitlab.projects.get(project)
            except Exception as exp:
                self.log.debug("Couldn't find project: {}, exp: {}".format(project, exp))
                continue

            # Lookup the MR from the project/id
            try:
                a_mr = a_project.mergerequests.get(mr_id)
                mr_attrs = a_mr.attributes
            except Exception as exp:
                self.log.debug("Couldn't find project: {}, id: {}, exp: {}".format(project, mr_id, exp))
                continue

            self.log.info('project: {}, id: {}, a_mr: {}'.format(project, id, a_mr))

            # If the MR is tagged 'never-close' or 'abandoned',  ignore it
            if 'labels' in mr_attrs and ('never-close' in mr_attrs['labels'] or 'abandoned' in mr_attrs['labels']):
                continue

            # If the MR is no longer open, skip to the next MR,
            # and don't include this MR in the next check
            if 'opened' not in mr_attrs['state']:
                self.log.info('state not opened: {}'.format(mr_attrs['state']))
                continue
            else:
                still_open_mrs[(project, mr_id, notify_rooms)] = True

            msg_dict = self.mr_status_msg(a_mr, author=sender_gitlab_id)
            if msg_dict is None:
                continue

            msg_list.append(msg_dict)

        self['OPEN_MRS'] = still_open_mrs

        return {'sender': sender, 'msg_list': msg_list}

    @arg_botcmd('rooms', type=str, help="Comma-separated room list without @conference-room suffix")
    @arg_botcmd('repo', type=str, help="repo to start watching for MRs in NAMESPACE/PROJECT_NAME format")
    def watchrepo(self, msg, repo, rooms):
        """
        Add the margebot webhook to a repo, and prepopulate any open MRs in the repo with margebot
        """
        self.log.info("msg={}".format(msg))
        self.log.info("repo={}".format(repo))
        self.log.info("rooms={}".format(rooms))

        # get the group/repo repo, error out if it doesn't exist
        project = self.gitlab.projects.get(repo)
        if not project:
            msg = "Couldn't find repo {}".format(repo)
            self.log.info("watchrepo: {}".format(msg))
            return msg

        self.log.info('project: {}'.format(project))

        target_project_id = project.id

        # Check is the project already includes the margebot hook
        # If no hooks, will it return False or [] ?
        marge_hook = None
        hooks = project.hooks.list()
        self.log.error("hooks = {}".format(hooks))
        if hooks is False:
            msg = "Couldn't find {} hooks".format(repo)
            self.log.error("watchrepo: {}".format(msg))
            return msg
        for a_hook in hooks:
            self.log.info('a_hook: {}'.format(a_hook))
            hook_attributes = a_hook.attributes
            if hook_attributes['merge_requests_events'] and hook_attributes['url'].startswith(self.webhook_url):
                marge_hook = a_hook
                break

        # If so replace it (or error out ?)
        url = "{}{}".format(self.webhook_url, rooms)  # webhooks_url will end in '/'
        hook_updated = True
        if marge_hook:
            old_rooms = marge_hook.attributes['url'].split(self.webhook_url, 1)[1]
            if old_rooms == rooms:
                msg = "Already reporting {} MRs to the {} room(s)".format(repo, rooms)
                self.log.info('watchrepo: {}'.format(msg))
                return msg
            try:
                s_action = "update"
                marge_hook.attributes['url'] = url
                marge_hook.save()
                s_watch_msg = "Updating room list for {} MRs from {} to {}".format(repo, old_rooms, rooms)
            except Exception as exp:
                hook_updated = False
                self.log.error("watchrepo; update hook {}  raised exception {}".format(repo, exp))
        else:
            try:
                s_action = "add"
                project.hooks.create({'url': url, 'merge_requests_events': 1, 'enable_ssl_verification': True})
                s_watch_msg = "Now watching for new MRs in the {} repo to the {} room(s)".format(repo, rooms)
            except Exception as exp:
                hook_updated = False
                self.log.error("watchrepo; create hook {}  raised exception {}".format(repo, exp))

        if not hook_updated:
            msg = "Couldn't {} hook: {}".format(s_action, repo)
            self.log.error("watchrepo: {}".format(msg))
            return msg

        open_mrs = self['OPEN_MRS']
        mr_count = 0
        # get the open MRs in the repo

        # If updating the room list, walk through the existing MR list
        if s_action == "update":
            new_open_mrs = {}
            for (project_id, mr_id, old_rooms) in open_mrs:
                # pragma pylint: disable=simplifiable-if-statement
                if project_id == target_project_id:
                    new_open_mrs[(project_id, mr_id, rooms)] = True
                else:
                    new_open_mrs[(project_id, mr_id, old_rooms)] = True
                # pragma pylint: enable=simplifiable-if-statement
            open_mrs = new_open_mrs

        # If adding a new repo, check for existing opened MRs in the repo.
        else:
            for state in ['opened']:
                a_project = self.gitlab.projects.get(target_project_id)
                mr_list = a_project.mergerequests.list(state=state)
                for an_mr in mr_list:
                    mr_count += 1
                    mr_id = an_mr.attributes['iid']
                    open_mrs[(target_project_id, mr_id, rooms)] = True

        self['OPEN_MRS'] = open_mrs

        if mr_count == 0:
            mr_msg = "No open MRs were found in the repo."
        elif mr_count == 1:
            mr_msg = "1 open MR was found in the repo.  Run !reviews to see the updated MR list."
        else:
            mr_msg = "{} open MRs were found in the repo.  Run !reviews to see the updated MR list.".format(mr_count)
        return "{}\n{}".format(s_watch_msg, mr_msg)

    # pragma pylint: disable=unused-argument

    # Check Chucklebot for the chuckles

    @re_botcmd(pattern=r".*", prefixed=True)
    def catchall(self, msg, args):
        """
        Don't have the bot complain about unknown commands if the first word in a msg is its name
        """
        return

    # pragma pylint: enable=unused-argument
