"""
Manage Glassfish/Payara server
.. versionadded:: 2016.11.0

Management of glassfish using its RESTful API
You can setup connection parameters like this

.. code-block:: yaml

    - server:
      - ssl: true
      - url: localhost
      - port: 4848
      - user: admin
      - password: changeit
"""

import salt.utils.json
from salt.exceptions import CommandExecutionError

try:
    import requests

    HAS_LIBS = True
except ImportError:
    HAS_LIBS = False


def __virtual__():
    """
    Only load if glassfish module is available
    """
    if "glassfish.enum_connector_c_pool" in __salt__ and HAS_LIBS:
        return True
    return (False, "glassfish module could not be loaded")


def _json_to_unicode(data):
    """
    Encode json values in unicode to match that of the API
    """
    ret = {}
    for key, value in data.items():
        if not isinstance(value, str):
            if isinstance(value, dict):
                ret[key] = _json_to_unicode(value)
            else:
                ret[key] = str(value).lower()
        else:
            ret[key] = value
    return ret


def _is_updated(old_conf, new_conf):
    """
    Compare the API results to the current statefile data
    """
    changed = {}

    # Dirty json hacking to get parameters in the same format
    new_conf = _json_to_unicode(
        salt.utils.json.loads(salt.utils.json.dumps(new_conf, ensure_ascii=False))
    )
    old_conf = salt.utils.json.loads(
        salt.utils.json.dumps(old_conf, ensure_ascii=False)
    )

    for key, value in old_conf.items():
        oldval = str(value).lower()
        if key in new_conf:
            newval = str(new_conf[key]).lower()
        if oldval == "null" or oldval == "none":
            oldval = ""
        if key in new_conf and newval != oldval:
            changed[key] = {"old": oldval, "new": newval}
    return changed


def _do_element_present(name, elem_type, data, server=None):
    """
    Generic function to create or update an element
    """
    ret = {"changes": {}, "update": False, "create": False, "error": None}
    try:
        elements = __salt__["glassfish.enum_{}".format(elem_type)]()
    except requests.ConnectionError as error:
        if __opts__["test"]:
            ret["changes"] = {"Name": name, "Params": data}
            ret["create"] = True
            return ret
        else:
            ret["error"] = "Can't connect to the server"
            return ret

    if not elements or name not in elements:
        ret["changes"] = {"Name": name, "Params": data}
        ret["create"] = True
        if not __opts__["test"]:
            try:
                __salt__["glassfish.create_{}".format(elem_type)](
                    name, server=server, **data
                )
            except CommandExecutionError as error:
                ret["error"] = error
                return ret
    elif elements and any(data):
        current_data = __salt__["glassfish.get_{}".format(elem_type)](
            name, server=server
        )
        data_diff = _is_updated(current_data, data)
        if data_diff:
            ret["update"] = True
            ret["changes"] = data_diff
            if not __opts__["test"]:
                try:
                    __salt__["glassfish.update_{}".format(elem_type)](
                        name, server=server, **data
                    )
                except CommandExecutionError as error:
                    ret["error"] = error
    return ret


def _do_element_absent(name, elem_type, data, server=None):
    """
    Generic function to delete an element
    """
    ret = {"delete": False, "error": None}
    try:
        elements = __salt__["glassfish.enum_{}".format(elem_type)]()
    except requests.ConnectionError as error:
        if __opts__["test"]:
            ret["create"] = True
            return ret
        else:
            ret["error"] = "Can't connect to the server"
            return ret

    if elements and name in elements:
        ret["delete"] = True
        if not __opts__["test"]:
            try:
                __salt__["glassfish.delete_{}".format(elem_type)](
                    name, server=server, **data
                )
            except CommandExecutionError as error:
                ret["error"] = error
    return ret


