from __future__ import absolute_import
from __future__ import unicode_literals

import logging

import six
from six.moves import filter

from tron import event
from tron import eventloop
from tron import node
from tron.core import serviceinstance
from tron.core.serviceinstance import ServiceInstance
from tron.utils import collections
from tron.utils import observer


log = logging.getLogger(__name__)


class ServiceState(object):
    """Determine the state of a Service."""
    DISABLED = "disabled"
    STARTING = "starting"
    UP = "up"
    DEGRADED = "degraded"
    FAILED = "failed"
    STOPPING = "stopping"
    UNKNOWN = "unknown"

    FAILURE_STATES = {DEGRADED, FAILED}

    @classmethod
    def from_service(cls, service):
        if not service.enabled:
            return cls.disabled_states(service)

        if service.instances.is_up():
            return cls.UP

        if service.instances.is_starting():
            return cls.STARTING

        if service.instances.all(ServiceInstance.STATE_FAILED):
            return cls.FAILED

        return cls.DEGRADED

    @classmethod
    def disabled_states(cls, service):
        if not len(service.instances):
            return cls.DISABLED

        if service.instances.all(ServiceInstance.STATE_STOPPING):
            return cls.STOPPING

        return cls.UNKNOWN


class Service(observer.Observer, observer.Observable):
    """Manage a collection of service instances."""

    NOTIFY_STATE_CHANGE = 'event_state_changed'

    def __init__(self, config, instance_collection):
        super(Service, self).__init__()
        self.config = config
        self.instances = instance_collection
        self.enabled = False
        args = config.restart_delay, self.repair
        self.repair_callback = eventloop.UniqueCallback(*args)
        self.event_recorder = event.get_recorder(str(self))

    @classmethod
    def from_config(cls, config, base_context):
        node_repo = node.NodePoolRepository.get_instance()
        node_pool = node_repo.get_by_name(config.node)
        args = config, node_pool, base_context
        instance_collection = serviceinstance.ServiceInstanceCollection(*args)
        return cls(config, instance_collection)

    def get_name(self):
        return self.config.name

    name = property(get_name)

    def get_state(self):
        return ServiceState.from_service(self)

    def enable(self):
        """Enable the service."""
        self.enabled = True
        self.event_recorder.ok('enabled')
        self.repair()

    def disable(self, force=False):
        self.enabled = False
        (self.instances.kill if force else self.instances.stop)()
        self.repair_callback.cancel()
        self.event_recorder.ok('disabled')

    def repair(self):
        """Repair the service by restarting instances."""
        self.instances.clear_failed()
        self.instances.restore()
        self.watch_instances(self.instances.create_missing())
        self.notify(self.NOTIFY_STATE_CHANGE)
        self.event_recorder.ok('repairing')
        self.instances.start()

    def _handle_instance_state_change(self, _instance, event):
        """Handle any changes to the state of this service's instances."""
        if event == serviceinstance.ServiceInstance.STATE_DOWN:
            self.instances.clear_down()
            self.notify(self.NOTIFY_STATE_CHANGE)

        if event in (
            serviceinstance.ServiceInstance.STATE_FAILED,
            serviceinstance.ServiceInstance.STATE_UP,
        ):
            self.record_events()

        if self.get_state() in ServiceState.FAILURE_STATES:
            log.info("Starting service repair for %s", self)
            self.repair_callback.start()

    handler = _handle_instance_state_change

    def record_events(self):
        """Record an event when the state changes."""
        state = self.get_state()
        if state in ServiceState.FAILURE_STATES:
            return self.event_recorder.critical(state)

        if state == ServiceState.UP:
            return self.event_recorder.ok(state)

    @property
    def state_data(self):
        """Data used to serialize the state of this service."""
        return dict(enabled=self.enabled, instances=self.instances.state_data)

    def __eq__(self, other):
        if other is None or not isinstance(other, Service):
            return False

        return self.config == other.config and self.instances == other.instances

    def __ne__(self, other):
        return not self == other

    def __str__(self):
        return "Service:%s" % self.name

    def watch_instances(self, instances):
        self.watch_all(instance.get_observable() for instance in instances)

    def restore_state(self, state_data):
        instances = self.instances.restore_state(state_data['instances'])
        self.watch_instances(instances)

        (self.enable if state_data.get('enabled') else self.disable)()
        self.event_recorder.info("restored")


class ServiceCollection(object):
    """A collection of services."""

    def __init__(self):
        self.services = collections.MappingCollection('services')

    def load_from_config(self, service_configs, context):
        """Apply a configuration to this collection and return a generator of
        services which were added.
        """
        self.services.filter_by_name(service_configs.keys())

        def build(config):
            log.debug("Building new service %s", config.name)
            return Service.from_config(config, context)

        seq = (build(config) for config in six.itervalues(service_configs))
        return filter(self.add, seq)

    def add(self, service):
        return self.services.replace(service)

    def restore_state(self, service_state_data):
        self.services.restore_state(service_state_data)

    def get_by_name(self, name):
        return self.services.get(name)

    def get_names(self):
        return self.services.keys()

    def __iter__(self):
        return six.itervalues(self.services)
