"""Support for Google - Calendar Event Devices."""
from datetime import timedelta, datetime
import logging
import os
import yaml

import voluptuous as vol
from voluptuous.error import Error as VoluptuousError

import homeassistant.helpers.config_validation as cv
from homeassistant.setup import setup_component
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers.event import track_time_change
from homeassistant.util import convert, dt

_LOGGER = logging.getLogger(__name__)

DOMAIN = 'google'
ENTITY_ID_FORMAT = DOMAIN + '.{}'

CONF_CLIENT_ID = 'client_id'
CONF_CLIENT_SECRET = 'client_secret'
CONF_TRACK_NEW = 'track_new_calendar'

CONF_CAL_ID = 'cal_id'
CONF_DEVICE_ID = 'device_id'
CONF_NAME = 'name'
CONF_ENTITIES = 'entities'
CONF_TRACK = 'track'
CONF_SEARCH = 'search'
CONF_OFFSET = 'offset'
CONF_IGNORE_AVAILABILITY = 'ignore_availability'
CONF_MAX_RESULTS = 'max_results'

DEFAULT_CONF_TRACK_NEW = True
DEFAULT_CONF_OFFSET = '!!'

EVENT_CALENDAR_ID = 'calendar_id'
EVENT_DESCRIPTION = 'description'
EVENT_END_CONF = 'end'
EVENT_END_DATE = 'end_date'
EVENT_END_DATETIME = 'end_date_time'
EVENT_IN = 'in'
EVENT_IN_DAYS = 'days'
EVENT_IN_WEEKS = 'weeks'
EVENT_START_CONF = 'start'
EVENT_START_DATE = 'start_date'
EVENT_START_DATETIME = 'start_date_time'
EVENT_SUMMARY = 'summary'
EVENT_TYPES_CONF = 'event_types'

NOTIFICATION_ID = 'google_calendar_notification'
NOTIFICATION_TITLE = "Google Calendar Setup"
GROUP_NAME_ALL_CALENDARS = "Google Calendar Sensors"

SERVICE_SCAN_CALENDARS = 'scan_for_calendars'
SERVICE_FOUND_CALENDARS = 'found_calendar'
SERVICE_ADD_EVENT = 'add_event'

DATA_INDEX = 'google_calendars'

YAML_DEVICES = '{}_calendars.yaml'.format(DOMAIN)
SCOPES = 'https://www.googleapis.com/auth/calendar'

TOKEN_FILE = '.{}.token'.format(DOMAIN)

CONFIG_SCHEMA = vol.Schema({
    DOMAIN: vol.Schema({
        vol.Required(CONF_CLIENT_ID): cv.string,
        vol.Required(CONF_CLIENT_SECRET): cv.string,
        vol.Optional(CONF_TRACK_NEW): cv.boolean,
    })
}, extra=vol.ALLOW_EXTRA)

_SINGLE_CALSEARCH_CONFIG = vol.Schema({
    vol.Required(CONF_NAME): cv.string,
    vol.Required(CONF_DEVICE_ID): cv.string,
    vol.Optional(CONF_IGNORE_AVAILABILITY, default=True): cv.boolean,
    vol.Optional(CONF_OFFSET): cv.string,
    vol.Optional(CONF_SEARCH): cv.string,
    vol.Optional(CONF_TRACK): cv.boolean,
    vol.Optional(CONF_MAX_RESULTS): cv.positive_int,
})

DEVICE_SCHEMA = vol.Schema({
    vol.Required(CONF_CAL_ID): cv.string,
    vol.Required(CONF_ENTITIES, None):
        vol.All(cv.ensure_list, [_SINGLE_CALSEARCH_CONFIG]),
}, extra=vol.ALLOW_EXTRA)

_EVENT_IN_TYPES = vol.Schema(
    {
        vol.Exclusive(EVENT_IN_DAYS, EVENT_TYPES_CONF): cv.positive_int,
        vol.Exclusive(EVENT_IN_WEEKS, EVENT_TYPES_CONF): cv.positive_int,
    }
)

ADD_EVENT_SERVICE_SCHEMA = vol.Schema(
    {
        vol.Required(EVENT_CALENDAR_ID): cv.string,
        vol.Required(EVENT_SUMMARY): cv.string,
        vol.Optional(EVENT_DESCRIPTION, default=""): cv.string,
        vol.Exclusive(EVENT_START_DATE, EVENT_START_CONF): cv.date,
        vol.Exclusive(EVENT_END_DATE, EVENT_END_CONF): cv.date,
        vol.Exclusive(EVENT_START_DATETIME, EVENT_START_CONF): cv.datetime,
        vol.Exclusive(EVENT_END_DATETIME, EVENT_END_CONF): cv.datetime,
        vol.Exclusive(EVENT_IN, EVENT_START_CONF, EVENT_END_CONF):
        _EVENT_IN_TYPES
    }
)


