"""
An Abode alarm Python library.
"""

import functools
import logging
import uuid

from more_itertools import consume
from requests.exceptions import RequestException
from requests_toolbelt import sessions

import jaraco
from jaraco.collections import Everything
from jaraco.functools import retry
from jaraco.itertools import always_iterable
from jaraco.net.http import cookies

from . import config, settings
from .automation import Automation
from .devices import alarm as ALARM
from .devices.base import Device, Unknown
from .event_controller import EventController
from .exceptions import AuthenticationException
from .helpers import errors as ERROR
from .helpers import urls

log = logging.getLogger(__name__)


@retry(
    retries=1,
    cleanup=lambda: config.paths.user_data.joinpath('cookies.json').unlink(),
    trap=Exception,
)
def _cookies():
    return cookies.ShelvedCookieJar.create(config.paths.user_data)


class Client:
    """Client to an Abode system."""

    def __init__(
        self,
        username=None,
        password=None,
        auto_login=False,
        get_devices=False,
        get_automations=False,
    ):
        self._session = None
        self._token = None
        self._panel = None
        self._user = None
        self._username = username
        self._password = password

        self._event_controller = EventController(self)

        self._default_alarm_mode = 'away'

        self._devices = None

        self._automations = None

        self._session = sessions.BaseUrlSession(urls.BASE)
        self._session.cookies = _cookies()

        if auto_login:
            self.login()

        if get_devices:
            self.get_devices()

        if get_automations:
            self.get_automations()

    def login(self, username=None, password=None, mfa_code=None):
        """Explicit Abode login."""

        self._token = None

        username = username or self._username
        password = password or self._password

        if not isinstance(username, str):
            raise AuthenticationException(ERROR.USERNAME)

        if not isinstance(password, str):
            raise AuthenticationException(ERROR.PASSWORD)

        login_data = {
            'id': username,
            'password': password,
            'uuid': self._session.cookies.get('uuid') or str(uuid.uuid1()),
        }

        if mfa_code is not None:
            login_data['mfa_code'] = mfa_code
            login_data['remember_me'] = 1

        response = self._session.post(urls.LOGIN, json=login_data)
        AuthenticationException.raise_for(response)
        response_object = response.json()

        # Check for multi-factor authentication
        if 'mfa_type' in response_object:
            if response_object['mfa_type'] == "google_authenticator":
                raise AuthenticationException(ERROR.MFA_CODE_REQUIRED)

            raise AuthenticationException(ERROR.UNKNOWN_MFA_TYPE)

        oauth_response = self._session.get(urls.OAUTH_TOKEN)
        AuthenticationException.raise_for(oauth_response)
        oauth_response_object = oauth_response.json()

        log.debug("Login URL: %s", urls.LOGIN)
        log.debug("Login Response: %s", response.text)

        self._token = response_object['token']
        self._panel = response_object['panel']
        self._user = response_object['user']
        self._oauth_token = oauth_response_object['access_token']

        log.info("Login successful")

    def logout(self):
        """Explicit Abode logout."""
        if not self._token:
            return

        header_data = {'ABODE-API-KEY': self._token}

        self._session = sessions.BaseUrlSession(urls.BASE)
        self._token = None
        self._panel = None
        self._user = None
        self._devices = None
        self._automations = None

        try:
            response = self._session.post(urls.LOGOUT, headers=header_data)
        except OSError as exc:
            log.warning("Caught exception during logout: %s", exc)
            return

        AuthenticationException.raise_for(response)

        log.debug("Logout URL: %s", urls.LOGOUT)
        log.debug("Logout Response: %s", response.text)

        log.info("Logout successful")

    def refresh(self):
        """Do a full refresh of all devices and automations."""
        self.get_devices(refresh=True)
        self.get_automations(refresh=True)

    def get_devices(self, refresh=False, generic_type=None):
        """Get all devices from Abode."""
        if refresh or self._devices is None:
            self._load_devices()

        spec_types = (
            Everything() if generic_type is None else set(always_iterable(generic_type))
        )

        return [
            device
            for device in self._devices.values()
            if device.generic_type in spec_types
        ]

    def _load_devices(self):
        if self._devices is None:
            self._devices = {}

        log.info("Updating all devices...")
        response = self.send_request("get", urls.DEVICES)
        devices = always_iterable(response.json())

        log.debug("Get Devices URL (get): %s", urls.AUTOMATION)
        log.debug("Get Devices Response: %s", response.text)

        consume(map(self._load_device, devices))

        # We will be treating the Abode panel itself as an armable device.
        panel_response = self.send_request("get", urls.PANEL)
        panel_json = panel_response.json()

        self._panel.update(panel_json)

        log.debug("Get Mode Panel URL (get): %s", urls.AUTOMATION)
        log.debug("Get Mode Panel Response: %s", response.text)

        alarm_device = self._devices.get(ALARM.id(1))

        if alarm_device:
            alarm_device.update(self._panel)
        else:
            alarm_device = ALARM.create_alarm(self._panel, self)
            self._devices[alarm_device.id] = alarm_device

    def _load_device(self, doc):
        self._reuse_device(doc) or self._create_new_device(doc)

    def _reuse_device(self, doc):
        device = self._devices.get(doc['id'])

        if not device:
            return

        device.update(doc)
        return device

    def _create_new_device(self, doc):
        device = Device.new(doc, self)

        if isinstance(device, Unknown):
            log.debug("Skipping unknown device: %s", doc)
            return

        self._devices[device.id] = device

    def get_device(self, device_id, refresh=False):
        """Get a single device."""
        if self._devices is None:
            self.get_devices()
            refresh = False

        device = self._devices.get(device_id)

        if device and refresh:
            device.refresh()

        return device

    def get_automations(self, refresh=False):
        """Get all automations."""
        if refresh or self._automations is None:
            self._update_all()

        return list(self._automations.values())

    def _update_all(self):
        if self._automations is None:
            # Set up the device libraries
            self._automations = {}

        log.info("Updating all automations...")
        resp = self.send_request("get", urls.AUTOMATION)
        log.debug("Get Automations URL (get): %s", urls.AUTOMATION)
        log.debug("Get Automations Response: %s", resp.text)

        for state in always_iterable(resp.json()):
            # Attempt to reuse an existing automation object
            automation = self._automations.get(str(state['id']))

            # No existing automation, create a new one
            if automation:
                automation.update(state)
            else:
                automation = Automation(state, self)
                self._automations[automation.id] = automation

    def get_automation(self, automation_id, refresh=False):
        """Get a single automation."""
        if self._automations is None:
            self.get_automations()
            refresh = False

        automation = self._automations.get(str(automation_id))

        if automation and refresh:
            automation.refresh()

        return automation

    def get_alarm(self, area='1', refresh=False):
        """Shortcut method to get the alarm device."""
        return self.get_device(ALARM.id(area), refresh)

    def set_default_mode(self, default_mode):
        """Set the default mode when alarms are turned 'on'."""
        if default_mode.lower() not in ('away', 'home'):
            raise jaraco.abode.Exception(ERROR.INVALID_DEFAULT_ALARM_MODE)

        self._default_alarm_mode = default_mode.lower()

    def set_setting(self, name, value, area='1'):
        """Set an abode system setting to a given value."""
        setting = settings.Setting.load(name.lower(), value, area)
        return self.send_request(method="put", path=setting.path, data=setting.data)

    def send_request(self, method, path, headers=None, data=None):
        """Send requests to Abode."""
        attempt = functools.partial(self._send_request, method, path, headers, data)
        return jaraco.functools.retry_call(
            attempt,
            retries=1,
            cleanup=self.login,
            trap=(jaraco.abode.Exception),
        )

    def _send_request(self, method, path, headers, data):
        if not self._token:
            self.login()

        if not headers:
            headers = {}

        headers['Authorization'] = 'Bearer ' + self._oauth_token
        headers['ABODE-API-KEY'] = self._token

        try:
            response = getattr(self._session, method)(path, headers=headers, json=data)

            if response and response.status_code < 400:
                return response
        except RequestException:
            log.info("Abode connection reset...")

        raise jaraco.abode.Exception(ERROR.REQUEST)

    @property
    def default_mode(self):
        """Get the default mode."""
        return self._default_alarm_mode

    @property
    def events(self):
        """Get the event controller."""
        return self._event_controller

    @property
    def uuid(self):
        """Get the UUID."""
        return self._session.cookies['uuid']

    def _get_session(self):
        # Perform a generic update so we know we're logged in
        self.send_request("get", urls.PANEL)

        return self._session