def connection_factory_present(
    name,
    restype="connection_factory",
    description="",
    enabled=True,
    min_size=1,
    max_size=250,
    resize_quantity=2,
    idle_timeout=300,
    wait_timeout=60,
    reconnect_on_failure=False,
    transaction_support="",
    connection_validation=False,
    server=None,
):
    """
    Ensures that the Connection Factory is present

    name
        Name of the connection factory

    restype
        Type of the connection factory, can be either ``connection_factory``,
        ``queue_connection_factory` or ``topic_connection_factory``,
        defaults to ``connection_factory``

    description
        Description of the connection factory

    enabled
        Is the connection factory enabled? defaults to ``true``

    min_size
        Minimum and initial number of connections in the pool, defaults to ``1``

    max_size
        Maximum number of connections that can be created in the pool, defaults to ``250``

    resize_quantity
        Number of connections to be removed when idle_timeout expires, defaults to ``2``

    idle_timeout
        Maximum time a connection can remain idle in the pool, in seconds, defaults to ``300``

    wait_timeout
        Maximum time a caller can wait before timeout, in seconds, defaults to ``60``

    reconnect_on_failure
        Close all connections and reconnect on failure (or reconnect only when used), defaults to ``false``
    transaction_support
        Level of transaction support, can be either ``XATransaction``, ``LocalTransaction`` or ``NoTransaction``

    connection_validation
        Connection validation is required, defaults to ``false``
    """
    ret = {"name": name, "result": None, "comment": None, "changes": {}}

    # Manage parameters
    pool_data = {}
    res_data = {}
    pool_name = "{}-Connection-Pool".format(name)
    if restype == "topic_connection_factory":
        pool_data["connectionDefinitionName"] = "javax.jms.TopicConnectionFactory"
    elif restype == "queue_connection_factory":
        pool_data["connectionDefinitionName"] = "javax.jms.QueueConnectionFactory"
    elif restype == "connection_factory":
        pool_data["connectionDefinitionName"] = "javax.jms.ConnectionFactory"
    else:
        ret["result"] = False
        ret["comment"] = "Invalid restype"
        return ret
    pool_data["description"] = description
    res_data["description"] = description
    res_data["enabled"] = enabled
    res_data["poolName"] = pool_name
    pool_data["steadyPoolSize"] = min_size
    pool_data["maxPoolSize"] = max_size
    pool_data["poolResizeQuantity"] = resize_quantity
    pool_data["idleTimeoutInSeconds"] = idle_timeout
    pool_data["maxWaitTimeInMillis"] = wait_timeout * 1000
    pool_data["failAllConnections"] = reconnect_on_failure
    if transaction_support:
        if transaction_support == "xa_transaction":
            pool_data["transactionSupport"] = "XATransaction"
        elif transaction_support == "local_transaction":
            pool_data["transactionSupport"] = "LocalTransaction"
        elif transaction_support == "no_transaction":
            pool_data["transactionSupport"] = "NoTransaction"
        else:
            ret["result"] = False
            ret["comment"] = "Invalid transaction_support"
            return ret
    pool_data["isConnectionValidationRequired"] = connection_validation

    pool_ret = _do_element_present(pool_name, "connector_c_pool", pool_data, server)
    res_ret = _do_element_present(name, "connector_resource", res_data, server)

    if not pool_ret["error"] and not res_ret["error"]:
        if not __opts__["test"]:
            ret["result"] = True

        if pool_ret["create"] or res_ret["create"]:
            ret["changes"]["pool"] = pool_ret["changes"]
            ret["changes"]["resource"] = res_ret["changes"]
            if __opts__["test"]:
                ret["comment"] = "Connection factory set to be created"
            else:
                ret["comment"] = "Connection factory created"
        elif pool_ret["update"] or res_ret["update"]:
            ret["changes"]["pool"] = pool_ret["changes"]
            ret["changes"]["resource"] = res_ret["changes"]
            if __opts__["test"]:
                ret["comment"] = "Connection factory set to be updated"
            else:
                ret["comment"] = "Connection factory updated"
        else:
            ret["result"] = True
            ret["changes"] = {}
            ret["comment"] = "Connection factory is already up-to-date"
    else:
        ret["result"] = False
        ret["comment"] = "ERROR: {} // {}".format(pool_ret["error"], res_ret["error"])

    return ret


