"""
Module for working with the Glassfish/Payara 4.x management API
.. versionadded:: 2016.11.0
:depends: requests
"""

import urllib.parse

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

try:
    import requests

    HAS_LIBS = True
except ImportError:
    HAS_LIBS = False

__virtualname__ = "glassfish"

# Default server
DEFAULT_SERVER = {
    "ssl": False,
    "url": "localhost",
    "port": 4848,
    "user": None,
    "password": None,
}


def __virtual__():
    """
    Only load if requests is installed
    """
    if HAS_LIBS:
        return __virtualname__
    else:
        return (
            False,
            'The "{}" module could not be loaded: "requests" is not installed.'.format(
                __virtualname__
            ),
        )


def _get_headers():
    """
    Return fixed dict with headers (JSON data + mandatory "Requested by" header)
    """
    return {
        "Accept": "application/json",
        "Content-Type": "application/json",
        "X-Requested-By": "GlassFish REST HTML interface",
    }


def _get_auth(username, password):
    """
    Returns the HTTP auth header
    """
    if username and password:
        return requests.auth.HTTPBasicAuth(username, password)
    else:
        return None


def _get_url(ssl, url, port, path):
    """
    Returns the URL of the endpoint
    """
    if ssl:
        return "https://{}:{}/management/domain/{}".format(url, port, path)
    else:
        return "http://{}:{}/management/domain/{}".format(url, port, path)


def _get_server(server):
    """
    Returns the server information if provided, or the defaults
    """
    return server if server else DEFAULT_SERVER


def _clean_data(data):
    """
    Removes SaltStack params from **kwargs
    """
    for key in list(data):
        if key.startswith("__pub"):
            del data[key]
    return data


def _api_response(response):
    """
    Check response status code + success_code returned by glassfish
    """
    if response.status_code == 404:
        __context__["retcode"] = salt.defaults.exitcodes.SALT_BUILD_FAIL
        raise CommandExecutionError("Element doesn't exists")
    if response.status_code == 401:
        __context__["retcode"] = salt.defaults.exitcodes.SALT_BUILD_FAIL
        raise CommandExecutionError("Bad username or password")
    elif response.status_code == 200 or response.status_code == 500:
        try:
            data = salt.utils.json.loads(response.content)
            if data["exit_code"] != "SUCCESS":
                __context__["retcode"] = salt.defaults.exitcodes.SALT_BUILD_FAIL
                raise CommandExecutionError(data["message"])
            return data
        except ValueError:
            __context__["retcode"] = salt.defaults.exitcodes.SALT_BUILD_FAIL
            raise CommandExecutionError("The server returned no data")
    else:
        response.raise_for_status()


def _api_get(path, server=None):
    """
    Do a GET request to the API
    """
    server = _get_server(server)
    response = requests.get(
        url=_get_url(server["ssl"], server["url"], server["port"], path),
        auth=_get_auth(server["user"], server["password"]),
        headers=_get_headers(),
        verify=True,
    )
    return _api_response(response)


def _api_post(path, data, server=None):
    """
    Do a POST request to the API
    """
    server = _get_server(server)
    response = requests.post(
        url=_get_url(server["ssl"], server["url"], server["port"], path),
        auth=_get_auth(server["user"], server["password"]),
        headers=_get_headers(),
        data=salt.utils.json.dumps(data),
        verify=True,
    )
    return _api_response(response)


def _api_delete(path, data, server=None):
    """
    Do a DELETE request to the API
    """
    server = _get_server(server)
    response = requests.delete(
        url=_get_url(server["ssl"], server["url"], server["port"], path),
        auth=_get_auth(server["user"], server["password"]),
        headers=_get_headers(),
        params=data,
        verify=True,
    )
    return _api_response(response)


# "Middle layer": uses _api_* functions to enum/get/create/update/delete elements
def _enum_elements(name, server=None):
    """
    Enum elements
    """
    elements = []
    data = _api_get(name, server)

    if any(data["extraProperties"]["childResources"]):
        for element in data["extraProperties"]["childResources"]:
            elements.append(element)
        return elements
    return None


def _get_element_properties(name, element_type, server=None):
    """
    Get an element's properties
    """
    properties = {}
    data = _api_get("{}/{}/property".format(element_type, name), server)

    # Get properties into a dict
    if any(data["extraProperties"]["properties"]):
        for element in data["extraProperties"]["properties"]:
            properties[element["name"]] = element["value"]
        return properties
    return {}


