"""Web socket API for OpenZWave."""
import logging

from openzwavemqtt.const import (
    ATTR_CODE_SLOT,
    ATTR_LABEL,
    ATTR_POSITION,
    ATTR_VALUE,
    EVENT_NODE_ADDED,
    EVENT_NODE_CHANGED,
)
from openzwavemqtt.exceptions import NotFoundError, NotSupportedError
from openzwavemqtt.util.lock import clear_usercode, get_code_slots, set_usercode
from openzwavemqtt.util.node import (
    get_config_parameters,
    get_node_from_manager,
    set_config_parameter,
)
import voluptuous as vol
import voluptuous_serialize

from homeassistant.components import websocket_api
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv

from .const import ATTR_CONFIG_PARAMETER, ATTR_CONFIG_VALUE, DOMAIN, MANAGER
from .lock import ATTR_USERCODE
from .migration import async_get_migration_data, async_migrate, map_node_values

_LOGGER = logging.getLogger(__name__)

DRY_RUN = "dry_run"
TYPE = "type"
ID = "id"
OZW_INSTANCE = "ozw_instance"
NODE_ID = "node_id"
PARAMETER = ATTR_CONFIG_PARAMETER
VALUE = ATTR_CONFIG_VALUE
SCHEMA = "schema"

ATTR_NODE_QUERY_STAGE = "node_query_stage"
ATTR_IS_ZWAVE_PLUS = "is_zwave_plus"
ATTR_IS_AWAKE = "is_awake"
ATTR_IS_FAILED = "is_failed"
ATTR_NODE_BAUD_RATE = "node_baud_rate"
ATTR_IS_BEAMING = "is_beaming"
ATTR_IS_FLIRS = "is_flirs"
ATTR_IS_ROUTING = "is_routing"
ATTR_IS_SECURITYV1 = "is_securityv1"
ATTR_NODE_BASIC_STRING = "node_basic_string"
ATTR_NODE_GENERIC_STRING = "node_generic_string"
ATTR_NODE_SPECIFIC_STRING = "node_specific_string"
ATTR_NODE_MANUFACTURER_NAME = "node_manufacturer_name"
ATTR_NODE_PRODUCT_NAME = "node_product_name"
ATTR_NEIGHBORS = "neighbors"


@callback
def async_register_api(hass):
    """Register all of our api endpoints."""
    websocket_api.async_register_command(hass, websocket_migrate_zwave)
    websocket_api.async_register_command(hass, websocket_get_instances)
    websocket_api.async_register_command(hass, websocket_get_nodes)
    websocket_api.async_register_command(hass, websocket_network_status)
    websocket_api.async_register_command(hass, websocket_network_statistics)
    websocket_api.async_register_command(hass, websocket_node_metadata)
    websocket_api.async_register_command(hass, websocket_node_status)
    websocket_api.async_register_command(hass, websocket_node_statistics)
    websocket_api.async_register_command(hass, websocket_refresh_node_info)
    websocket_api.async_register_command(hass, websocket_get_config_parameters)
    websocket_api.async_register_command(hass, websocket_set_config_parameter)
    websocket_api.async_register_command(hass, websocket_set_usercode)
    websocket_api.async_register_command(hass, websocket_clear_usercode)
    websocket_api.async_register_command(hass, websocket_get_code_slots)


def _call_util_function(hass, connection, msg, send_result, function, *args):
    """Call an openzwavemqtt.util function."""
    try:
        node = get_node_from_manager(
            hass.data[DOMAIN][MANAGER], msg[OZW_INSTANCE], msg[NODE_ID]
        )
    except NotFoundError as err:
        connection.send_error(
            msg[ID],
            websocket_api.const.ERR_NOT_FOUND,
            err.args[0],
        )
        return

    try:
        payload = function(node, *args)
    except NotFoundError as err:
        connection.send_error(
            msg[ID],
            websocket_api.const.ERR_NOT_FOUND,
            err.args[0],
        )
        return
    except NotSupportedError as err:
        connection.send_error(
            msg[ID],
            websocket_api.const.ERR_NOT_SUPPORTED,
            err.args[0],
        )
        return

    if send_result:
        connection.send_result(
            msg[ID],
            payload,
        )
        return

    connection.send_result(msg[ID])


