"""
Support for Vanderbilt (formerly Siemens) SPC alarm systems.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/spc/
"""
import asyncio
import json
import logging
from urllib.parse import urljoin

import aiohttp
import async_timeout
import voluptuous as vol

from homeassistant.const import (
    STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
    STATE_ALARM_TRIGGERED, STATE_OFF, STATE_ON, STATE_UNAVAILABLE,
    STATE_UNKNOWN)
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv

REQUIREMENTS = ['websockets==3.2']

_LOGGER = logging.getLogger(__name__)

ATTR_DISCOVER_DEVICES = 'devices'
ATTR_DISCOVER_AREAS = 'areas'

CONF_WS_URL = 'ws_url'
CONF_API_URL = 'api_url'

DATA_REGISTRY = 'spc_registry'
DATA_API = 'spc_api'
DOMAIN = 'spc'

CONFIG_SCHEMA = vol.Schema({
    DOMAIN: vol.Schema({
        vol.Required(CONF_WS_URL): cv.string,
        vol.Required(CONF_API_URL): cv.string
    }),
}, extra=vol.ALLOW_EXTRA)


@asyncio.coroutine
def async_setup(hass, config):
    """Set up the SPC platform."""
    hass.data[DATA_REGISTRY] = SpcRegistry()

    api = SpcWebGateway(hass,
                        config[DOMAIN].get(CONF_API_URL),
                        config[DOMAIN].get(CONF_WS_URL))

    hass.data[DATA_API] = api

    # add sensor devices for each zone (typically motion/fire/door sensors)
    zones = yield from api.get_zones()
    if zones:
        hass.async_add_job(discovery.async_load_platform(
            hass, 'binary_sensor', DOMAIN,
            {ATTR_DISCOVER_DEVICES: zones}, config))

    # create a separate alarm panel for each area
    areas = yield from api.get_areas()
    if areas:
        hass.async_add_job(discovery.async_load_platform(
            hass, 'alarm_control_panel', DOMAIN,
            {ATTR_DISCOVER_AREAS: areas}, config))

    # start listening for incoming events over websocket
    api.start_listener(_async_process_message, hass.data[DATA_REGISTRY])

    return True


@asyncio.coroutine
def _async_process_message(sia_message, spc_registry):
    spc_id = sia_message['sia_address']
    sia_code = sia_message['sia_code']

    # BA - Burglary Alarm
    # CG - Close Area
    # NL - Perimeter Armed
    # OG - Open Area
    # ZO - Zone Open
    # ZC - Zone Close
    # ZX - Zone Short
    # ZD - Zone Disconnected

    extra = {}

    if sia_code in ('BA', 'CG', 'NL', 'OG'):
        # change in area status, notify alarm panel device
        device = spc_registry.get_alarm_device(spc_id)
        data = sia_message['description'].split('¦')
        if len(data) == 3:
            extra['changed_by'] = data[1]
    else:
        # Change in zone status, notify sensor device
        device = spc_registry.get_sensor_device(spc_id)

    sia_code_to_state_map = {
        'BA': STATE_ALARM_TRIGGERED,
        'CG': STATE_ALARM_ARMED_AWAY,
        'NL': STATE_ALARM_ARMED_HOME,
        'OG': STATE_ALARM_DISARMED,
        'ZO': STATE_ON,
        'ZC': STATE_OFF,
        'ZX': STATE_UNKNOWN,
        'ZD': STATE_UNAVAILABLE,
    }

    new_state = sia_code_to_state_map.get(sia_code, None)

    if new_state and not device:
        _LOGGER.warning(
            "No device mapping found for SPC area/zone id %s", spc_id)
    elif new_state:
        yield from device.async_update_from_spc(new_state, extra)


class SpcRegistry:
    """Maintain mappings between SPC zones/areas and HA entities."""

    def __init__(self):
        """Initialize the registry."""
        self._zone_id_to_sensor_map = {}
        self._area_id_to_alarm_map = {}

    def register_sensor_device(self, zone_id, device):
        """Add a sensor device to the registry."""
        self._zone_id_to_sensor_map[zone_id] = device

    def get_sensor_device(self, zone_id):
        """Retrieve a sensor device for a specific zone."""
        return self._zone_id_to_sensor_map.get(zone_id, None)

    def register_alarm_device(self, area_id, device):
        """Add an alarm device to the registry."""
        self._area_id_to_alarm_map[area_id] = device

    def get_alarm_device(self, area_id):
        """Retrieve an alarm device for a specific area."""
        return self._area_id_to_alarm_map.get(area_id, None)