def _get_element(name, element_type, server=None, with_properties=True):
    """
    Get an element with or without properties
    """
    element = {}
    name = urllib.parse.quote(name, safe="")
    data = _api_get("{}/{}".format(element_type, name), server)

    # Format data, get properties if asked, and return the whole thing
    if any(data["extraProperties"]["entity"]):
        for key, value in data["extraProperties"]["entity"].items():
            element[key] = value
        if with_properties:
            element["properties"] = _get_element_properties(name, element_type)
        return element
    return None


def _create_element(name, element_type, data, server=None):
    """
    Create a new element
    """
    # Define property and id from name and properties + remove SaltStack parameters
    if "properties" in data:
        data["property"] = ""
        for key, value in data["properties"].items():
            if not data["property"]:
                data["property"] += "{}={}".format(key, value.replace(":", "\\:"))
            else:
                data["property"] += ":{}={}".format(key, value.replace(":", "\\:"))
        del data["properties"]

    # Send request
    _api_post(element_type, _clean_data(data), server)
    return urllib.parse.unquote(name)


def _update_element(name, element_type, data, server=None):
    """
    Update an element, including its properties
    """
    # Urlencode the name (names may have slashes)
    name = urllib.parse.quote(name, safe="")

    # Update properties first
    if "properties" in data:
        properties = []
        for key, value in data["properties"].items():
            properties.append({"name": key, "value": value})
        _api_post("{}/{}/property".format(element_type, name), properties, server)
        del data["properties"]

        # If the element only contained properties
        if not data:
            return urllib.parse.unquote(name)

    # Get the current data then merge updated data into it
    update_data = _get_element(name, element_type, server, with_properties=False)
    if update_data:
        update_data.update(data)
    else:
        __context__["retcode"] = salt.defaults.exitcodes.SALT_BUILD_FAIL
        raise CommandExecutionError("Cannot update {}".format(name))

    # Finally, update the element
    _api_post("{}/{}".format(element_type, name), _clean_data(update_data), server)
    return urllib.parse.unquote(name)


def _delete_element(name, element_type, data, server=None):
    """
    Delete an element
    """
    _api_delete(
        "{}/{}".format(element_type, urllib.parse.quote(name, safe="")), data, server
    )
    return name


# Connector connection pools
def enum_connector_c_pool(server=None):
    """
    Enum connection pools
    """
    return _enum_elements("resources/connector-connection-pool", server)


def get_connector_c_pool(name, server=None):
    """
    Get a specific connection pool
    """
    return _get_element(name, "resources/connector-connection-pool", server)


def create_connector_c_pool(name, server=None, **kwargs):
    """
    Create a connection pool
    """
    defaults = {
        "connectionDefinitionName": "javax.jms.ConnectionFactory",
        "resourceAdapterName": "jmsra",
        "associateWithThread": False,
        "connectionCreationRetryAttempts": 0,
        "connectionCreationRetryIntervalInSeconds": 0,
        "connectionLeakReclaim": False,
        "connectionLeakTimeoutInSeconds": 0,
        "description": "",
        "failAllConnections": False,
        "id": name,
        "idleTimeoutInSeconds": 300,
        "isConnectionValidationRequired": False,
        "lazyConnectionAssociation": False,
        "lazyConnectionEnlistment": False,
        "matchConnections": True,
        "maxConnectionUsageCount": 0,
        "maxPoolSize": 32,
        "maxWaitTimeInMillis": 60000,
        "ping": False,
        "poolResizeQuantity": 2,
        "pooling": True,
        "steadyPoolSize": 8,
        "target": "server",
        "transactionSupport": "",
        "validateAtmostOncePeriodInSeconds": 0,
    }

    # Data = defaults + merge kwargs + remove salt
    data = defaults
    data.update(kwargs)

    # Check TransactionSupport against acceptable values
    if data["transactionSupport"] and data["transactionSupport"] not in (
        "XATransaction",
        "LocalTransaction",
        "NoTransaction",
    ):
        raise CommandExecutionError("Invalid transaction support")

    return _create_element(name, "resources/connector-connection-pool", data, server)


def update_connector_c_pool(name, server=None, **kwargs):
    """
    Update a connection pool
    """
    if "transactionSupport" in kwargs and kwargs["transactionSupport"] not in (
        "XATransaction",
        "LocalTransaction",
        "NoTransaction",
    ):
        raise CommandExecutionError("Invalid transaction support")
    return _update_element(name, "resources/connector-connection-pool", kwargs, server)


def delete_connector_c_pool(name, target="server", cascade=True, server=None):
    """
    Delete a connection pool
    """
    data = {"target": target, "cascade": cascade}
    return _delete_element(name, "resources/connector-connection-pool", data, server)