def do_authentication(hass, hass_config, config):
    """Notify user of actions and authenticate.

    Notify user of user_code and verification_url then poll
    until we have an access token.
    """
    from oauth2client.client import (
        OAuth2WebServerFlow, OAuth2DeviceCodeError, FlowExchangeError)
    from oauth2client.file import Storage

    oauth = OAuth2WebServerFlow(
        client_id=config[CONF_CLIENT_ID],
        client_secret=config[CONF_CLIENT_SECRET],
        scope='https://www.googleapis.com/auth/calendar',
        redirect_uri='Home-Assistant.io',
    )
    try:
        dev_flow = oauth.step1_get_device_and_user_codes()
    except OAuth2DeviceCodeError as err:
        hass.components.persistent_notification.create(
            'Error: {}<br />You will need to restart hass after fixing.'
            ''.format(err),
            title=NOTIFICATION_TITLE,
            notification_id=NOTIFICATION_ID)
        return False

    hass.components.persistent_notification.create(
        'In order to authorize Home-Assistant to view your calendars '
        'you must visit: <a href="{}" target="_blank">{}</a> and enter '
        'code: {}'.format(dev_flow.verification_url,
                          dev_flow.verification_url,
                          dev_flow.user_code),
        title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID
    )

    def step2_exchange(now):
        """Keep trying to validate the user_code until it expires."""
        if now >= dt.as_local(dev_flow.user_code_expiry):
            hass.components.persistent_notification.create(
                'Authentication code expired, please restart '
                'Home-Assistant and try again',
                title=NOTIFICATION_TITLE,
                notification_id=NOTIFICATION_ID)
            listener()

        try:
            credentials = oauth.step2_exchange(device_flow_info=dev_flow)
        except FlowExchangeError:
            # not ready yet, call again
            return

        storage = Storage(hass.config.path(TOKEN_FILE))
        storage.put(credentials)
        do_setup(hass, hass_config, config)
        listener()
        hass.components.persistent_notification.create(
            'We are all setup now. Check {} for calendars that have '
            'been found'.format(YAML_DEVICES),
            title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID)

    listener = track_time_change(
        hass, step2_exchange, second=range(0, 60, dev_flow.interval))

    return True


def setup(hass, config):
    """Set up the Google platform."""
    if DATA_INDEX not in hass.data:
        hass.data[DATA_INDEX] = {}

    conf = config.get(DOMAIN, {})
    if not conf:
        # component is set up by tts platform
        return True

    token_file = hass.config.path(TOKEN_FILE)
    if not os.path.isfile(token_file):
        do_authentication(hass, config, conf)
    else:
        if not check_correct_scopes(token_file):
            do_authentication(hass, config, conf)
        else:
            do_setup(hass, config, conf)

    return True


def check_correct_scopes(token_file):
    """Check for the correct scopes in file."""
    tokenfile = open(token_file, "r").read()
    if "readonly" in tokenfile:
        _LOGGER.warning("Please re-authenticate with Google.")
        return False
    return True