def _get_config_params(node, *args):
    raw_values = get_config_parameters(node)
    config_params = []

    for param in raw_values:
        schema = {}

        if param["type"] in ("Byte", "Int", "Short"):
            schema = vol.Schema(
                {
                    vol.Required(param["label"], default=param["value"]): vol.All(
                        vol.Coerce(int), vol.Range(min=param["min"], max=param["max"])
                    )
                }
            )
            data = {param["label"]: param["value"]}

        if param["type"] == "List":

            for options in param["options"]:
                if options["Label"] == param["value"]:
                    selected = options
                    break

            schema = vol.Schema(
                {
                    vol.Required(param["label"],): vol.In(
                        {
                            option["Value"]: option["Label"]
                            for option in param["options"]
                        }
                    )
                }
            )
            data = {param["label"]: selected["Value"]}

        config_params.append(
            {
                "type": param["type"],
                "label": param["label"],
                "parameter": param["parameter"],
                "help": param["help"],
                "value": param["value"],
                "schema": voluptuous_serialize.convert(
                    schema, custom_serializer=cv.custom_serializer
                ),
                "data": data,
            }
        )

    return config_params


@websocket_api.require_admin
@websocket_api.async_response
@websocket_api.websocket_command(
    {
        vol.Required(TYPE): "ozw/migrate_zwave",
        vol.Optional(DRY_RUN, default=True): bool,
    }
)
async def websocket_migrate_zwave(hass, connection, msg):
    """Migrate the zwave integration device and entity data to ozw integration."""
    if "zwave" not in hass.config.components:
        _LOGGER.error("Can not migrate, zwave integration is not loaded")
        connection.send_message(
            websocket_api.error_message(
                msg["id"], "zwave_not_loaded", "Integration zwave is not loaded"
            )
        )
        return

    zwave = hass.components.zwave
    zwave_data = await zwave.async_get_ozw_migration_data(hass)
    _LOGGER.debug("Migration zwave data: %s", zwave_data)

    ozw_data = await async_get_migration_data(hass)
    _LOGGER.debug("Migration ozw data: %s", ozw_data)

    can_migrate = map_node_values(zwave_data, ozw_data)

    zwave_entity_ids = [
        entry["entity_entry"].entity_id for entry in zwave_data.values()
    ]
    ozw_entity_ids = [entry["entity_entry"].entity_id for entry in ozw_data.values()]
    migration_device_map = {
        zwave_device_id: ozw_device_id
        for ozw_device_id, zwave_device_id in can_migrate["device_entries"].items()
    }
    migration_entity_map = {
        zwave_entry["entity_entry"].entity_id: ozw_entity_id
        for ozw_entity_id, zwave_entry in can_migrate["entity_entries"].items()
    }
    _LOGGER.debug("Migration entity map: %s", migration_entity_map)

    if not msg[DRY_RUN]:
        await async_migrate(hass, can_migrate)

    connection.send_result(
        msg[ID],
        {
            "migration_device_map": migration_device_map,
            "zwave_entity_ids": zwave_entity_ids,
            "ozw_entity_ids": ozw_entity_ids,
            "migration_entity_map": migration_entity_map,
            "migrated": not msg[DRY_RUN],
        },
    )


@websocket_api.websocket_command({vol.Required(TYPE): "ozw/get_instances"})
def websocket_get_instances(hass, connection, msg):
    """Get a list of OZW instances."""
    manager = hass.data[DOMAIN][MANAGER]
    instances = []

    for instance in manager.collections["instance"]:
        instances.append(dict(instance.get_status().data, ozw_instance=instance.id))

    connection.send_result(
        msg[ID],
        instances,
    )


@websocket_api.websocket_command(
    {
        vol.Required(TYPE): "ozw/get_nodes",
        vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int),
    }
)
def websocket_get_nodes(hass, connection, msg):
    """Get a list of nodes for an OZW instance."""
    manager = hass.data[DOMAIN][MANAGER]
    nodes = []

    for node in manager.get_instance(msg[OZW_INSTANCE]).collections["node"]:
        nodes.append(
            {
                ATTR_NODE_QUERY_STAGE: node.node_query_stage,
                NODE_ID: node.node_id,
                ATTR_IS_ZWAVE_PLUS: node.is_zwave_plus,
                ATTR_IS_AWAKE: node.is_awake,
                ATTR_IS_FAILED: node.is_failed,
                ATTR_NODE_BAUD_RATE: node.node_baud_rate,
                ATTR_IS_BEAMING: node.is_beaming,
                ATTR_IS_FLIRS: node.is_flirs,
                ATTR_IS_ROUTING: node.is_routing,
                ATTR_IS_SECURITYV1: node.is_securityv1,
                ATTR_NODE_BASIC_STRING: node.node_basic_string,
                ATTR_NODE_GENERIC_STRING: node.node_generic_string,
                ATTR_NODE_SPECIFIC_STRING: node.node_specific_string,
                ATTR_NODE_MANUFACTURER_NAME: node.node_manufacturer_name,
                ATTR_NODE_PRODUCT_NAME: node.node_product_name,
                ATTR_NEIGHBORS: node.neighbors,
                OZW_INSTANCE: msg[OZW_INSTANCE],
            }
        )

    connection.send_result(
        msg[ID],
        nodes,
    )