# Connector resources
def enum_connector_resource(server=None):
    """
    Enum connection resources
    """
    return _enum_elements("resources/connector-resource", server)


def get_connector_resource(name, server=None):
    """
    Get a specific connection resource
    """
    return _get_element(name, "resources/connector-resource", server)


def create_connector_resource(name, server=None, **kwargs):
    """
    Create a connection resource
    """
    defaults = {
        "description": "",
        "enabled": True,
        "id": name,
        "poolName": "",
        "objectType": "user",
        "target": "server",
    }

    # Data = defaults + merge kwargs + poolname
    data = defaults
    data.update(kwargs)

    if not data["poolName"]:
        raise CommandExecutionError("No pool name!")

    # Fix for lowercase vs camelCase naming differences
    for key, value in list(data.items()):
        del data[key]
        data[key.lower()] = value

    return _create_element(name, "resources/connector-resource", data, server)


def update_connector_resource(name, server=None, **kwargs):
    """
    Update a connection resource
    """
    # You're not supposed to update jndiName, if you do so, it will crash, silently
    if "jndiName" in kwargs:
        del kwargs["jndiName"]
    return _update_element(name, "resources/connector-resource", kwargs, server)


def delete_connector_resource(name, target="server", server=None):
    """
    Delete a connection resource
    """
    return _delete_element(
        name, "resources/connector-resource", {"target": target}, server
    )


# JMS Destinations
def enum_admin_object_resource(server=None):
    """
    Enum JMS destinations
    """
    return _enum_elements("resources/admin-object-resource", server)


def get_admin_object_resource(name, server=None):
    """
    Get a specific JMS destination
    """
    return _get_element(name, "resources/admin-object-resource", server)


def create_admin_object_resource(name, server=None, **kwargs):
    """
    Create a JMS destination
    """
    defaults = {
        "description": "",
        "className": "com.sun.messaging.Queue",
        "enabled": True,
        "id": name,
        "resAdapter": "jmsra",
        "resType": "javax.jms.Queue",
        "target": "server",
    }

    # Data = defaults + merge kwargs + poolname
    data = defaults
    data.update(kwargs)

    # ClassName isn't optional, even if the API says so
    if data["resType"] == "javax.jms.Queue":
        data["className"] = "com.sun.messaging.Queue"
    elif data["resType"] == "javax.jms.Topic":
        data["className"] = "com.sun.messaging.Topic"
    else:
        raise CommandExecutionError(
            'resType should be "javax.jms.Queue" or "javax.jms.Topic"!'
        )

    if data["resAdapter"] != "jmsra":
        raise CommandExecutionError('resAdapter should be "jmsra"!')

    # Fix for lowercase vs camelCase naming differences
    if "resType" in data:
        data["restype"] = data["resType"]
        del data["resType"]
    if "className" in data:
        data["classname"] = data["className"]
        del data["className"]

    return _create_element(name, "resources/admin-object-resource", data, server)


def update_admin_object_resource(name, server=None, **kwargs):
    """
    Update a JMS destination
    """
    if "jndiName" in kwargs:
        del kwargs["jndiName"]
    return _update_element(name, "resources/admin-object-resource", kwargs, server)


def delete_admin_object_resource(name, target="server", server=None):
    """
    Delete a JMS destination
    """
    return _delete_element(
        name, "resources/admin-object-resource", {"target": target}, server
    )


# JDBC Pools
def enum_jdbc_connection_pool(server=None):
    """
    Enum JDBC pools
    """
    return _enum_elements("resources/jdbc-connection-pool", server)


def get_jdbc_connection_pool(name, server=None):
    """
    Get a specific JDBC pool
    """
    return _get_element(name, "resources/jdbc-connection-pool", server)