def connection_factory_absent(name, both=True, server=None):
    """
    Ensures the transaction factory is absent.

    name
        Name of the connection factory

    both
        Delete both the pool and the resource, defaults to ``true``
    """
    ret = {"name": name, "result": None, "comment": None, "changes": {}}
    pool_name = "{}-Connection-Pool".format(name)
    pool_ret = _do_element_absent(
        pool_name, "connector_c_pool", {"cascade": both}, server
    )

    if not pool_ret["error"]:
        if __opts__["test"] and pool_ret["delete"]:
            ret["comment"] = "Connection Factory set to be deleted"
        elif pool_ret["delete"]:
            ret["result"] = True
            ret["comment"] = "Connection Factory deleted"
        else:
            ret["result"] = True
            ret["comment"] = "Connection Factory doesn't exist"
    else:
        ret["result"] = False
        ret["comment"] = "Error: {}".format(pool_ret["error"])
    return ret


def destination_present(
    name, physical, restype="queue", description="", enabled=True, server=None
):
    """
    Ensures that the JMS Destination Resource (queue or topic) is present

    name
        The JMS Queue/Topic name

    physical
        The Physical destination name

    restype
        The JMS Destination resource type, either ``queue`` or ``topic``, defaults is ``queue``

    description
        A description of the resource

    enabled
        Defaults to ``True``
    """
    ret = {"name": name, "result": None, "comment": None, "changes": {}}

    params = {}
    # Set parameters dict
    if restype == "queue":
        params["resType"] = "javax.jms.Queue"
        params["className"] = "com.sun.messaging.Queue"
    elif restype == "topic":
        params["resType"] = "javax.jms.Topic"
        params["className"] = "com.sun.messaging.Topic"
    else:
        ret["result"] = False
        ret["comment"] = "Invalid restype"
        return ret
    params["properties"] = {"Name": physical}
    params["description"] = description
    params["enabled"] = enabled

    jms_ret = _do_element_present(name, "admin_object_resource", params, server)
    if not jms_ret["error"]:
        if not __opts__["test"]:
            ret["result"] = True
        if jms_ret["create"] and __opts__["test"]:
            ret["comment"] = "JMS Queue set to be created"
        elif jms_ret["create"]:
            ret["changes"] = jms_ret["changes"]
            ret["comment"] = "JMS queue created"
        elif jms_ret["update"] and __opts__["test"]:
            ret["comment"] = "JMS Queue set to be updated"
        elif jms_ret["update"]:
            ret["changes"] = jms_ret["changes"]
            ret["comment"] = "JMS Queue updated"
        else:
            ret["result"] = True
            ret["comment"] = "JMS Queue already up-to-date"
    else:
        ret["result"] = False
        ret["comment"] = "Error from API: {}".format(jms_ret["error"])
    return ret


def destination_absent(name, server=None):
    """
    Ensures that the JMS Destination doesn't exists

    name
        Name of the JMS Destination
    """
    ret = {"name": name, "result": None, "comment": None, "changes": {}}
    jms_ret = _do_element_absent(name, "admin_object_resource", {}, server)
    if not jms_ret["error"]:
        if __opts__["test"] and jms_ret["delete"]:
            ret["comment"] = "JMS Queue set to be deleted"
        elif jms_ret["delete"]:
            ret["result"] = True
            ret["comment"] = "JMS Queue deleted"
        else:
            ret["result"] = True
            ret["comment"] = "JMS Queue doesn't exist"
    else:
        ret["result"] = False
        ret["comment"] = "Error: {}".format(jms_ret["error"])
    return ret


