#!/usr/bin/env python
# coding: utf-8

from __future__ import unicode_literals
from __future__ import print_function
from simplejson.scanner import JSONDecodeError
import logging
import re
import requests


import hmac
import base64
import struct
import hashlib
import time

logger = logging.getLogger(__name__)


def get_hotp_token(secret, intervals_no):
    key = base64.b32decode(secret, True)
    msg = struct.pack(">Q", intervals_no)
    h = bytes(hmac.new(key, msg, hashlib.sha1).digest())
    o = h[19] & 15
    h = (struct.unpack(">I", h[o : o + 4])[0] & 0x7FFFFFFF) % 1000000
    return h


def get_totp_token(secret):
    return get_hotp_token(secret, intervals_no=int(time.time()) // 30)


class Guacamole:
    def __init__(
        self,
        hostname,
        username,
        password,
        secret=None,
        method="https",
        url_path="/",
        default_datasource=None,
        cookies=False,
        verify=True,
    ):
        if method.lower() not in ["https", "http"]:
            raise ValueError("Only http and https methods are valid.")
        if not url_path:
            url_path = "/"
        self.REST_API = "{}://{}{}/api".format(method, hostname, url_path)
        self.username = username
        self.password = password
        self.secret = secret
        self.verify = verify
        resp = self.__authenticate()
        auth = resp.json()
        assert "authToken" in auth, "Failed to retrieve auth token"
        assert "dataSource" in auth, "Failed to retrieve primaray data source"
        assert "availableDataSources" in auth, "Failed to retrieve data sources"
        self.datasources = auth["availableDataSources"]
        if cookies:
            self.cookies = resp.cookies
        else:
            self.cookies = None
        if default_datasource:
            assert (
                default_datasource in self.datasources
            ), "Datasource {} does not exist. Possible values: {}".format(
                default_datasource, self.datasources
            )
            self.primary_datasource = default_datasource
        else:
            self.primary_datasource = auth["dataSource"]
        self.token = auth["authToken"]

    def __authenticate(self):
        parameters = {"username": self.username, "password": self.password}
        if self.secret is not None:
            parameters["guac-totp"] = get_totp_token(self.secret)
        r = requests.post(
            url=self.REST_API + "/tokens",
            data=parameters,
            verify=self.verify,
            allow_redirects=True,
        )
        r.raise_for_status()
        return r

    def __auth_request(
        self, method, url, payload=None, url_params=None, json_response=True
    ):
        params = [("token", self.token)]
        if url_params:
            params += url_params
        logger.debug(
            "{method} {url} - Params: {params}- Payload: {payload}".format(
                method=method, url=url, params=params, payload=payload
            )
        )
        r = requests.request(
            method=method,
            url=url,
            params=params,
            json=payload,
            verify=self.verify,
            allow_redirects=True,
            cookies=self.cookies,
        )
        if not r.ok:
            logger.error(r.content)
        r.raise_for_status()
        if json_response:
            try:
                return r.json()
            except JSONDecodeError:
                logger.error("Could not decode JSON response")
                return r
        else:
            return r

    def __no_auth_request(
        self, method, url, payload=None, url_params=None, json_response=True
    ):
        logger.debug(
            "{method} {url} - Params: {params}- Payload: {payload}".format(
                method=method, url=url, params=url_params, payload=payload
            )
        )
        r = requests.request(
            method=method,
            url=url,
            params=url_params,
            data=payload,
            verify=self.verify,
            allow_redirects=True,
            cookies=self.cookies,
        )
        if not r.ok:
            logger.error(r.content)
        r.raise_for_status()
        if json_response:
            try:
                return r.json()
            except JSONDecodeError:
                logger.error("Could not decode JSON response")
                return r
        else:
            return r

    def get_connections(self, datasource=None):
        if not datasource:
            datasource = self.primary_datasource
        params = [("permission", "UPDATE"), ("permission", "DELETE")]
        return self.__auth_request(
            method="GET",
            url="{}/session/data/{}/connectionGroups/ROOT/tree".format(
                self.REST_API, datasource
            ),
            url_params=params,
        )

    def get_connection_group_connections(
        self, connection_group_id, datasource=None
    ):
        """Get a list of connections linked to an organizational or balancing
        connection group"""
        if not datasource:
            datasource = self.primary_datasource
        return self.__auth_request(
            method="GET",
            url="{}/session/data/{}/connectionGroups/{}/tree".format(
                self.REST_API, datasource, connection_group_id
            ),
        )

    def get_active_connections(self, datasource=None):
        if not datasource:
            datasource = self.primary_datasource
        return self.__auth_request(
            method="GET",
            url="{}/session/data/{}/activeConnections".format(
                self.REST_API, datasource
            ),
        )

    def get_connection(self, connection_id, datasource=None):
        if not datasource:
            datasource = self.primary_datasource
        return self.__auth_request(
            method="GET",
            url="{}/session/data/{}/connections/{}".format(
                self.REST_API, datasource, connection_id
            ),
        )

    def get_connection_parameters(self, connection_id, datasource=None):
        if not datasource:
            datasource = self.primary_datasource
        return self.__auth_request(
            method="GET",
            url="{}/session/data/{}/connections/{}/parameters".format(
                self.REST_API, datasource, connection_id
            ),
        )

    def get_connection_full(self, connection_id, datasource=None):
        c = self.get_connection(connection_id, datasource)
        c["parameters"] = self.get_connection_parameters(
            connection_id, datasource
        )
        return c

    def __get_connection_by_name(self, cons, name, regex=False):
        # FIXME This need refactoring (DRY)
        if "childConnections" not in cons:
            if "childConnectionGroups" in cons:
                for c in cons["childConnectionGroups"]:
                    res = self.__get_connection_by_name(c, name, regex)
                    if res:
                        return res
        else:
            children = cons["childConnections"]
            if regex:
                res = [x for x in children if re.search(name, x["name"])]
            else:
                res = [x for x in children if x["name"] == name]
            if not res:
                if "childConnectionGroups" in cons:
                    for c in cons["childConnectionGroups"]:
                        res = self.__get_connection_by_name(c, name, regex)
                        if res:
                            return res
            else:
                return res[0]

    def get_connection_by_name(self, name, regex=False, datasource=None):
        """
        Get a connection by its name
        """
        cons = self.get_connections(datasource)
        res = self.__get_connection_by_name(cons, name, regex)
        if not res:
            logger.error("Could not find connection named {}".format(name))
        return res

    def add_connection(self, payload, datasource=None):
        """
        Add a new connection

        Example payload:
        {"name":"iaas-067-mgt01 (Admin)",
        "parentIdentifier":"4",
        "protocol":"rdp",
        "attributes":{"max-connections":"","max-connections-per-user":""},
        "activeConnections":0,
        "parameters":{
            "port":"3389",
            "enable-menu-animations":"true",
            "enable-desktop-composition":"true",
            "hostname":"iaas-067-mgt01.vcloud",
            "color-depth":"32",
            "enable-font-smoothing":"true",
            "ignore-cert":"true",
            "enable-drive":"true",
            "enable-full-window-drag":"true",
            "security":"any",
            "password":"XXX",
            "enable-wallpaper":"true",
            "create-drive-path":"true",
            "enable-theming":"true",
            "username":"Administrator",
            "console":"true",
            "disable-audio":"true",
            "domain":"iaas-067-mgt01.vcloud",
            "drive-path":"/var/tmp",
            "disable-auth":"",
            "server-layout":"",
            "width":"",
            "height":"",
            "dpi":"",
            "console-audio":"",
            "enable-printing":"",
            "preconnection-id":"",
            "enable-sftp":"",
            "sftp-port":""}}
        """
        if not datasource:
            datasource = self.primary_datasource
        return self.__auth_request(
            method="POST",
            url="{}/session/data/{}/connections".format(
                self.REST_API, datasource
            ),
            payload=payload,
        )

    def edit_connection(self, connection_id, payload, datasource=None):
        """
        Edit an existing connection

        Example payload:
        {"name":"test",
        "identifier":"7",
        "parentIdentifier":"ROOT",
        "protocol":"rdp",
        "attributes":{"max-connections":"","max-connections-per-user":""},
        "activeConnections":0,
        "parameters":{
            "disable-audio":"true",
            "server-layout":"fr-fr-azerty",
            "domain":"dt",
            "hostname":"127.0.0.1",
            "enable-font-smoothing":"true",
            "security":"rdp",
            "port":"3389",
            "disable-auth":"",
            "ignore-cert":"",
            "console":"",
            "width":"",
            "height":"",
            "dpi":"",
            "color-depth":"",
            "console-audio":"",
            "enable-printing":"",
            "enable-drive":"",
            "create-drive-path":"",
            "enable-wallpaper":"",
            "enable-theming":"",
            "enable-full-window-drag":"",
            "enable-desktop-composition":"",
            "enable-menu-animations":"",
            "preconnection-id":"",
            "enable-sftp":"",
            "sftp-port":""}}
        """
        if not datasource:
            datasource = self.primary_datasource
        return self.__auth_request(
            method="PUT",
            url="{}/session/data/{}/connections/{}".format(
                self.REST_API, datasource, connection_id
            ),
            payload=payload,
            json_response=False,
        )

    def delete_connection(self, connection_id, datasource=None):
        if not datasource:
            datasource = self.primary_datasource
        return self.__auth_request(
            method="DELETE",
            url="{}/session/data/{}/connections/{}".format(
                self.REST_API, datasource, connection_id
            ),
            json_response=False,
        )

    def get_history(self, datasource=None):
        if not datasource:
            datasource = self.primary_datasource

        raise NotImplementedError()

    def __get_connection_group_by_name(self, cons, name, regex=False):
        if (regex and re.search(name, cons["name"])) or (
            not regex and cons["name"] == name
        ):
            return cons
        if "childConnectionGroups" in cons:
            children = cons["childConnectionGroups"]
            if regex:
                res = [x for x in children if re.search(name, x["name"])]
            else:
                res = [x for x in children if x["name"] == name]
            if res:
                return res[0]
            for c in cons["childConnectionGroups"]:
                res = self.__get_connection_group_by_name(c, name, regex)
                if res:
                    return res

    def get_connection_group_by_name(self, name, regex=False, datasource=None):
        """
        Get a connection group by its name
        """
        if not datasource:
            datasource = self.primary_datasource
        cons = self.get_connections(datasource)
        return self.__get_connection_group_by_name(cons, name, regex)

    def get_connection_group(self, connectiongroup_id, datasource=None):
        if not datasource:
            datasource = self.primary_datasource
        return self.__auth_request(
            method="GET",
            url="{}/session/data/{}/connectionGroups/{}".format(
                self.REST_API, datasource, connectiongroup_id
            ),
        )

    def get_auth_json_token(self, payload):
        """
        Submit a signed/encrypted payload (JSON) for the guacamole-auth-json extension
        Return a valid token
        """
        json_token = self.__no_auth_request(
            method="POST",
            url=self.REST_API + "/tokens",
            payload={"data": payload},
        )
        return json_token["authToken"]

    def add_connection_group(self, payload, datasource=None):
        """
        Create a new connection group

        Example payload:
        {"parentIdentifier":"ROOT",
        "name":"iaas-099 (Test)",
        "type":"ORGANIZATIONAL",
        "attributes":{"max-connections":"","max-connections-per-user":""}}
        """
        if not datasource:
            datasource = self.primary_datasource
        return self.__auth_request(
            method="POST",
            url="{}/session/data/{}/connectionGroups".format(
                self.REST_API, datasource
            ),
            payload=payload,
        )

    def edit_connection_group(
        self, connection_group_id, payload, datasource=None
    ):
        """
        Edit an exiting connection group

        Example payload:
        {"parentIdentifier":"ROOT",
        "name":"iaas-099 (Test)",
        "type":"ORGANIZATIONAL",
        "attributes":{"max-connections":"","max-connections-per-user":""}}
        """
        if not datasource:
            datasource = self.primary_datasource
        return self.__auth_request(
            method="PUT",
            url="{}/session/data/{}/connectionGroups/{}".format(
                self.REST_API, datasource, connection_group_id
            ),
            payload=payload,
        )

    def delete_connection_group(self, connection_group_id, datasource=None):
        if not datasource:
            datasource = self.primary_datasource
        return self.__auth_request(
            method="DELETE",
            url="{}/session/data/{}/connectionGroups/{}".format(
                self.REST_API, datasource, connection_group_id
            ),
        )

    def get_users(self, datasource=None):
        if not datasource:
            datasource = self.primary_datasource
        return self.__auth_request(
            method="GET",
            url="{}/session/data/{}/users".format(self.REST_API, datasource),
        )

    def add_user(self, payload, datasource=None):
        """
        Add/enable a user

        Example payload:
        {"username":"test"
         "password":"testpwd",
         "attributes":{
                "disabled":"",
                "expired":"",
                "access-window-start":"",
                "access-window-end":"",
                "valid-from":"",
                "valid-until":"",
                "timezone":null}}
        """
        if not datasource:
            datasource = self.primary_datasource
        return self.__auth_request(
            method="POST",
            url="{}/session/data/{}/users".format(self.REST_API, datasource),
            payload=payload,
        )

    def edit_user(self, username, payload, datasource=None):
        """
        Edit a user

        Example payload:
        {
            "username": "username",
            "attributes": {
                "guac-email-address": null,
                "guac-organizational-role": null,
                "guac-full-name": null,
                "expired": "",
                "timezone": null,
                "access-window-start": "",
                "guac-organization": null,
                "access-window-end": "",
                "disabled": "",
                "valid-until": "",
                "valid-from": ""
            },
            "lastActive": 1588030687251,
            "password": "password"
        }
        """
        if not datasource:
            datasource = self.primary_datasource
        return self.__auth_request(
            method="PUT",
            url="{}/session/data/{}/users/{}".format(
                self.REST_API, datasource, username
            ),
            payload=payload,
            json_response=False,
        )

    def get_user(self, username, datasource=None):
        if not datasource:
            datasource = self.primary_datasource
        return self.__auth_request(
            method="GET",
            url="{}/session/data/{}/users/{}".format(
                self.REST_API, datasource, username
            ),
        )

    def get_user_usergroups(self, username, datasource=None):
        """
        List the usergroups a user belongs to
        """
        if not datasource:
            datasource = self.primary_datasource
        return self.__auth_request(
            method="GET",
            url="{}/session/data/{}/users/{}/userGroups".format(
                self.REST_API, datasource, username
            ),
        )

    def delete_user(self, username, datasource=None):
        if not datasource:
            datasource = self.primary_datasource
        return self.__auth_request(
            method="DELETE",
            url="{}/session/data/{}/users/{}".format(
                self.REST_API, datasource, username
            ),
        )

    def get_permissions(self, username, datasource=None):
        if not datasource:
            datasource = self.primary_datasource
        return self.__auth_request(
            method="GET",
            url="{}/session/data/{}/users/{}/permissions".format(
                self.REST_API, datasource, username
            ),
        )

    def grant_permission(self, username, payload, datasource=None):
        """
        Example payload:
        [{"op":"add","path":"/systemPermissions","value":"ADMINISTER"}]
        """
        if not datasource:
            datasource = self.primary_datasource
        return self.__auth_request(
            method="PATCH",
            url="{}/session/data/{}/users/{}/permissions".format(
                self.REST_API, datasource, username
            ),
            payload=payload,
            json_response=False,
        )

    def get_sharing_profile_parameters(
        self, sharing_profile_id, datasource=None
    ):
        if not datasource:
            datasource = self.primary_datasource
        return self.__auth_request(
            method="GET",
            url="{}/session/data/{}/sharingProfiles/{}/parameters".format(
                self.REST_API, datasource, sharing_profile_id
            ),
        )

    def get_sharing_profile_full(self, sharing_profile_id, datasource=None):
        s = self.get_sharing_profile(sharing_profile_id, datasource)
        s["parameters"] = self.get_sharing_profile_parameters(
            sharing_profile_id, datasource
        )
        return s

    def get_sharing_profile(self, sharing_profile_id, datasource=None):
        if not datasource:
            datasource = self.primary_datasource
        return self.__auth_request(
            method="GET",
            url="{}/session/data/{}/sharingProfiles/{}".format(
                self.REST_API, datasource, sharing_profile_id
            ),
        )

    def add_sharing_profile(self, payload, datasource=None):
        """
        Add/enable a sharing profile

        Example payload:
        {"primaryConnectionIdentifier":"8",
        "name":"share",
        "parameters":{"read-only":""},
        "attributes":{}}'
        """
        if not datasource:
            datasource = self.primary_datasource
        return self.__auth_request(
            method="POST",
            url="{}/session/data/{}/sharingProfiles".format(
                self.REST_API, datasource
            ),
            payload=payload,
        )

    def delete_sharing_profile(self, sharing_profile_id, datasource=None):
        if not datasource:
            datasource = self.primary_datasource
        return self.__auth_request(
            method="DELETE",
            url="{}/session/data/{}/sharingProfiles/{}".format(
                self.REST_API, datasource, sharing_profile_id
            ),
        )

    def get_user_groups(self, datasource=None):
        """
        List all user groups
        """
        if not datasource:
            datasource = self.primary_datasource
        return self.__auth_request(
            method="GET",
            url="{}/session/data/{}/userGroups".format(
                self.REST_API, datasource
            ),
        )

    def add_group(self, payload, datasource=None):
        """
        Add/enable a user group

        Example payload:
        {"identifier":"test"
         "attributes":{
                "disabled":""}}
        """
        if not datasource:
            datasource = self.primary_datasource
        return self.__auth_request(
            method="POST",
            url="{}/session/data/{}/userGroups".format(
                self.REST_API, datasource
            ),
            payload=payload,
        )

    def delete_group(self, usergroup, datasource=None):
        if not datasource:
            datasource = self.primary_datasource
        return self.__auth_request(
            method="DELETE",
            url="{}/session/data/{}/userGroups/{}".format(
                self.REST_API, datasource, usergroup
            ),
        )

    def get_group(self, usergroup, datasource=None):
        """
        Details of User Group
        """
        if not datasource:
            datasource = self.primary_datasource
        return self.__auth_request(
            method="GET",
            url="{}/session/data/{}/userGroups/{}".format(
                self.REST_API, datasource, usergroup
            ),
        )

    def get_group_members(self, usergroup, datasource=None):
        if not datasource:
            datasource = self.primary_datasource
        return self.__auth_request(
            method="GET",
            url="{}/session/data/{}/userGroups/{}/memberUsers".format(
                self.REST_API, datasource, usergroup
            ),
        )

    def edit_group_members(self, usergroup, payload, datasource=None):
        """
        Add Members to User Group
        Example add payload:
        [{"op":"add","path":"/","value":"username"}]
        Example remove payload:
        [{"op":"remove","path":"/","value":"username"}]
        """
        if not datasource:
            datasource = self.primary_datasource
        return self.__auth_request(
            method="PATCH",
            url="{}/session/data/{}/userGroups/{}/memberUsers".format(
                self.REST_API, datasource, usergroup
            ),
            payload=payload,
            json_response=False,
        )

    def grant_group_permission(self, groupname, payload, datasource=None):
        """
        Example payload:
        [{"op":"add","path":"/systemPermissions","value":"ADMINISTER"}]
        """
        if not datasource:
            datasource = self.primary_datasource
        return self.__auth_request(
            method="PATCH",
            url="{}/session/data/{}/userGroups/{}/permissions".format(
                self.REST_API, datasource, groupname
            ),
            payload=payload,
            json_response=False,
        )

    def get_group_permissions(self, groupname, datasource=None):
        if not datasource:
            datasource = self.primary_datasource
        return self.__auth_request(
            method="GET",
            url="{}/session/data/{}/userGroups/{}/permissions".format(
                self.REST_API, datasource, groupname
            ),
        )