def create_jdbc_connection_pool(name, server=None, **kwargs):
    """
    Create a connection resource
    """
    defaults = {
        "allowNonComponentCallers": False,
        "associateWithThread": False,
        "connectionCreationRetryAttempts": "0",
        "connectionCreationRetryIntervalInSeconds": "10",
        "connectionLeakReclaim": False,
        "connectionLeakTimeoutInSeconds": "0",
        "connectionValidationMethod": "table",
        "datasourceClassname": "",
        "description": "",
        "driverClassname": "",
        "failAllConnections": False,
        "idleTimeoutInSeconds": "300",
        "initSql": "",
        "isConnectionValidationRequired": False,
        "isIsolationLevelGuaranteed": True,
        "lazyConnectionAssociation": False,
        "lazyConnectionEnlistment": False,
        "matchConnections": False,
        "maxConnectionUsageCount": "0",
        "maxPoolSize": "32",
        "maxWaitTimeInMillis": 60000,
        "name": name,
        "nonTransactionalConnections": False,
        "ping": False,
        "poolResizeQuantity": "2",
        "pooling": True,
        "resType": "",
        "sqlTraceListeners": "",
        "statementCacheSize": "0",
        "statementLeakReclaim": False,
        "statementLeakTimeoutInSeconds": "0",
        "statementTimeoutInSeconds": "-1",
        "steadyPoolSize": "8",
        "target": "server",
        "transactionIsolationLevel": "",
        "validateAtmostOncePeriodInSeconds": "0",
        "validationClassname": "",
        "validationTableName": "",
        "wrapJdbcObjects": True,
    }

    # Data = defaults + merge kwargs + poolname
    data = defaults
    data.update(kwargs)

    # Check resType against acceptable values
    if data["resType"] not in (
        "javax.sql.DataSource",
        "javax.sql.XADataSource",
        "javax.sql.ConnectionPoolDataSource",
        "java.sql.Driver",
    ):
        raise CommandExecutionError("Invalid resource type")

    # Check connectionValidationMethod against acceptable velues
    if data["connectionValidationMethod"] not in (
        "auto-commit",
        "meta-data",
        "table",
        "custom-validation",
    ):
        raise CommandExecutionError("Invalid connection validation method")

    if data["transactionIsolationLevel"] and data["transactionIsolationLevel"] not in (
        "read-uncommitted",
        "read-committed",
        "repeatable-read",
        "serializable",
    ):
        raise CommandExecutionError("Invalid transaction isolation level")

    if not data["datasourceClassname"] and data["resType"] in (
        "javax.sql.DataSource",
        "javax.sql.ConnectionPoolDataSource",
        "javax.sql.XADataSource",
    ):
        raise CommandExecutionError(
            "No datasource class name while using datasource resType"
        )
    if not data["driverClassname"] and data["resType"] == "java.sql.Driver":
        raise CommandExecutionError("No driver class nime while using driver resType")

    return _create_element(name, "resources/jdbc-connection-pool", data, server)


def update_jdbc_connection_pool(name, server=None, **kwargs):
    """
    Update a JDBC pool
    """
    return _update_element(name, "resources/jdbc-connection-pool", kwargs, server)


def delete_jdbc_connection_pool(name, target="server", cascade=False, server=None):
    """
    Delete a JDBC pool
    """
    data = {"target": target, "cascade": cascade}
    return _delete_element(name, "resources/jdbc-connection-pool", data, server)


# JDBC resources
def enum_jdbc_resource(server=None):
    """
    Enum JDBC resources
    """
    return _enum_elements("resources/jdbc-resource", server)


def get_jdbc_resource(name, server=None):
    """
    Get a specific JDBC resource
    """
    return _get_element(name, "resources/jdbc-resource", server)


def create_jdbc_resource(name, server=None, **kwargs):
    """
    Create a JDBC resource
    """
    defaults = {
        "description": "",
        "enabled": True,
        "id": name,
        "poolName": "",
        "target": "server",
    }

    # Data = defaults + merge kwargs + poolname
    data = defaults
    data.update(kwargs)

    if not data["poolName"]:
        raise CommandExecutionError("No pool name!")

    return _create_element(name, "resources/jdbc-resource", data, server)


def update_jdbc_resource(name, server=None, **kwargs):
    """
    Update a JDBC resource
    """
    # You're not supposed to update jndiName, if you do so, it will crash, silently
    if "jndiName" in kwargs:
        del kwargs["jndiName"]
    return _update_element(name, "resources/jdbc-resource", kwargs, server)


def delete_jdbc_resource(name, target="server", server=None):
    """
    Delete a JDBC resource
    """
    return _delete_element(name, "resources/jdbc-resource", {"target": target}, server)


# System properties
def get_system_properties(server=None):
    """
    Get system properties
    """
    properties = {}
    data = _api_get("system-properties", server)

    # Get properties into a dict
    if any(data["extraProperties"]["systemProperties"]):
        for element in data["extraProperties"]["systemProperties"]:
            properties[element["name"]] = element["value"]
        return properties
    return {}


def update_system_properties(data, server=None):
    """
    Update system properties
    """
    _api_post("system-properties", _clean_data(data), server)
    return data


def delete_system_properties(name, server=None):
    """
    Delete a system property
    """
    _api_delete("system-properties/{}".format(name), None, server)
