import re
import logging

from itertools import groupby
from six import iteritems, string_types
from cached_property import cached_property_with_ttl
from six.moves.urllib.parse import unquote_plus

from saas.library.python.saas_slot import Slot
from saas.library.python.nanny_rest import NannyServiceBase, NannyApiError

from .client import DeployManagerApiClient


class SaasServiceConfig(object):

    def __init__(self, service, name, url):
        self._service = service
        self._name = name
        self._url = url

    @property
    def name(self):
        return self._name

    @cached_property_with_ttl(ttl=30)
    def content(self):
        return self._service.dm.get_config_content(
            self._url, self._service.name, self._service.ctype, service_type=self._service.service_type
        )


class SaasService(object):
    LOGGER = logging.getLogger(__name__)

    def __init__(self, ctype, service, service_type='rtyserver'):
        self._dm = DeployManagerApiClient()  # TODO(vbushev): pass in the constructor
        self._ctype = ctype
        self._service = service
        self._service_type = service_type

    def __eq__(self, other):
        if isinstance(other, SaasService):
            return self._ctype == other._ctype and self.name == other.name and self._service_type == other._service_type
        else:
            return NotImplemented

    def __ne__(self, other):
        if isinstance(other, SaasService):
            return self._ctype != other._ctype or self.name != other.name or self._service_type != other._service_type
        elif isinstance(other, string_types):
            return self.name != other
        else:
            return NotImplemented

    def __hash__(self):
        return hash((self._ctype, self._service, self._service_type))

    def __repr__(self):
        return 'SaasService({}, {})'.format(self._ctype, self._service)

    @property
    def dm(self):
        return self._dm

    @property
    def ctype(self):
        return self._ctype

    @property
    def service_type(self):
        return self._service_type

    @property
    def locations(self):
        return list(set([slot.geo for slot in self.slots]))

    @property
    def name(self):
        return self._service

    @property
    def prj_tag(self):
        if self.ctype.startswith('stable'):
            return '-'.join([self.ctype.replace('stable', 'saas'), self.name]).replace('_', '-')
        else:
            return 'saas-{}'.format(self.ctype)

    @cached_property_with_ttl(ttl=30)
    def shards(self):
        return self._dm.get_cluster_map(self._ctype, self._service).keys()

    @cached_property_with_ttl(ttl=30)
    def slots(self):
        # type: () -> List[Slot]
        all_slots = []
        for replicas in self._dm.slots_by_interval(self._ctype, self._service).values():
            all_slots.extend(replicas)
        return all_slots

    @cached_property_with_ttl(ttl=30)
    def slots_by_interval(self, **kwargs):
        return self._dm.get_slots_by_interval(self._ctype, self._service, **kwargs)

    @property
    def free_slots(self):
        return self._dm.get_free_slots(ctype=self._ctype, service=self._service)

    def get_search_map(self):
        return self._dm.search_map(self.ctype, self._service)

    def get_slots_on_hosts(self, hostlist):
        result = []
        host_set = set(filter(None, hostlist))
        for replicas in self._dm.slots_by_interval(self._ctype, self._service).values():
            result.extend([s for s in replicas if s.host in host_set or s.physical_host in host_set])
        return result

    @cached_property_with_ttl(ttl=30)
    def sla_info(self):
        return self._dm.get_sla(self._ctype, self._service)

    @cached_property_with_ttl(ttl=30)
    def tags_info(self):
        return self._dm.get_tags_info(self._ctype, self._service)

    @property
    def nanny_services(self):
        return [x.split('@')[0] for x in self.tags_info.get('nanny_services', [])]

    @cached_property_with_ttl(ttl=60)
    def slots_nanny_services(self):
        return set(self._dm.get_slots_nanny_services(self.ctype, self.name))

    def update_tags_info_nanny(self, nanny_services, resolve_containers=True):
        return self._dm.modify_tags_info(self._ctype, self._service, {'nanny_services': ','.join(nanny_services), 'use_container_names': resolve_containers})

    @cached_property_with_ttl(ttl=30)
    def gencfg_groups(self):
        # type: () -> Set[saas_slot.gencfg.GencfgGroup]
        return self.get_gencfg_groups(names_only=False)

    @cached_property_with_ttl(ttl=30)
    def gencfg_groups_names(self):
        # type: () -> Set[str]
        return self.get_gencfg_groups(names_only=True)

    def get_gencfg_groups(self, names_only=False):
        # type: (bool) -> (Set[str], Set[saas_slot.gencfg.GencfgGroup])
        all_groups = set()
        for nanny_service_name in self.nanny_services:
            try:
                nanny_service = NannyServiceBase(nanny_service_name)
                if not nanny_service.is_gencfg_allocated():
                    continue
                all_groups |= nanny_service.get_gencfg_groups(no_raise=True, names_only=names_only)
            except NannyApiError:
                self.LOGGER.critical('Saas service %s%:%s has invalid nanny service %s', self.ctype, self.name, nanny_service_name)
        return all_groups

    @cached_property_with_ttl(ttl=5)
    def per_dc_search(self):
        return self._dm.get_per_dc_search(self._ctype, self._service)

    def get_free_slots(self, geo):
        return self._dm.get_free_slots_in_geo(ctype=self._ctype, service=self._service, geo=geo)

    def get_config_diff(self, kind='rtyserver'):
        path = 'configs/{}/{}.diff-{}'.format(self._service, kind, self._service)
        return self._dm.get_storage_file(path=path)

    def duplicate_slots(self, slots, new_slots_pool=None):
        # type: (List[saas_slot.Slot], List[saas_slot.Slot]) -> List[saas_slot.Slot]
        """
        :param slots: List of slots to duplicate
        :param new_slots_pool: List of free slots
        :type slots: List[Slot]
        :type new_slots_pool: List[Slot]
        """
        result = []
        for geo, slots in groupby(slots, lambda sl: sl.geo):
            intervals = [s.interval for s in slots]
            free_slots_loc = [fs for fs in new_slots_pool if fs.geo == geo] if new_slots_pool else None
            added_slots = self._dm.allocate_same_slots(ctype=self._ctype, service=self._service, geo=geo, intervals=intervals, new_slots_pool=free_slots_loc)
            for slot in added_slots:
                slot.restart()
                slot.disable_search()
            result.extend(added_slots)
        return result

    def get_release_branch(self):
        info_server = self.sample_info_server()
        svn_root = info_server['Svn_root']
        branch_candidates = re.findall(r'.*branches/saas/([\d.]+)/.*', svn_root)
        assert len(branch_candidates) == 1, "Can't guess branch from svn_root {}".format(svn_root)
        return branch_candidates[0]

    def clear_dm_cache(self):
        self._dm.clear_cache(self.ctype, self.name)

    def modify_searchmap(self, slots, action, **kwargs):
        return self._dm.modify_searchmap(ctype=self._ctype, service=self._service, slots=slots, action=action, **kwargs)

    def set_searchmap(self, slots=None, search_enabled=None, indexing_enabled=None, **kwargs):
        actions = []
        if slots is None:
            slots = self.slots
        if search_enabled is not None:
            if search_enabled:
                actions.append('enable_search')
            else:
                actions.append('disable_search')
        if indexing_enabled is not None:
            if indexing_enabled:
                actions.append('enable_indexing')
            else:
                actions.append('disable_indexing')
        for action in actions:
            self.modify_searchmap(slots, action, **kwargs)

    def release_slots(self, slots):
        self._dm.release_slots(self._ctype, self._service, slots)

    def check_configs(self):
        def has_diff(file_info):
            return not (file_info.get('from_host', None) == file_info['last_deployed'] == file_info['last_stored'])

        def has_configs_diff(configs_info):
            if any([has_diff(file_info) for file_name, file_info in iteritems(configs_info) if file_name != 'description']):
                return True
            else:
                return False

        slots_info = self._dm.check_configs(self.ctype, self._service)[self._service]
        return {
            Slot.from_id(slot_id): {config_file: deploy_info for config_file, deploy_info in iteritems(files_info) if has_diff(deploy_info)}
            for slot_id, files_info in iteritems(slots_info)
            if has_configs_diff(files_info)
        }

    @cached_property_with_ttl(ttl=30)
    def configs(self):
        result = []
        dm_configs = self._dm.get_configs(self._service, self._ctype, service_type=self._service_type)
        for dm_config in dm_configs:
            result.append(
                SaasServiceConfig(
                    self,
                    dm_config['rename'],
                    unquote_plus(dm_config['url'])
                )
            )
        return result

    def get_config(self, config_name):
        for config in self.configs:
            if config.name == config_name:
                return config

        raise ValueError('Unable to find config {name}'.format(name=config_name))