@websocket_api.websocket_command(
    {
        vol.Required(TYPE): "ozw/set_usercode",
        vol.Required(NODE_ID): vol.Coerce(int),
        vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int),
        vol.Required(ATTR_CODE_SLOT): vol.Coerce(int),
        vol.Required(ATTR_USERCODE): cv.string,
    }
)
def websocket_set_usercode(hass, connection, msg):
    """Set a usercode to a node code slot."""
    _call_util_function(
        hass, connection, msg, False, set_usercode, msg[ATTR_CODE_SLOT], ATTR_USERCODE
    )


@websocket_api.websocket_command(
    {
        vol.Required(TYPE): "ozw/clear_usercode",
        vol.Required(NODE_ID): vol.Coerce(int),
        vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int),
        vol.Required(ATTR_CODE_SLOT): vol.Coerce(int),
    }
)
def websocket_clear_usercode(hass, connection, msg):
    """Clear a node code slot."""
    _call_util_function(
        hass, connection, msg, False, clear_usercode, msg[ATTR_CODE_SLOT]
    )


@websocket_api.websocket_command(
    {
        vol.Required(TYPE): "ozw/get_code_slots",
        vol.Required(NODE_ID): vol.Coerce(int),
        vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int),
    }
)
def websocket_get_code_slots(hass, connection, msg):
    """Get status of node's code slots."""
    _call_util_function(hass, connection, msg, True, get_code_slots)


@websocket_api.websocket_command(
    {
        vol.Required(TYPE): "ozw/get_config_parameters",
        vol.Required(NODE_ID): vol.Coerce(int),
        vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int),
    }
)
def websocket_get_config_parameters(hass, connection, msg):
    """Get a list of configuration parameters for an OZW node instance."""
    _call_util_function(hass, connection, msg, True, _get_config_params)


@websocket_api.websocket_command(
    {
        vol.Required(TYPE): "ozw/set_config_parameter",
        vol.Required(NODE_ID): vol.Coerce(int),
        vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int),
        vol.Required(PARAMETER): vol.Coerce(int),
        vol.Required(VALUE): vol.Any(
            vol.All(
                cv.ensure_list,
                [
                    vol.All(
                        {
                            vol.Exclusive(ATTR_LABEL, "bit"): cv.string,
                            vol.Exclusive(ATTR_POSITION, "bit"): vol.Coerce(int),
                            vol.Required(ATTR_VALUE): bool,
                        },
                        cv.has_at_least_one_key(ATTR_LABEL, ATTR_POSITION),
                    )
                ],
            ),
            vol.Coerce(int),
            bool,
            cv.string,
        ),
    }
)
def websocket_set_config_parameter(hass, connection, msg):
    """Set a config parameter to a node."""
    _call_util_function(
        hass, connection, msg, True, set_config_parameter, msg[PARAMETER], msg[VALUE]
    )


@websocket_api.websocket_command(
    {
        vol.Required(TYPE): "ozw/network_status",
        vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int),
    }
)
def websocket_network_status(hass, connection, msg):
    """Get Z-Wave network status."""

    manager = hass.data[DOMAIN][MANAGER]
    status = manager.get_instance(msg[OZW_INSTANCE]).get_status().data
    connection.send_result(
        msg[ID],
        dict(status, ozw_instance=msg[OZW_INSTANCE]),
    )


@websocket_api.websocket_command(
    {
        vol.Required(TYPE): "ozw/network_statistics",
        vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int),
    }
)
def websocket_network_statistics(hass, connection, msg):
    """Get Z-Wave network statistics."""

    manager = hass.data[DOMAIN][MANAGER]
    statistics = manager.get_instance(msg[OZW_INSTANCE]).get_statistics().data
    node_count = len(
        manager.get_instance(msg[OZW_INSTANCE]).collections["node"].collection
    )
    connection.send_result(
        msg[ID],
        dict(statistics, ozw_instance=msg[OZW_INSTANCE], node_count=node_count),
    )