def jdbc_datasource_present(
    name,
    description="",
    enabled=True,
    restype="datasource",
    vendor="mysql",
    sql_url="",
    sql_user="",
    sql_password="",
    min_size=8,
    max_size=32,
    resize_quantity=2,
    idle_timeout=300,
    wait_timeout=60,
    non_transactional=False,
    transaction_isolation="",
    isolation_guaranteed=True,
    server=None,
):
    """
    Ensures that the JDBC Datasource exists

    name
        Name of the datasource

    description
        Description of the datasource

    enabled
        Is the datasource enabled? defaults to ``true``

    restype
        Resource type, can be ``datasource``, ``xa_datasource``,
        ``connection_pool_datasource`` or ``driver``, defaults to ``datasource``

    vendor
        SQL Server type, currently supports ``mysql``,
        ``postgresql`` and ``mssql``, defaults to ``mysql``

    sql_url
        URL of the server in jdbc form

    sql_user
        Username for the server

    sql_password
        Password for that username

    min_size
        Minimum and initial number of connections in the pool, defaults to ``8``

    max_size
        Maximum number of connections that can be created in the pool, defaults to ``32``

    resize_quantity
        Number of connections to be removed when idle_timeout expires, defaults to ``2``

    idle_timeout
        Maximum time a connection can remain idle in the pool, in seconds, defaults to ``300``

    wait_timeout
        Maximum time a caller can wait before timeout, in seconds, defaults to ``60``

    non_transactional
        Return non-transactional connections

    transaction_isolation
        Defaults to the JDBC driver default

    isolation_guaranteed
        All connections use the same isolation level
    """
    ret = {"name": name, "result": None, "comment": None, "changes": {}}

    # Manage parameters
    res_name = "jdbc/{}".format(name)
    pool_data = {}
    pool_data_properties = {}
    res_data = {}
    if restype == "datasource":
        pool_data["resType"] = "javax.sql.DataSource"
    elif restype == "xa_datasource":
        pool_data["resType"] = "javax.sql.XADataSource"
    elif restype == "connection_pool_datasource":
        pool_data["resType"] = "javax.sql.ConnectionPoolDataSource"
    elif restype == "driver":
        pool_data["resType"] = "javax.sql.Driver"

    datasources = {}
    datasources["mysql"] = {
        "driver": "com.mysql.jdbc.Driver",
        "datasource": "com.mysql.jdbc.jdbc2.optional.MysqlDataSource",
        "xa_datasource": "com.mysql.jdbc.jdbc2.optional.MysqlXADataSource",
        "connection_pool_datasource": (
            "com.mysql.jdbc.jdbc2.optional.MysqlConnectionPoolDataSource"
        ),
    }
    datasources["postgresql"] = {
        "driver": "org.postgresql.Driver",
        "datasource": "org.postgresql.ds.PGSimpleDataSource",
        "xa_datasource": "org.postgresql.xa.PGXADataSource",
        "connection_pool_datasource": "org.postgresql.ds.PGConnectionPoolDataSource",
    }
    datasources["mssql"] = {
        "driver": "com.microsoft.sqlserver.jdbc.SQLServerDriver",
        "datasource": "com.microsoft.sqlserver.jdbc.SQLServerDataSource",
        "xa_datasource": "com.microsoft.sqlserver.jdbc.SQLServerXADataSource",
        "connection_pool_datasource": (
            "com.microsoft.sqlserver.jdbc.SQLServerConnectionPoolDataSource"
        ),
    }

    if restype == "driver":
        pool_data["driverClassname"] = datasources[vendor]["driver"]
    else:
        pool_data["datasourceClassname"] = datasources[vendor][restype]

    pool_data_properties["url"] = sql_url
    pool_data_properties["user"] = sql_user
    pool_data_properties["password"] = sql_password
    pool_data["properties"] = pool_data_properties
    pool_data["description"] = description
    res_data["description"] = description
    res_data["poolName"] = name
    res_data["enabled"] = enabled
    pool_data["steadyPoolSize"] = min_size
    pool_data["maxPoolSize"] = max_size
    pool_data["poolResizeQuantity"] = resize_quantity
    pool_data["idleTimeoutInSeconds"] = idle_timeout
    pool_data["maxWaitTimeInMillis"] = wait_timeout * 1000
    pool_data["nonTransactionalConnections"] = non_transactional
    pool_data["transactionIsolationLevel"] = transaction_isolation
    pool_data["isIsolationLevelGuaranteed"] = isolation_guaranteed

    pool_ret = _do_element_present(name, "jdbc_connection_pool", pool_data, server)
    res_ret = _do_element_present(res_name, "jdbc_resource", res_data, server)

    if not pool_ret["error"] and not res_ret["error"]:
        if not __opts__["test"]:
            ret["result"] = True

        if pool_ret["create"] or res_ret["create"]:
            ret["changes"]["pool"] = pool_ret["changes"]
            ret["changes"]["resource"] = res_ret["changes"]
            if __opts__["test"]:
                ret["comment"] = "JDBC Datasource set to be created"
            else:
                ret["comment"] = "JDBC Datasource created"
        elif pool_ret["update"] or res_ret["update"]:
            ret["changes"]["pool"] = pool_ret["changes"]
            ret["changes"]["resource"] = res_ret["changes"]
            if __opts__["test"]:
                ret["comment"] = "JDBC Datasource set to be updated"
            else:
                ret["comment"] = "JDBC Datasource updated"
        else:
            ret["result"] = True
            ret["changes"] = {}
            ret["comment"] = "JDBC Datasource is already up-to-date"
    else:
        ret["result"] = False
        ret["comment"] = "ERROR: {} // {}".format(pool_ret["error"], res_ret["error"])

    return ret