def setup_services(hass, hass_config, track_new_found_calendars,
                   calendar_service):
    """Set up the service listeners."""
    def _found_calendar(call):
        """Check if we know about a calendar and generate PLATFORM_DISCOVER."""
        calendar = get_calendar_info(hass, call.data)
        if hass.data[DATA_INDEX].get(calendar[CONF_CAL_ID], None) is not None:
            return

        hass.data[DATA_INDEX].update({calendar[CONF_CAL_ID]: calendar})

        update_config(
            hass.config.path(YAML_DEVICES),
            hass.data[DATA_INDEX][calendar[CONF_CAL_ID]]
        )

        discovery.load_platform(hass, 'calendar', DOMAIN,
                                hass.data[DATA_INDEX][calendar[CONF_CAL_ID]],
                                hass_config)

    hass.services.register(
        DOMAIN, SERVICE_FOUND_CALENDARS, _found_calendar)

    def _scan_for_calendars(service):
        """Scan for new calendars."""
        service = calendar_service.get()
        cal_list = service.calendarList()
        calendars = cal_list.list().execute()['items']
        for calendar in calendars:
            calendar['track'] = track_new_found_calendars
            hass.services.call(DOMAIN, SERVICE_FOUND_CALENDARS,
                               calendar)

    hass.services.register(
        DOMAIN, SERVICE_SCAN_CALENDARS, _scan_for_calendars)

    def _add_event(call):
        """Add a new event to calendar."""
        service = calendar_service.get()
        start = {}
        end = {}

        if EVENT_IN in call.data:
            if EVENT_IN_DAYS in call.data[EVENT_IN]:
                now = datetime.now()

                start_in = now + timedelta(
                    days=call.data[EVENT_IN][EVENT_IN_DAYS])
                end_in = start_in + timedelta(days=1)

                start = {'date': start_in.strftime('%Y-%m-%d')}
                end = {'date': end_in.strftime('%Y-%m-%d')}

            elif EVENT_IN_WEEKS in call.data[EVENT_IN]:
                now = datetime.now()

                start_in = now + timedelta(
                    weeks=call.data[EVENT_IN][EVENT_IN_WEEKS])
                end_in = start_in + timedelta(days=1)

                start = {'date': start_in.strftime('%Y-%m-%d')}
                end = {'date': end_in.strftime('%Y-%m-%d')}

        elif EVENT_START_DATE in call.data:
            start = {'date': str(call.data[EVENT_START_DATE])}
            end = {'date': str(call.data[EVENT_END_DATE])}

        elif EVENT_START_DATETIME in call.data:
            start_dt = str(call.data[EVENT_START_DATETIME]
                           .strftime('%Y-%m-%dT%H:%M:%S'))
            end_dt = str(call.data[EVENT_END_DATETIME]
                         .strftime('%Y-%m-%dT%H:%M:%S'))
            start = {'dateTime': start_dt,
                     'timeZone': str(hass.config.time_zone)}
            end = {'dateTime': end_dt,
                   'timeZone': str(hass.config.time_zone)}

        event = {
            'summary': call.data[EVENT_SUMMARY],
            'description': call.data[EVENT_DESCRIPTION],
            'start': start,
            'end': end,
        }
        service_data = {'calendarId': call.data[EVENT_CALENDAR_ID],
                        'body': event}
        event = service.events().insert(**service_data).execute()

    hass.services.register(
        DOMAIN, SERVICE_ADD_EVENT, _add_event, schema=ADD_EVENT_SERVICE_SCHEMA
    )
    return True


def do_setup(hass, hass_config, config):
    """Run the setup after we have everything configured."""
    # Load calendars the user has configured
    hass.data[DATA_INDEX] = load_config(hass.config.path(YAML_DEVICES))

    calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE))
    track_new_found_calendars = convert(config.get(CONF_TRACK_NEW),
                                        bool, DEFAULT_CONF_TRACK_NEW)
    setup_services(hass, hass_config, track_new_found_calendars,
                   calendar_service)

    # Ensure component is loaded
    setup_component(hass, 'calendar', config)

    for calendar in hass.data[DATA_INDEX].values():
        discovery.load_platform(hass, 'calendar', DOMAIN, calendar,
                                hass_config)

    # Look for any new calendars
    hass.services.call(DOMAIN, SERVICE_SCAN_CALENDARS, None)
    return True


class GoogleCalendarService:
    """Calendar service interface to Google."""

    def __init__(self, token_file):
        """Init the Google Calendar service."""
        self.token_file = token_file

    def get(self):
        """Get the calendar service from the storage file token."""
        import httplib2
        from oauth2client.file import Storage
        from googleapiclient import discovery as google_discovery
        credentials = Storage(self.token_file).get()
        http = credentials.authorize(httplib2.Http())
        service = google_discovery.build(
            'calendar', 'v3', http=http, cache_discovery=False)
        return service


def get_calendar_info(hass, calendar):
    """Convert data from Google into DEVICE_SCHEMA."""
    calendar_info = DEVICE_SCHEMA({
        CONF_CAL_ID: calendar['id'],
        CONF_ENTITIES: [{
            CONF_TRACK: calendar['track'],
            CONF_NAME: calendar['summary'],
            CONF_DEVICE_ID: generate_entity_id(
                '{}', calendar['summary'], hass=hass),
        }]
    })
    return calendar_info


def load_config(path):
    """Load the google_calendar_devices.yaml."""
    calendars = {}
    try:
        with open(path) as file:
            data = yaml.safe_load(file)
            for calendar in data:
                try:
                    calendars.update({calendar[CONF_CAL_ID]:
                                      DEVICE_SCHEMA(calendar)})
                except VoluptuousError as exception:
                    # keep going
                    _LOGGER.warning("Calendar Invalid Data: %s", exception)
    except FileNotFoundError:
        # When YAML file could not be loaded/did not contain a dict
        return {}

    return calendars


def update_config(path, calendar):
    """Write the google_calendar_devices.yaml."""
    with open(path, 'a') as out:
        out.write('\n')
        yaml.dump([calendar], out, default_flow_style=False)