#!/usr/bin/env python3

# Copyright: (c) 2019, Alexandre Iooss <erdnaxe@crans.org>
#
# GNU General Public License v3.0+

import re
import urllib.error
import urllib.parse
import urllib.request
import difflib


from ansible.errors import AnsibleError
from ansible.plugins.action import ActionBase
from ansible.utils.display import Display
from ansible.module_utils._text import to_native

display = Display()


class ActionModule(ActionBase):

    TRANSFERS_FILES = False
    _VALID_ARGS = frozenset(('url', 'user', 'password', 'content', 'revision_comment'))

    def login(self, url, user, password):
        """
        Log in and return session cookie or None if failed

        :param url: random wiki url (not root page)
        :param user: wiki user
        :param password: user's password
        :return: session cookie
        """
        # Send a HTTP POST request
        data = urllib.parse.urlencode({
            'action': 'login',
            'login': 'Connexion',
            'name': user,
            'password': password
        }).encode()
        req = urllib.request.Request(url, data)
        try:
            response = urllib.request.urlopen(req)
            cookie = response.getheader('set-cookie')
        except urllib.error.HTTPError as e:
            # If 404, then also return header
            cookie = e.getheader('set-cookie')

        # Check that authentication worked
        if not cookie:
            raise AnsibleError(to_native('server did not return a session cookie'))
        return cookie

    def craft_request(self, suffix):
        """
        Crafts a function that takes an url and a cookie,
        and returns the content of the requested page with given action suffix.
        """
        def f(url, cookie):
            req = urllib.request.Request(url + suffix)
            req.add_header("Cookie", cookie)
            content = urllib.request.urlopen(req).read().decode('utf-8')
            return content
        return f


    def edit_ticket(self, url, cookie):
        """
        Return edition ticket of url

        :param url: page to edit
        :param cookie: session cookie
        :return: edit ticket
        """
        # Send request with session cookie
        content = self.craft_request("?action=edit&editor=text")(url, cookie)

        # Search for ticket
        search = re.search('name=\"ticket\" value=\"([^\"]*)\"', content)
        if not search:
            raise AnsibleError(to_native('no edit ticket was found'))

        return search.group(1)


    def edit(self, url, user, password, content, revision_comment, cookie):
        """
        Edit a MoinMoin wiki page

        :param url: page to edit
        :param user: wiki user
        :param password: user's password
        :param content: content to place on this page
        :param revision_comment: revision comment
        """
        # Connect and get edit ticket
        ticket = self.edit_ticket(url, cookie)

        # Create request and send
        data = {
            'button_save': 'Enregistrer les modifications',
            'category': '',
            'comment': revision_comment.encode("utf-8"),
            'savetext': content.encode("utf-8"),
            'action': 'edit',
            'ticket': ticket
        }
        req = urllib.request.Request(url, urllib.parse.urlencode(data).encode())
        req.add_header("Cookie", cookie)
        urllib.request.urlopen(req)


    def run(self, tmp=None, task_vars=None):
        """
        The run method is the main Action Plugin driver. All work is done from within this 	method.

        tmp: Temporary directory. Sometimes an action plugin sets up
             a temporary directory and then calls another module. This parameter
             allows us to reuse the same directory for both.

        task_vars: The variables (host vars, group vars, config vars, etc) associated with this task.
                   Note that while this will contain Ansible facts from the host, they should be used
                   with caution as a user running Ansible can disable their collection. If you want
                   make sure that your Action Plugin always has access to the ones it needs, you may
                   want to consider running the setup module directly in the run the method and getting
                   the Ansible facts that way.
                   The strategy plugin which manages running tasks on instances uses an ansible.vars.manager
                   VariableManager instance to retrieve this context specific dict of variables.
        """
        if task_vars is None:
            task_vars = dict()


        result = super(ActionModule, self).run(tmp, task_vars)
        del tmp


        url = self._task.args.get("url")
        user = self._task.args.get("user")
        password = self._task.args.get("password")
        content = self._task.args.get("content")
        revision_comment = self._task.args.get("revision_comment")

        cookie = self.login(url, user, password)

        try:
            raw = self.craft_request("?action=raw")(url, cookie)
        except urllib.error.HTTPError:  # We will create the page.
            raw = ""

        diff = difflib.unified_diff(raw.splitlines(), content.splitlines(), fromfile="before", tofile=f"after: {url}", lineterm="")
        i=0

        # Display any change
        for line in diff:
            i+=1
            if line.startswith("-"):
                display.display(line, "red")
            elif line.startswith("+"):
                display.display(line, "green")
            elif line.startswith("@"):
                display.display(line, "yellow")
            else:
                display.display(line)

        # Do apply the change if not in check mode
        if not self._play_context.check_mode:
            self.edit(url, user, password, content, revision_comment, cookie)

        result['changed']=i>0

        self._supports_check_mode = True
        self._supports_async = False

        return result