@websocket_api.websocket_command(
    {
        vol.Required(TYPE): "ozw/node_status",
        vol.Required(NODE_ID): vol.Coerce(int),
        vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int),
    }
)
def websocket_node_status(hass, connection, msg):
    """Get the status for a Z-Wave node."""
    try:
        node = get_node_from_manager(
            hass.data[DOMAIN][MANAGER], msg[OZW_INSTANCE], msg[NODE_ID]
        )
    except NotFoundError as err:
        connection.send_error(
            msg[ID],
            websocket_api.const.ERR_NOT_FOUND,
            err.args[0],
        )
        return

    connection.send_result(
        msg[ID],
        {
            ATTR_NODE_QUERY_STAGE: node.node_query_stage,
            NODE_ID: node.node_id,
            ATTR_IS_ZWAVE_PLUS: node.is_zwave_plus,
            ATTR_IS_AWAKE: node.is_awake,
            ATTR_IS_FAILED: node.is_failed,
            ATTR_NODE_BAUD_RATE: node.node_baud_rate,
            ATTR_IS_BEAMING: node.is_beaming,
            ATTR_IS_FLIRS: node.is_flirs,
            ATTR_IS_ROUTING: node.is_routing,
            ATTR_IS_SECURITYV1: node.is_securityv1,
            ATTR_NODE_BASIC_STRING: node.node_basic_string,
            ATTR_NODE_GENERIC_STRING: node.node_generic_string,
            ATTR_NODE_SPECIFIC_STRING: node.node_specific_string,
            ATTR_NODE_MANUFACTURER_NAME: node.node_manufacturer_name,
            ATTR_NODE_PRODUCT_NAME: node.node_product_name,
            ATTR_NEIGHBORS: node.neighbors,
            OZW_INSTANCE: msg[OZW_INSTANCE],
        },
    )


@websocket_api.websocket_command(
    {
        vol.Required(TYPE): "ozw/node_metadata",
        vol.Required(NODE_ID): vol.Coerce(int),
        vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int),
    }
)
def websocket_node_metadata(hass, connection, msg):
    """Get the metadata for a Z-Wave node."""
    try:
        node = get_node_from_manager(
            hass.data[DOMAIN][MANAGER], msg[OZW_INSTANCE], msg[NODE_ID]
        )
    except NotFoundError as err:
        connection.send_error(
            msg[ID],
            websocket_api.const.ERR_NOT_FOUND,
            err.args[0],
        )
        return

    connection.send_result(
        msg[ID],
        {
            "metadata": node.meta_data,
            NODE_ID: node.node_id,
            OZW_INSTANCE: msg[OZW_INSTANCE],
        },
    )


@websocket_api.websocket_command(
    {
        vol.Required(TYPE): "ozw/node_statistics",
        vol.Required(NODE_ID): vol.Coerce(int),
        vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int),
    }
)
def websocket_node_statistics(hass, connection, msg):
    """Get the statistics for a Z-Wave node."""
    manager = hass.data[DOMAIN][MANAGER]
    stats = (
        manager.get_instance(msg[OZW_INSTANCE]).get_node(msg[NODE_ID]).get_statistics()
    )
    connection.send_result(
        msg[ID],
        {
            NODE_ID: msg[NODE_ID],
            "send_count": stats.send_count,
            "sent_failed": stats.sent_failed,
            "retries": stats.retries,
            "last_request_rtt": stats.last_request_rtt,
            "last_response_rtt": stats.last_response_rtt,
            "average_request_rtt": stats.average_request_rtt,
            "average_response_rtt": stats.average_response_rtt,
            "received_packets": stats.received_packets,
            "received_dup_packets": stats.received_dup_packets,
            "received_unsolicited": stats.received_unsolicited,
            OZW_INSTANCE: msg[OZW_INSTANCE],
        },
    )


@websocket_api.require_admin
@websocket_api.websocket_command(
    {
        vol.Required(TYPE): "ozw/refresh_node_info",
        vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int),
        vol.Required(NODE_ID): vol.Coerce(int),
    }
)
def websocket_refresh_node_info(hass, connection, msg):
    """Tell OpenZWave to re-interview a node."""

    manager = hass.data[DOMAIN][MANAGER]
    options = manager.options

    @callback
    def forward_node(node):
        """Forward node events to websocket."""
        if node.node_id != msg[NODE_ID]:
            return

        forward_data = {
            "type": "node_updated",
            ATTR_NODE_QUERY_STAGE: node.node_query_stage,
        }
        connection.send_message(websocket_api.event_message(msg["id"], forward_data))

    @callback
    def async_cleanup() -> None:
        """Remove signal listeners."""
        for unsub in unsubs:
            unsub()

    connection.subscriptions[msg["id"]] = async_cleanup
    unsubs = [
        options.listen(EVENT_NODE_CHANGED, forward_node),
        options.listen(EVENT_NODE_ADDED, forward_node),
    ]

    instance = manager.get_instance(msg[OZW_INSTANCE])
    instance.refresh_node(msg[NODE_ID])
    connection.send_result(msg["id"])
