import sys
import os
import logging
import traceback
import time
import datetime
import subprocess
import socket
import json
import uuid
import hashlib
import copy
import _thread
import urllib.request
import urllib.parse
import urllib.error
import base64
import io
import gzip
import binascii
import re
import requests

try:
    requests.packages.urllib3.disable_warnings()
except BaseException:
    print(traceback.format_exc(), file=sys.stderr)  # Python 3

logging.basicConfig(
    format="(%(thread)d@%(asctime)-15s) %(message)s",
    level=logging.INFO,
    datefmt='%Y-%m-%d %H:%M:%S')

ACCSYN_CLOUD_DOMAIN = "accsyn.com"
ACCSYN_CLOUD_REGISTRY_HOSTNAME = "registry.%s" % ACCSYN_CLOUD_DOMAIN
ACCSYN_PORT = 443
DEFAULT_EVENT_PAYLOAD_COMPRESS_SIZE_TRESHOLD = 100 * \
    1024  # Compress event data payloads above 100k

CLEARANCE_CLOUDADMIN = "cloudadmin"
CLEARANCE_ADMIN = "admin"
CLEARANCE_EMPLOYEE = "employee"
CLEARANCE_CLIENT = "client"
CLEARANCE_NONE = "none"


class JSONEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(
                obj,
                datetime.date) or isinstance(
                obj,
                datetime.datetime):
            return obj.strftime("%Y-%m-%dT%H:%M:%S")
            # return "{USD::date}%s"%obj.strftime("%Y%m%d %H:%M:%S") DEPRECATED
        else:
            try:
                from bson.objectid import ObjectId
                if isinstance(obj, ObjectId):
                    return str(obj)
            except BaseException:
                pass
        return super(JSONEncoder, self).default(obj)


class JSONDecoder(json.JSONDecoder):
    def decode(self, json_string):
        json_data = json.loads(json_string)

        def recursive_decode(d):
            if isinstance(d, dict):
                for key in d.keys():
                    if isinstance(d[key], dict):
                        d[key] = recursive_decode(d[key])
                    elif isinstance(d[key], list):
                        newlist = []
                        for i in d[key]:
                            newlist.append(recursive_decode(i))
                        d[key] = newlist
                    elif isinstance(d[key], str):
                        if key == "_id":
                            from bson.objectid import ObjectId
                            d[key] = ObjectId(d[key])
                        if re.match(
                            "^[0-9]{2,4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}$", str(
                                Session.safely_printable(
                                    d[key]))):
                            if len(d[key].split("-")[0]) == 4:
                                d[key] = datetime.datetime.strptime(
                                    d[key], "%Y-%m-%dT%H:%M:%S")
                            else:
                                d[key] = datetime.datetime.strptime(
                                    d[key], "%y-%m-%dT%H:%M:%S")
                        # With millis
                        elif re.match("^[0-9]{2,4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{3}$", str(Session.safely_printable(d[key]))):
                            if len(d[key].split("-")[0]) == 4:
                                d[key] = datetime.datetime.strptime(
                                    d[key], "%Y-%m-%dT%H:%M:%S.%f")
                            else:
                                d[key] = datetime.datetime.strptime(
                                    d[key], "%y-%m-%dT%H:%M:%S.%f")
                        # DEPRECATED
                        elif str(Session.safely_printable(d[key])).find("{USD::date}") > -1:
                            s = d[key][d[key].find("}") + 1:]
                            d[key] = datetime.datetime.strptime(
                                s, "%Y%m%d %H:%M:%S" if s.find("+") == -1 else "%Y%m%d+%H:%M:%S")
            return d
        return recursive_decode(json_data)


