import asyncio
import os.path
from importlib import import_module

import yaml
from structlog import get_logger

from .state import State
from .action_descriptor import ActionDescriptor
from .schedule import Schedule


MAIN_CONFIG_FILE = 'config.yaml'


def import_class(class_str):
    module_name, class_name = class_str.rsplit('.', 1)
    module = import_module(module_name)
    return getattr(module, class_name)


def expand_actions(action_descriptors, actions):
    result = []
    if not isinstance(actions, (list, tuple)):
        actions = [actions]
    for action in actions:
        if not isinstance(action, dict):
            action = {'id': action}
        action_id = action.pop('id')
        action_data = action
        result.append(action_descriptors[action_id].clone(**action_data))
    return result


class Application:

    def __init__(self, exception_handler=None):
        self._exception_handler = exception_handler
        self.sensors = {}
        self.states = {}
        self.action_descriptors = {}
        self.state = None
        self.logger = get_logger()
        self.loop = asyncio.get_event_loop()
        self._app_run_future = None
        self.config_path = None

    async def load_config(self, config_path,
                          default_state_factory=State,
                          default_action_descriptor_factory=ActionDescriptor,
                          default_schedule_factory=Schedule):
        with open(os.path.join(config_path, MAIN_CONFIG_FILE)) as config_file:
            config = yaml.load(config_file)
        self.config_path = config_path

        for action_id, action_data in config.get('actions', {}).items():
            abstract = action_data.pop('abstract', False)
            if not abstract:
                action_class = import_class(action_data.pop('class'))
                action = default_action_descriptor_factory(self, action_id, action_class, **action_data)
                self.add_action_descriptor(action_id, action)

        for sensor_id, sensor_data in config.get('sensors', {}).items():
            sensor_class = import_class(sensor_data.pop('class'))
            behaviours = sensor_data.pop('behaviours', {})
            sensor = sensor_class(self, sensor_id, **sensor_data)
            for code, actions in behaviours.items():
                for action_descriptor in expand_actions(self.action_descriptors, actions):
                    sensor.add_behaviour(code, action_descriptor)
            self.add_sensor(sensor_id, sensor)

        for state_id, state_data in config.get('states', {}).items():
            state_class = default_state_factory
            sensors = {}
            behaviours = []
            for sensor in state_data.pop('sensors', []):
                if isinstance(sensor, dict):
                    sensor_id = sensor['id']
                    for behaviour_code, actions in sensor['behaviours'].items():
                        for action_descriptor in expand_actions(self.action_descriptors, actions):
                            behaviours.append((sensor_id, behaviour_code, action_descriptor))
                else:
                    sensor_id = sensor
                sensors[sensor_id] = self.sensors[sensor_id]
            schedules_data = state_data.pop('schedules', {})
            state = state_class(self, state_id, sensors, **state_data)
            for behaviour in behaviours:
                state.add_behaviour(*behaviour)
            for schedule_id, schedule_data in schedules_data.items():
                schedule_class = default_schedule_factory
                actions = schedule_data.pop('actions')
                schedule = schedule_class(self, schedule_id, state, **schedule_data)
                for action_descriptor in expand_actions(self.action_descriptors, actions):
                    schedule.add_action(action_descriptor)
                state.add_schedule(schedule_id, schedule)
            self.add_state(state_id, state)

        await self.set_state(self.states[config['initial_state']])

    def exception_handler(self, exception):
        if self._exception_handler:
            return self._exception_handler(exception)
        raise exception

    def add_sensor(self, id_, sensor):
        self.sensors[id_] = sensor

    def add_state(self, id_, state):
        self.states[id_] = state

    def add_action_descriptor(self, id_, action_descriptor):
        self.action_descriptors[id_] = action_descriptor

    async def set_state(self, state):
        real = self.state != state or state.reactivatable
        self.logger.info('set_state', state=state.id, old_state=self.state.id if self.state else None, ignore=not real)
        if real:
            if self.state:
                await self.state.deactivate()
            self.state = state
            await self.state.activate()

    async def run(self):
        self.logger.info('application_start')
        sensor_tasks = [asyncio.ensure_future(sensor.run_forever())
                        for sensor in self.sensors.values()]
        self._app_run_future = asyncio.Future()
        await self._app_run_future
        self._app_run_future = None
        for sensor in self.sensors.values():
            sensor.stop()
        await asyncio.wait(sensor_tasks)
        if self.state:
            await self.state.deactivate()
        self.logger.info('application_end')

    def stop(self):
        self.logger.info('application_stop')
        if self._app_run_future:
            self._app_run_future.set_result(None)

    async def notify(self, sensor, code):
        if self.state:
            return await self.state.notify(sensor, code)
        self.logger.info('notify_ignore', reason='no_active_state')
