import textwrap
from functools import partial

from django.apps import apps
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _

from orchestra import plugins

from . import methods


def replace(context, pattern, repl):
    """ applies replace to all context str values """
    for key, value in context.items():
        if isinstance(value, str):
            context[key] = value.replace(pattern, repl)
    return context


class ServiceMount(plugins.PluginMount):
    def __init__(cls, name, bases, attrs):
        # Make sure backends specify a model attribute
        if not (attrs.get('abstract', False) or name == 'ServiceBackend' or cls.model):
            raise AttributeError("'%s' does not have a defined model attribute." % cls)
        super(ServiceMount, cls).__init__(name, bases, attrs)


class ServiceBackend(plugins.Plugin, metaclass=ServiceMount):
    """
    Service management backend base class
    
    It uses the _unit of work_ design principle, which allows bulk operations to
    be conviniently supported. Each backend generates the configuration for all
    the changes of all modified objects, reloading the daemon just once.
    """
    model = None
    related_models = ()  # ((model, accessor__attribute),)
    script_method = methods.SSH
    script_executable = '/bin/bash'
    function_method = methods.Python
    type = 'task'  # 'sync'
    # Don't wait for the backend to finish before continuing with request/response
    ignore_fields = []
    actions = []
    default_route_match = 'True'
    # Force the backend manager to block in multiple backend executions executing them synchronously
    serialize = False
    doc_settings = None
    # By default backend will not run if actions do not generate insctructions,
    # If your backend uses prepare() or commit() only then you should set force_empty_action_execution = True
    force_empty_action_execution = False
    
    def __str__(self):
        return type(self).__name__
    
    def __init__(self):
        self.head = []
        self.content = []
        self.tail = []
    
    def __getattribute__(self, attr):
        """ Select head, content or tail section depending on the method name """
        IGNORE_ATTRS = (
            'append',
            'cmd_section',
            'head',
            'tail',
            'content',
            'script_method',
            'function_method',
            'set_head',
            'set_tail',
            'set_content',
            'actions',
        )
        if attr == 'prepare':
            self.set_head()
        elif attr == 'commit':
            self.set_tail()
        elif attr not in IGNORE_ATTRS and attr in self.actions:
            self.set_content()
        return super(ServiceBackend, self).__getattribute__(attr)
    
    def set_head(self):
        self.cmd_section = self.head
    
    def set_tail(self):
        self.cmd_section = self.tail
    
    def set_content(self):
        self.cmd_section = self.content
    
    @classmethod
    def get_actions(cls):
        return [ action for action in cls.actions if action in dir(cls) ]
    
    @classmethod
    def get_name(cls):
        return cls.__name__
    
    @classmethod
    def is_main(cls, obj):
        opts = obj._meta
        return cls.model == '%s.%s' % (opts.app_label, opts.object_name)
    
    @classmethod
    def get_related(cls, obj):
        opts = obj._meta
        model = '%s.%s' % (opts.app_label, opts.object_name)
        for rel_model, field in cls.related_models:
            if rel_model == model:
                related = obj
                for attribute in field.split('__'):
                    related = getattr(related, attribute)
                return related
        return None
    
    @classmethod
    def get_backends(cls, instance=None, action=None):
        backends = cls.get_plugins()
        included = []
        # Filter for instance or action
        for backend in backends:
            include = True
            if instance:
                opts = instance._meta
                if backend.model != '.'.join((opts.app_label, opts.object_name)):
                    include = False
            if include and action:
                if action not in backend.get_actions():
                    include = False
            if include:
                included.append(backend)
        return included
    
    @classmethod
    def get_backend(cls, name):
        return cls.get(name)
    
    @classmethod
    def model_class(cls):
        return apps.get_model(cls.model)
    
    @property
    def scripts(self):
        """ group commands based on their method """
        if not self.content:
            return []
        scripts = {}
        for method, cmd in self.content:
            scripts[method] = []
        for method, commands in self.head + self.content + self.tail:
            try:
                scripts[method] += commands
            except KeyError:
                pass
        return list(scripts.items())
    
    def get_banner(self):
        now = timezone.localtime(timezone.now())
        time = now.strftime("%h %d, %Y %I:%M:%S %Z")
        return "Generated by Orchestra at %s" % time
    
    def create_log(self, server, **kwargs):
        from .models import BackendLog
        state = BackendLog.RECEIVED
        run = bool(self.scripts) or (self.force_empty_action_execution or bool(self.content))
        if not run:
            state = BackendLog.NOTHING
        using = kwargs.pop('using', None)
        manager = BackendLog.objects
        if using:
            manager = manager.using(using)
        log = manager.create(backend=self.get_name(), state=state, server=server)
        return log
    
    def execute(self, server, async=False, log=None):
        from .models import BackendLog
        if log is None:
            log = self.create_log(server)
        run = log.state != BackendLog.NOTHING
        if run:
            scripts = self.scripts
            for method, commands in scripts:
                method(log, server, commands, async)
                if log.state != BackendLog.SUCCESS:
                    break
        return log
    
    def append(self, *cmd):
        # aggregate commands acording to its execution method
        if isinstance(cmd[0], str):
            method = self.script_method
            cmd = cmd[0]
        else:
            method = self.function_method
            cmd = partial(*cmd)
        if not self.cmd_section or self.cmd_section[-1][0] != method:
            self.cmd_section.append((method, [cmd]))
        else:
            self.cmd_section[-1][1].append(cmd)
    
    def get_context(self, obj):
        return {}
    
    def prepare(self):
        """
        hook for executing something at the beging
        define functions or initialize state
        """
        self.append(textwrap.dedent("""\
            set -e
            set -o pipefail
            exit_code=0""")
        )
    
    def commit(self):
        """
        hook for executing something at the end
        apply the configuration, usually reloading a service
        reloading a service is done in a separated method in order to reload
        the service once in bulk operations
        """
        self.append('exit $exit_code')


class ServiceController(ServiceBackend):
    actions = ('save', 'delete')
    abstract = True
    
    @classmethod
    def get_verbose_name(cls):
        return _("[S] %s") % super(ServiceController, cls).get_verbose_name()
    
    @classmethod
    def get_backends(cls):
        """ filter controller classes """
        backends = super(ServiceController, cls).get_backends()
        return [
            backend for backend in backends if issubclass(backend, ServiceController)
        ]