@asyncio.coroutine
def _ws_process_message(message, async_callback, *args):
    if message.get('status', '') != 'success':
        _LOGGER.warning(
            "Unsuccessful websocket message delivered, ignoring: %s", message)
    try:
        yield from async_callback(message['data']['sia'], *args)
    except:  # noqa: E722  # pylint: disable=bare-except
        _LOGGER.exception("Exception in callback, ignoring")


class SpcWebGateway:
    """Simple binding for the Lundix SPC Web Gateway REST API."""

    AREA_COMMAND_SET = 'set'
    AREA_COMMAND_PART_SET = 'set_a'
    AREA_COMMAND_UNSET = 'unset'

    def __init__(self, hass, api_url, ws_url):
        """Initialize the web gateway client."""
        self._hass = hass
        self._api_url = api_url
        self._ws_url = ws_url
        self._ws = None

    @asyncio.coroutine
    def get_zones(self):
        """Retrieve all available zones."""
        return (yield from self._get_data('zone'))

    @asyncio.coroutine
    def get_areas(self):
        """Retrieve all available areas."""
        return (yield from self._get_data('area'))

    @asyncio.coroutine
    def send_area_command(self, area_id, command):
        """Send an area command."""
        _LOGGER.debug(
            "Sending SPC area command '%s' to area %s", command, area_id)
        resource = "area/{}/{}".format(area_id, command)
        return (yield from self._call_web_gateway(resource, use_get=False))

    def start_listener(self, async_callback, *args):
        """Start the websocket listener."""
        try:
            from asyncio import ensure_future
        except ImportError:
            from asyncio import async as ensure_future

        ensure_future(self._ws_listen(async_callback, *args))

    def _build_url(self, resource):
        return urljoin(self._api_url, "spc/{}".format(resource))

    @asyncio.coroutine
    def _get_data(self, resource):
        """Get the data from the resource."""
        data = yield from self._call_web_gateway(resource)
        if not data:
            return False
        if data['status'] != 'success':
            _LOGGER.error(
                "SPC Web Gateway call unsuccessful for resource: %s", resource)
            return False
        return [item for item in data['data'][resource]]

    @asyncio.coroutine
    def _call_web_gateway(self, resource, use_get=True):
        """Call web gateway for data."""
        response = None
        session = None
        url = self._build_url(resource)
        try:
            _LOGGER.debug("Attempting to retrieve SPC data from %s", url)
            session = aiohttp.ClientSession()
            with async_timeout.timeout(10, loop=self._hass.loop):
                action = session.get if use_get else session.put
                response = yield from action(url)
            if response.status != 200:
                _LOGGER.error(
                    "SPC Web Gateway returned http status %d, response %s",
                    response.status, (yield from response.text()))
                return False
            result = yield from response.json()
        except asyncio.TimeoutError:
            _LOGGER.error("Timeout getting SPC data from %s", url)
            return False
        except aiohttp.ClientError:
            _LOGGER.exception("Error getting SPC data from %s", url)
            return False
        finally:
            if session:
                yield from session.close()
            if response:
                yield from response.release()
        _LOGGER.debug("Data from SPC: %s", result)
        return result

    @asyncio.coroutine
    def _ws_read(self):
        """Read from websocket."""
        import websockets as wslib

        try:
            if not self._ws:
                self._ws = yield from wslib.connect(self._ws_url)
                _LOGGER.info("Connected to websocket at %s", self._ws_url)
        except Exception as ws_exc:    # pylint: disable=broad-except
            _LOGGER.error("Failed to connect to websocket: %s", ws_exc)
            return

        result = None

        try:
            result = yield from self._ws.recv()
            _LOGGER.debug("Data from websocket: %s", result)
        except Exception as ws_exc:    # pylint: disable=broad-except
            _LOGGER.error("Failed to read from websocket: %s", ws_exc)
            try:
                yield from self._ws.close()
            finally:
                self._ws = None

        return result

    @asyncio.coroutine
    def _ws_listen(self, async_callback, *args):
        """Listen on websocket."""
        try:
            while True:
                result = yield from self._ws_read()

                if result:
                    yield from _ws_process_message(
                        json.loads(result), async_callback, *args)
                else:
                    _LOGGER.info("Trying again in 30 seconds")
                    yield from asyncio.sleep(30)

        finally:
            if self._ws:
                yield from self._ws.close()