def jdbc_datasource_absent(name, both=True, server=None):
    """
    Ensures the JDBC Datasource doesn't exists

    name
        Name of the datasource
    both
        Delete both the pool and the resource, defaults to ``true``
    """
    ret = {"name": name, "result": None, "comment": None, "changes": {}}
    pool_ret = _do_element_absent(
        name, "jdbc_connection_pool", {"cascade": both}, server
    )

    if not pool_ret["error"]:
        if __opts__["test"] and pool_ret["delete"]:
            ret["comment"] = "JDBC Datasource set to be deleted"
        elif pool_ret["delete"]:
            ret["result"] = True
            ret["comment"] = "JDBC Datasource deleted"
        else:
            ret["result"] = True
            ret["comment"] = "JDBC Datasource doesn't exist"
    else:
        ret["result"] = False
        ret["comment"] = "Error: {}".format(pool_ret["error"])
    return ret


def system_properties_present(server=None, **kwargs):
    """
    Ensures that the system properties are present

    properties
        The system properties
    """
    ret = {"name": "", "result": None, "comment": None, "changes": {}}

    del kwargs["name"]
    try:
        data = __salt__["glassfish.get_system_properties"](server=server)
    except requests.ConnectionError as error:
        if __opts__["test"]:
            ret["changes"] = kwargs
            ret["result"] = None
            return ret
        else:
            ret["error"] = "Can't connect to the server"
            return ret

    ret["changes"] = {"data": data, "kwargs": kwargs}
    if not data == kwargs:
        data.update(kwargs)
        if not __opts__["test"]:
            try:
                __salt__["glassfish.update_system_properties"](data, server=server)
                ret["changes"] = kwargs
                ret["result"] = True
                ret["comment"] = "System properties updated"
            except CommandExecutionError as error:
                ret["comment"] = error
                ret["result"] = False
        else:
            ret["result"] = None
            ret["changes"] = kwargs
            ret["coment"] = "System properties would have been updated"
    else:
        ret["changes"] = {}
        ret["result"] = True
        ret["comment"] = "System properties are already up-to-date"
    return ret


def system_properties_absent(name, server=None):
    """
    Ensures that the system property doesn't exists

    name
        Name of the system property
    """
    ret = {"name": "", "result": None, "comment": None, "changes": {}}

    try:
        data = __salt__["glassfish.get_system_properties"](server=server)
    except requests.ConnectionError as error:
        if __opts__["test"]:
            ret["changes"] = {"Name": name}
            ret["result"] = None
            return ret
        else:
            ret["error"] = "Can't connect to the server"
            return ret

    if name in data:
        if not __opts__["test"]:
            try:
                __salt__["glassfish.delete_system_properties"](name, server=server)
                ret["result"] = True
                ret["comment"] = "System properties deleted"
            except CommandExecutionError as error:
                ret["comment"] = error
                ret["result"] = False
        else:
            ret["result"] = None
            ret["comment"] = "System properties would have been deleted"
        ret["changes"] = {"Name": name}
    else:
        ret["result"] = True
        ret["comment"] = "System properties are already absent"
    return ret