class Session(object):

    __version__ = "1.3.4-6"

    def __init__(
            self,
            domain=None,
            username=None,
            api_key=None,
            pwd=None,
            session_key=None,
            hostname=None,
            port=None,
            proxy=None,
            verbose=False,
            pretty_json=False,
            dev=False,
            path_logfile=None):
        ''' Setup; store credentials, authenticate, get a session key '''
        # Generate a session ID
        self._pwd = None
        self._session_id = str(uuid.uuid4())
        self._session_key = None
        self._session_key_provided = None
        self._verbose = verbose
        self._pretty_json = pretty_json
        self._proxy = proxy
        self._dev = dev
        Session._p_logfile = path_logfile
        self._clearance = CLEARANCE_NONE
        self.verbose(
            "Creating Accsyn Python API session (v%s)" %
            Session.__version__)
        for key in os.environ:
            if key.startswith("FILMHUB_"):
                Session.warning(
                    "Found old FilmHUB product environment variable '%s', please migrate!" %
                    key)
        if domain is None:
            if not (
                    'ACCSYN_DOMAIN' in os.environ or 'ACCSYN_ORG' in os.environ or 'FILMHUB_DOMAIN' in os.environ or 'FILMHUB_ORG' in os.environ):
                raise AccsynException(
                    "Please supply your Accsyn domain/organization or set ACCSYN_DOMAIN environment!")
        self._domain = domain or (
            os.environ['ACCSYN_DOMAIN'] if 'ACCSYN_DOMAIN' in os.environ else os.environ.get(
                'ACCSYN_ORG', os.environ.get(
                    'FILMHUB_DOMAIN', os.environ.get('FILMHUB_ORG'))))
        if username is None:
            if not (
                    'ACCSYN_API_USER' in os.environ or 'FILMHUB_API_USER' in os.environ):
                raise AccsynException(
                    "Please supply your Accsyn user name (E-mail) or set ACCSYN_API_USER environment!")
        self._username = username or os.environ.get(
            'ACCSYN_API_USER') or os.environ['FILMHUB_API_USER']
        if api_key:
            self._api_key = api_key
        else:
            self._api_key = os.environ.get(
                'ACCSYN_API_KEY') or os.environ.get('FILMHUB_API_KEY')
        if 0 < len(session_key or ""):
            # User has a session key for us to use, validate at login, store it
            # temporarily
            self._session_key_provided = session_key
        elif len(self._api_key or "") == 0:
            if 0 < len(pwd or ""):
                # Store it temporarily
                self._pwd = pwd
            else:
                raise AccsynException(
                    "Please supply your Accsyn API KEY or set ACCSYN_API_KEY environment!")
        self._hostname = hostname
        self._port = port or ACCSYN_PORT
        if self._hostname is None:
            if self._dev:
                self._hostname = "172.16.178.161"
            else:
                # Get domain
                result = self.rest("PUT",
                                   ACCSYN_CLOUD_REGISTRY_HOSTNAME,
                                   "registry/organization/domain",
                                   {'organization': self._domain})
                # Store hostname
                assert ('domain' in result), ("No domain were provided for us!")
                self._hostname = "%s.%s" % (
                    result['domain'], ACCSYN_CLOUD_DOMAIN)
        self._last_message = None
        self.login()

    @staticmethod
    def get_hostname():
        return socket.gethostname()

    @staticmethod
    def info(s, standout=False):
        if Session._p_logfile:
            with open(Session._p_logfile, "a") as f:
                try:
                    if standout:
                        f.write("-" * 80 + "\n")
                    f.write(s + "\n")
                    if standout:
                        f.write("-" * 80 + "\n")
                finally:
                    f.flush()
        else:
            if standout:
                logging.info("-" * 80)
            logging.info(s)
            if standout:
                logging.info("-" * 80)

    @staticmethod
    def warning(s, standout=True):
        if Session._p_logfile:
            with open(Session._p_logfile, "a") as f:
                if standout:
                    f.write("[WARNING]" + "-" * 80 + "\n")
                f.write("[WARNING]" + s + "\n")
                if standout:
                    f.write("[WARNING]" + "-" * 80 + "\n")
        else:
            if standout:
                logging.warning("-" * 80)
            logging.warning(s)
            if standout:
                logging.warning("-" * 80)

    def verbose(self, s):
        if self._verbose:
            Session.info("[ACCSYN_API] %s" % (s))

    @staticmethod
    def safe_dumps(d, indent=None):
        return json.dumps(d, cls=JSONEncoder, indent=indent)

    @staticmethod
    def safely_printable(s):
        return ((s or "").encode()).decode("ascii", "ignore")











    @staticmethod
    def json_serial(obj):
        """JSON serializer for objects not serializable by default json code"""
        if isinstance(
                obj,
                datetime.datetime) or isinstance(
                obj,
                datetime.date):
            return obj.isoformat()
        raise TypeError("Type %s not serializable" % type(obj))

    @staticmethod
    def str(d, indent=4):
        ''' Return a string representation of a dict '''
        return json.dumps(d, default=Session.json_serial,
                          indent=indent) if d is not None else ""

    def get_last_message(self):
        return self._last_message

    @staticmethod
    def base64_encode(s):
        return (base64.b64encode((s).encode())).decode("utf-8", "ignore")

    def login(self, revive_session_key=None):
        # TODO: Load session key from safe disk storage/key chain?
        assert (self._session_key is None), ("Already logged in!")
        d = {
            'session': self._session_id,
        }
        if revive_session_key:
            d['session_key_reuse'] = revive_session_key
        if self._api_key:
            headers = {
                "Authorization": "ASCredentials %s" %
                Session.base64_encode(
                    '{"domain":"%s","username":"%s","api_key":"%s"}' %
                    (self._domain, self._username, self._api_key))}
        elif self._pwd:
            headers = {
                "Authorization": "ASCredentials %s" %
                Session.base64_encode(
                    '{"domain":"%s","username":"%s","pwd":"%s"}' %
                    (self._domain,
                     self._username,
                     Session.base64_encode(
                         self._pwd)))}
            self._pwd = None  # Forget this now
        elif self._session_key_provided:
            headers = {
                "Authorization": "ASSession %s" %
                Session.base64_encode(
                    '{"domain":"%s","username":"%s","session_key":"%s"}' %
                    (self._domain, self._username, self._session_key_provided))}
            self._session_key_provided = None  # Forget this now
        else:
            raise Exception("No means of authentication available!")
        result = self.rest(
            "PUT",
            self._hostname,
            "/user/login/auth",
            d,
            headers=headers,
            port=self._port)
        # Store session key
        assert ('session_key' in result), ("No session key were provided for us!")
        self._session_key = result['session_key']
        self._clearance = result['clearance'] or CLEARANCE_NONE
        self._uid = result['id']
        return True

    @staticmethod
    def obscure_dict_string(s):
        if s is None:
            return s
        for key in ['pwd', '_key', 'token']:
            last_pos = 0
            while True:
                new_pos = s.find(key, last_pos)
                if new_pos == -1:
                    break
                else:
                    idx_start = s.find('"', new_pos + len(key) + 1)
                    idx_end = s.find('"', idx_start + 1)
                    s = s[:idx_start + 1] + "*" + s[idx_end:]
                    last_pos = idx_start
        return s

    # REST get

    def event(
            self,
            method,
            uri,
            data,
            query=None,
            entityid=None,
            timeout=None,
            ssl=True,
            quiet=False):
        ''' Generate an Event and send using REST to Accsyn cloud '''
        assert (self._session_key), ("Please login before attempting to post event!")
        event = {
            'audience': "api",
            'domain': self._domain,
            'eid': str(uuid.uuid4()),
            'session': self._session_id,
            'uri': uri,
            'ident': self._username,
            'created': datetime.datetime.now(),
            'hostname': Session.get_hostname()
        }
        did_compress_payload = False
        if data is not None and 0 < len(data):
            # Check if should compress payload
            def recursive_estimate_dict_size(o):
                result = 0
                if o is not None:
                    if isinstance(o, dict):
                        d = o
                        for key in d:
                            result += len(key) + \
                                recursive_estimate_dict_size(d[key])
                    elif isinstance(o, list):
                        l = o
                        for _o in l:
                            result += recursive_estimate_dict_size(_o)
                    elif isinstance(o, str) or isinstance(o, str):
                        result += len(o)
                    else:
                        result += 10
                return result

            size = recursive_estimate_dict_size(data)
            if (DEFAULT_EVENT_PAYLOAD_COMPRESS_SIZE_TRESHOLD < size):
                out = io.StringIO()
                with gzip.GzipFile(fileobj=out, mode="w") as f:
                    f.write(Session.safe_dumps(data))
                b = out.getvalue()
                event['gz_data'] = binascii.b2a_base64(b)
                self.verbose("Compressed event payload %d>%d(%s%%)" % (
                    size, len(event['gz_data']), (100 * len(event['gz_data']) / size)))
                did_compress_payload = True
        if not did_compress_payload:
            event['data'] = data

        if query:
            event['query'] = query
        if entityid:
            event['id'] = entityid
        retval = self.rest(
            method,
            hostname=self._hostname,
            uri="/event",
            data=event,
            timeout=timeout,
            ssl=ssl,
            port=self._port,
            quiet=quiet)
        return retval

    def rest(
            self,
            method,
            hostname,
            uri,
            data,
            timeout=None,
            ssl=True,
            port=None,
            quiet=False,
            headers=None):
        ''' Talk REST with Accsyn Cloud '''
        if port is None:
            port = self._port or ACCSYN_PORT
        if hostname is None:
            hostname = "%s.%s" % (self._domain, ACCSYN_CLOUD_DOMAIN)
        # Proxy set?
        proxy_type = None
        proxy_hostname = None
        proxy_port = -1
        proxy_setting = self._proxy or os.environ.get('ACCSYN_PROXY')
        if 0 < len(proxy_setting or ""):
            if 0 < proxy_setting.find(":"):
                parts = proxy_setting.split(":")
                if len(parts) == 3:
                    proxy_type = parts[0]
                    proxy_hostname = parts[1]
                    proxy_port = int(parts[2])
                elif len(parts) == 2:
                    proxy_type = "accsyn"
                    proxy_hostname = parts[0]
                    proxy_port = int(parts[1])
            else:
                proxy_type = "accsyn"
                proxy_hostname = proxy
                proxy_port = 80
            ssl = False
        if self._dev and (uri or "").find("registry") != 0:
            ssl = False
            port = 80
        if proxy_type == "accsyn":
            if proxy_port == -1:
                proxy_port = 80
            self.verbose(
                "Using Accsyn proxy @ %s:%s" %
                (proxy_hostname, proxy_port))
            hostname = proxy_hostname
            port = proxy_port
        elif proxy_type in ["socks", "socks5"]:
            try:
                self.verbose(
                    "Using SOCKS5 proxy @ %s:%s" %
                    (proxy_hostname, proxy_port))
                import socks
                socks.setdefaultproxy(
                    socks.PROXY_TYPE_SOCKS5, proxy_hostname, proxy_port)
                socket.socket = socks.socksocket
            except ImportError as ie:
                Session.warning(
                    "Python lacks SOCKS support, please install 'pysocks' and try again...")
                raise ie
        elif proxy_type is not None:
            raise AccsynException("Unknown proxy type '%s'!" % proxy_type)
        url = "http%s://%s:%d/api/v1.0%s" % ("s" if ssl else "",
                                             hostname,
                                             port,
                                             ("/" if not uri.startswith("/") else "") + uri)
        if timeout is None:
            timeout = 999999
        if data is None:
            data = {}
        #data = json.dumps({'name':'test', 'description':'some test repo'})
        #r = requests.post(github_url, data, auth=('user', '*****'))
        # Wait 10s to reach machine, 2min for it to send back data
        CONNECT_TO, READ_TO = (10, 2 * 60)
        r = None
        #data = Accsyn.prepare_rest_serialize(data)
        initial_timeout = timeout
        if headers is None:
            if uri.find("registry/") != 0:
                assert (self._session_key is not None), (
                    "Need to be authenticated when communicating with Accsyn!")
                headers = {
                    'Authorization': "ASSession %s" % (Session.base64_encode(
                        '{"domain":"%s","username":"%s","session_key":"%s"}' % (self._domain,
                                                                                self._username,
                                                                                self._session_key)))}
            else:
                headers = {}
        headers['ASDevice'] = "PythonAPI v%s @ %s %s(%s)" % (
            Session.__version__, sys.platform, Session.get_hostname(), os.name)
        for iteration in range(0, 2):
            t_start = int(round(time.time() * 1000))
            try:
                self.verbose(
                    "REST %s %s, data: %s" %
                    (method, url, data if not self._pretty_json else Session.str(data)))
                if method.lower() == "get":
                    r = requests.get(
                        url, params=urllib.parse.quote(
                            Session.safe_dumps(data)), timeout=(
                            CONNECT_TO, READ_TO), verify=False, headers=headers)
                elif method.lower() == "put":
                    r = requests.put(
                        url, Session.safe_dumps(data), timeout=(
                            CONNECT_TO, READ_TO), verify=False, headers=headers)
                elif method.lower() == "post":
                    r = requests.post(
                        url, Session.safe_dumps(data), timeout=(
                            CONNECT_TO, READ_TO), verify=False, headers=headers)
                elif method.lower() == "delete":
                    r = requests.delete(
                        url, params=urllib.parse.quote(
                            Session.safe_dumps(data)), timeout=(
                            CONNECT_TO, READ_TO), verify=False, headers=headers)
                t_end = int(round(time.time() * 1000))
                # break
            except BaseException:
                sleep_time = 2
                t_end = int(round(time.time() * 1000))
                timeout -= int((t_end - t_start) / 1000)
                timeout -= sleep_time
                if timeout <= 0 or True:
                    raise AccsynException(
                        "Could not reach %s:%d! Make sure cloud server(%s) can be reached from you location and no firewall is blocking outgoing traffic at port %s. Details: %s" %
                        (hostname, port, port, hostname, traceback.format_exc() if not quiet else "(quiet)"))

                Session.warning(
                    "Could not reach %s:%d! Waited %ds/%s, will try again in %ds... Details: %s" %
                    (hostname,
                     port,
                     initial_timeout -
                     timeout,
                     "%ss" %
                     initial_timeout if initial_timeout < 99999999 else "INF",
                     sleep_time,
                     traceback.format_exc()))
                time.sleep(sleep_time)

            try:
                #retval = Accsyn.prepare_rest_deserialize(r.json())
                retval = json.loads(r.text, cls=JSONDecoder)
                if not quiet:
                    self.verbose(
                        "%s/%s REST %s result: %s (~%sms)" %
                        (hostname, uri, method, Session.obscure_dict_string(
                            Session.safely_printable(
                                str(retval) if not self._pretty_json else Session.str(retval)).replace(
                                "'", '"')), t_start - t_end + 1))
                do_retry = False
                if not retval.get('message') is None:
                    # Some thing went wrong
                    if retval.get('session_expired') is True:
                        if self._api_key is not None:
                            # We should be able to get a new session and retry
                            revive_session_key = self._session_key
                            self._session_key = None
                            self.login(revive_session_key=revive_session_key)
                            self.info(
                                "Authenticated using API KEY and reused expired session...")
                            do_retry = True
                    if not do_retry:
                        self._last_message = retval['message']
                if not do_retry:
                    break
            except BaseException:
                print(traceback.format_exc(), file=sys.stderr)  # Python 3
                message = "The %s:%d/%s REST %s %s operation failed! Details: %s %s" % (hostname, port, uri, method, Session.obscure_dict_string(
                    Session.safely_printable(str(data)).replace("'", '"')), r.text, traceback.format_exc())
                Session.warning(message)
                raise AccsynException(message)
        if 'exception' in retval:
            message = "%s caused an exception! Please contact %s admin for more further support." % (
                uri, self._domain)
            Session.warning(message)
            if self._clearance in [CLEARANCE_ADMIN, CLEARANCE_CLOUDADMIN]:
                Session.warning(retval['exception'])
            raise AccsynException(message)
        elif 'message' in retval:
            message_effective = retval.get('message_hr') or retval['message']
            Session.warning(message_effective)
            raise AccsynException(message_effective)
        return retval

    def decode_query(self, query):
        # Scenarios:
        #   entities
        #   attributes WHERE entitytype="job"
        #   Job WHERE code="my_transfer"
        #   Job WHERE (dest="lars@edit.com" OR ..)

        # First replace tabs with spaces, remove double spaces
        s = ""
        is_escaped = False
        is_at_whitespace = False
        query = (query or "").replace("\t", " ").replace("\n", "").strip()
        parts = []
        idx_part_start = 0
        paranthesis_depth = 0
        for idx in range(0, len(query)):
            do_append = True
            if query[idx] == " ":
                if not is_escaped and paranthesis_depth == 0:
                    if is_at_whitespace:
                        # Ignore this
                        do_append = False
                    else:
                        is_at_whitespace = True
                        if idx_part_start < idx:
                            # Add this part
                            parts.append(query[idx_part_start:idx])
                            idx_part_start = idx + 1
            else:
                is_at_whitespace = False
                if query[idx] == '"':
                    is_escaped = not is_escaped
                elif query[idx] == "(":
                    if not is_escaped:
                        paranthesis_depth += 1
                elif query[idx] == ")":
                    if not is_escaped:
                        paranthesis_depth -= 1
            if do_append:
                s += query[idx]
        if idx_part_start < len(query):
            parts.append(query[idx_part_start:])
        self.verbose("Query: '%s', parts: '%s'" % (query, parts))
        # ['Job', 'WHERE', '(dest="lars@edit.com" OR id=...)]
        assert (len(parts) == 1 or 3 <= len(
            parts)), ("Query has invalid syntax; statements can either be single ('<entity>') or with a WHERE statement ('<(sub)entity> WHERE {<entity>.}id=..{ AND ..}')")
        if len(parts) == 1:
            if parts[0].lower() == "user":
                return {
                    'entitytype': parts[0].lower(),
                    'expression': "code=%s" % self._username}
            else:
                return {'entitytype': parts[0].lower()}
        else:
            assert (parts[1].strip().lower() == "where"), (
                "Invalid query '%s', should be on the form '<entitytype> where <expression>'.")
            # Decode expression
            return {'entitytype': parts[0].lower(),
                    'expression': (" ".join(parts[2:])).lower()}

    @staticmethod
    def get_base_uri(entitytype):
        uri_base = entitytype
        # Send query to server, first determine uri
        # if entitytype == "share":
        #	uri_base = "organization/share"
        if entitytype == "site":
            uri_base = "organization/site"
        elif entitytype == "queue":
            uri_base = "job"
        elif entitytype == "task":
            uri_base = "job/task"
        return uri_base

    # Create

    def create(
            self,
            entitytype,
            data,
            entitytype_id=None,
            allow_duplicates=True):
        ''' Create an entity '''
        assert (0 < len((entitytype or "").strip())
                ), ("You must provide the entity type!")
        entitytype = entitytype.lower().strip()
        if isinstance(data, str):
            assert (0 < len((data or "").strip())
                    ), ("You must provide the data to create!")
            # Is it JSON as a string or JSON in a file pointed to?
            try:
                data = json.loads(data)
            except BaseException:
                # Not JSON string, maybe a path?
                if os.path.exists(data):
                    data = json.load(open(data, "r"))
                else:
                    raise AccsynException(
                        "Cannot build JSON payload data, not a valid JSON string or path to a JSON file!")
        else:
            if isinstance(data, list):
                data = {'tasks': data}
            assert (data is not None and 0 < len(data)
                    ), ("Empty create data submitted!")
        if entitytype == "task" and 'tasks' not in data:
            data = {'tasks': data}
        if entitytype in ["job", "task"]:
            data['allow_duplicates'] = allow_duplicates
        d = self.event(
            "POST",
            "%s/create" %
            Session.get_base_uri(entitytype),
            data,
            query=entitytype_id)
        if d:
            if 'result' in d:
                return d['result'][0]
            else:
                return d

    # Query

    def find(
            self,
            query,
            attributes=None,
            finished=None,
            limit=None,
            skip=None,
            create=False,
            update=False):
        ''' Return a list of something '''
        assert (
            0 < len(
                query or "") and
                isinstance(
                    query, str)), ("Invalid query type supplied, must be of string type!")


        retval = None
        d = self.decode_query(query)
        data = {}
        if d['entitytype'] == "entitytypes":
            # Ask cloud server, the Python API is rarely updated and should not
            # need to know
            d = self.event("GET", "entitytypes", {})
            if d:
                retval = d['result']
        elif d['entitytype'] == "attributes":
            assert (d.get('expression')), (
                "Please query which entity to obtain attributes for (i.e. 'attributes WHERE entitytype=job')")
            # Look at expression
            parts = d['expression'].split("=")
            assert (len(parts) == 2 and parts[0].strip() == "entitytype"), (
                "Please query attributes by expressions on the form 'attributes WHERE entitytype=job'")
            entitytype = parts[1].strip().replace("'", "").replace('"', "")
            d = self.event(
                "GET", "attributes", {
                    'entitytype': entitytype, 'create': create, 'update': update})
            if d:
                retval = d['result']
        else:
            # Send query to server, first determine uri
            uri_base = Session.get_base_uri(d['entitytype'])
            if d['entitytype'] == "queue":
                data = {'type': 2}
            elif d['entitytype'] == "job":
                data = {'type': 1}
            if finished is not None:
                data['finished'] = finished
            if limit:
                data['limit'] = limit
            if skip:
                data['skip'] = skip
            if attributes:
                data['attributes'] = attributes
            d = self.event(
                "GET",
                "%s/find" %
                uri_base,
                data,
                query=d.get('expression'))
            if d:
                retval = d['result']
        return retval

    def find_one(self, query, attributes=None):
        ''' Return a list of something '''
        assert (
            0 < len(
                query or "") and (
                isinstance(
                    query, str) or isinstance(
                    query, str))), ("Invalid query type supplied, must be of string type!")
        result = self.find(query, attributes=attributes)
        if result and 0 < len(result):
            retval = result[0]
            return retval
        return None

    def report(self, query):
        ''' Return a list of something '''
        d = self.decode_query(query)
        # Send query to server, first determine uri
        uri_base = Session.get_base_uri(d['entitytype'])
        data = {}
        d = self.event(
            "GET",
            "%s/report" %
            uri_base,
            data,
            query=d.get('expression'))
        return d['report']

    # Update an entity

    def update_one(self, entitytype, entityid, data):
        ''' Update an entity '''
        assert (0 < len(entitytype or "") and isinstance(entitytype, str))\
            , ("Invalid entity type supplied, must be of string type!")
        entitytype = entitytype.lower().strip()
        assert (0 < len(entityid or "") and (isinstance(entityid, str)))\
            , ("Invalid entity ID supplied, must be of string type!")
        assert (0 < len(data or {}) and isinstance(data, dict))\
            , ("Invalid data supplied, must be dict and have content!")
        response = self.event(
            "PUT",
            "%s/edit" %
            Session.get_base_uri(entitytype),
            data,
            entityid=entityid)
        if response:
            return response['result'][0]

    def update_many(self, entitytype, data, entityid=None):
        ''' Update many entities '''
        assert (0 < len(entitytype or "") and isinstance(entitytype, str))\
            , ("Invalid entity type supplied, must be of string type!")
        entitytype = entitytype.lower().strip()
        assert (entitytype == "task")\
            , ("Only multiple 'task' entities can be updated!")
        if entitytype.lower() == "task":
            assert (0 < len(entityid or "") and (isinstance(entityid, str)))\
            , ("Invalid entity ID supplied, must be of string type!")
        assert (0 < len(data or []) and isinstance(data, list))\
            , ("Invalid data supplied, must be a list!")
        response = self.event(
            "PUT",
            "%s/edit" %
            Session.get_base_uri(entitytype),
            data,
            entityid=entityid)
        if response:
            return response['result']

    # Delete an entity

    def delete_one(self, entitytype, entityid):
        ''' Update an entity '''
        assert (0 < len(entitytype or "") and isinstance(entitytype, str))\
            , ("Invalid entity type supplied, must be of string type!")
        entitytype = entitytype.lower().strip()
        assert (0 < len(entityid or "") and (isinstance(entityid, str)))\
            ,("Invalid entity ID supplied, must be of string type!")
        response = self.event(
            "DELETE",
            "%s/delete" %
            Session.get_base_uri(entitytype),
            {},
            entityid=entityid)
        if response:
            return response['result']

    # File operations
    def ls(
            self,
            p,
            recursive=False,
            maxdepth=None,
            getsize=False,
            files_only=False,
            directories_only=False):
        assert (
            0 < len(
                p or "") and (
                isinstance(
                    p, str) or isinstance(
                    p, str) or isinstance(
                        p, dict) or isinstance(
                            p, list))), ("No path supplied, or not a string/list!")
        data = {
            'op': "ls",
            'path': p,
            'download': True,
            'recursive': recursive,
            'getsize': getsize
        }
        if maxdepth:
            data['maxdepth'] = maxdepth
        if directories_only:
            data['directories_only'] = directories_only
        if files_only:
            data['files_only'] = files_only
        response = self.event("GET", "organization/file", data)
        if response:
            return response['result']

    def getsize(self, p):
        assert (
            0 < len(
                p or "") and (
                isinstance(
                    p, str) or isinstance(
                    p, str) or isinstance(
                        p, dict) or isinstance(
                            p, list))), ("No path supplied, or not a string/list!")
        data = {
            'op': "getsize",
            'path': p,
        }
        response = self.event("GET", "organization/file", data)
        if response:
            return response['result']

    def exists(self, p):
        assert (
            0 < len(
                p or "") and (
                isinstance(
                    p, str) or isinstance(
                    p, str) or isinstance(
                        p, dict) or isinstance(
                            p, list))), ("No path supplied, or not a string/list!")
        data = {
            'op': "exists",
            'path': p,
        }
        response = self.event("GET", "organization/file", data)
        if response:
            return response['result']

    # Pre publish
    def prepublish(self, data):
        if data is None or not isinstance(data, list):
            raise AccsynException("None or empty data supplied!")
        # Check entries, calculate size

        def recursive_get_size(files):
            result = 0
            for d in files:
                if 'size' not in d:
                    d['size'] = 0 if not d.get('is_dir') is True or d.get(
                        'files') is None else recursive_get_size(d['files'])
                result += d['size']
            return result
        event_data = {
            'files': data,
            'size': recursive_get_size(data)
        }
        response = self.event(
            "PUT",
            "organization/publish/preprocess",
            event_data)
        return response['result']

    # Misc
    # def get_api_key(self):
    #	''' DEPRECATED as of 1.3-4 '''
    #	return self.event("GET", "user/api_key", {})['api_key']

    def get_session_key(self):
        ''' Return the session key. '''
        return self._session_key

    def gui_is_running(self):
        result = self.event(
            "GET",
            "client/find",
            {},
            query="user={0} AND code={1} AND type={2}".format(
                self._uid,
                Session.get_hostname(),
                0))['result']
        retval = None
        if 0 < len(result):
            for c in result:
                retval = c['status'] in ["online", "disabled"]
                if retval is True:
                    break
        return retval

    def server_is_running(self):
        result = self.event(
            "GET",
            "client/find",
            {},
            query="user={0} AND code={1} AND type!={2}".format(
                self._uid,
                Session.get_hostname(),
                0))['result']
        retval = None
        if 0 < len(result):
            for c in result:
                retval = c['status'] in ["online", "disabled"]
                if retval is True:
                    break
        return retval

    # Help
    def help(self):
        print("Please have a look at the Python API reference: https://support.accsyn.com/python-api")


class AccsynException(Exception):
    def __init__(self, message):
        super(AccsynException, self).__init__(message)
