"""
A sensor to monitor incoming and outgoing phone calls on a Fritz!Box router.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.fritzbox_callmonitor/
"""
import logging
import socket
import threading
import datetime
import time
import re

import voluptuous as vol

from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (CONF_HOST, CONF_PORT, CONF_NAME,
                                 CONF_PASSWORD, CONF_USERNAME,
                                 EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle

REQUIREMENTS = ['fritzconnection==0.6.5']

_LOGGER = logging.getLogger(__name__)

CONF_PHONEBOOK = 'phonebook'
CONF_PREFIXES = 'prefixes'

DEFAULT_HOST = '169.254.1.1'  # IP valid for all Fritz!Box routers
DEFAULT_NAME = 'Phone'
DEFAULT_PORT = 1012

INTERVAL_RECONNECT = 60

VALUE_CALL = 'dialing'
VALUE_CONNECT = 'talking'
VALUE_DEFAULT = 'idle'
VALUE_DISCONNECT = 'idle'
VALUE_RING = 'ringing'

# Return cached results if phonebook was downloaded less then this time ago.
MIN_TIME_PHONEBOOK_UPDATE = datetime.timedelta(hours=6)
SCAN_INTERVAL = datetime.timedelta(hours=3)

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
    vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
    vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
    vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
    vol.Optional(CONF_PASSWORD, default='admin'): cv.string,
    vol.Optional(CONF_USERNAME, default=''): cv.string,
    vol.Optional(CONF_PHONEBOOK, default=0): cv.positive_int,
    vol.Optional(CONF_PREFIXES, default=[]):
        vol.All(cv.ensure_list, [cv.string])
})


def setup_platform(hass, config, add_entities, discovery_info=None):
    """Set up Fritz!Box call monitor sensor platform."""
    name = config.get(CONF_NAME)
    host = config.get(CONF_HOST)
    port = config.get(CONF_PORT)
    username = config.get(CONF_USERNAME)
    password = config.get(CONF_PASSWORD)
    phonebook_id = config.get(CONF_PHONEBOOK)
    prefixes = config.get(CONF_PREFIXES)

    try:
        phonebook = FritzBoxPhonebook(
            host=host, port=port, username=username, password=password,
            phonebook_id=phonebook_id, prefixes=prefixes)
    except:  # noqa: E722 pylint: disable=bare-except
        phonebook = None
        _LOGGER.warning("Phonebook with ID %s not found on Fritz!Box",
                        phonebook_id)

    sensor = FritzBoxCallSensor(name=name, phonebook=phonebook)

    add_entities([sensor])

    monitor = FritzBoxCallMonitor(host=host, port=port, sensor=sensor)
    monitor.connect()

    def _stop_listener(_event):
        monitor.stopped.set()

    hass.bus.listen_once(
        EVENT_HOMEASSISTANT_STOP,
        _stop_listener
    )

    return monitor.sock is not None


class FritzBoxCallSensor(Entity):
    """Implementation of a Fritz!Box call monitor."""

    def __init__(self, name, phonebook):
        """Initialize the sensor."""
        self._state = VALUE_DEFAULT
        self._attributes = {}
        self._name = name
        self.phonebook = phonebook

    def set_state(self, state):
        """Set the state."""
        self._state = state

    def set_attributes(self, attributes):
        """Set the state attributes."""
        self._attributes = attributes

    @property
    def should_poll(self):
        """Only poll to update phonebook, if defined."""
        return self.phonebook is not None

    @property
    def state(self):
        """Return the state of the device."""
        return self._state

    @property
    def name(self):
        """Return the name of the sensor."""
        return self._name

    @property
    def device_state_attributes(self):
        """Return the state attributes."""
        return self._attributes

    def number_to_name(self, number):
        """Return a name for a given phone number."""
        if self.phonebook is None:
            return 'unknown'
        return self.phonebook.get_name(number)

    def update(self):
        """Update the phonebook if it is defined."""
        if self.phonebook is not None:
            self.phonebook.update_phonebook()


class FritzBoxCallMonitor:
    """Event listener to monitor calls on the Fritz!Box."""

    def __init__(self, host, port, sensor):
        """Initialize Fritz!Box monitor instance."""
        self.host = host
        self.port = port
        self.sock = None
        self._sensor = sensor
        self.stopped = threading.Event()

    def connect(self):
        """Connect to the Fritz!Box."""
        _LOGGER.debug('Setting up socket...')
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.settimeout(10)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
        try:
            self.sock.connect((self.host, self.port))
            threading.Thread(target=self._listen).start()
        except socket.error as err:
            self.sock = None
            _LOGGER.error("Cannot connect to %s on port %s: %s",
                          self.host, self.port, err)

    def _listen(self):
        """Listen to incoming or outgoing calls."""
        _LOGGER.debug('Connection established, waiting for response...')
        while not self.stopped.isSet():
            try:
                response = self.sock.recv(2048)
            except socket.timeout:
                # if no response after 10 seconds, just recv again
                continue
            response = str(response, "utf-8")
            _LOGGER.debug('Received %s', response)

            if not response:
                # if the response is empty, the connection has been lost.
                # try to reconnect
                _LOGGER.warning('Connection lost, reconnecting...')
                self.sock = None
                while self.sock is None:
                    self.connect()
                    time.sleep(INTERVAL_RECONNECT)
            else:
                line = response.split("\n", 1)[0]
                self._parse(line)
                time.sleep(1)

    def _parse(self, line):
        """Parse the call information and set the sensor states."""
        line = line.split(";")