import logging
import time
import re
import random

from threading import Thread  # TBD: Replace it with threadpool executor when 3x comes
from typing import List, AnyStr  # noqa

from saas.tools.devops.lib23.saas_service import SaasService
from saas.tools.ssm.modules.dm_api import DeployManagerAPI, request
from saas.tools.ssm.modules.gencfg_api import GencfgGroupRequest
from saas.tools.devops.lib23.ask_for_user_action import ask_yes_or_no
from saas.tools.devops.lib.saas_recluster import recluster_phase_one, recluster_phase_two, wait_for_snaphsot_active

LOGGER = logging.getLogger(__name__)


def host_specification(host, domain_suffix='search.yandex.net'):

    s = re.findall("(.*)\.{}$".format(domain_suffix), host)
    if len(s) == 1:
        short_host_name = s[0]
        full_host_name = "{}.{}".format(s[0], domain_suffix)
    elif len(s) == 0:
        full_host_name = "{}.{}".format(host, domain_suffix)
        short_host_name = host

    return full_host_name, short_host_name


class Host(object):
    def __init__(self):
        pass


class DeployManagerSlotManipulator(DeployManagerAPI):
    def __init__(self, service_name, ctype):
        DeployManagerAPI.__init__(self, service_name, ctype)

        self.__service_name = service_name
        self.__ctype = ctype

    # TBD : get rid of this
    @request
    def dm_api_request(self, url, append_service_specification=False):
        request_url = self._DM_HOST + url
        if append_service_specification:
            request_url += '&ctype={}&service={}'.format(self.__ctype, self.__service_name)
        logging.debug(request_url)
        return self._connection.get(request_url)

    # TBD : expose it to parent DeployManagerAPI
    def dm_cache_clear(self):
        # curl 'http://saas-dm.yandex.net/modify_tags_info?action=invalidate&service_type=rtyserver&ctype=prestable&service=test_i024'
        request_url = '/modify_tags_info?action=invalidate&service_type=rtyserver'
        return self.dm_api_request(request_url, append_service_specification=True)

    def wait_for_dm_task(self, task_id):

        sleep_delay = 0

        while True:
            r_status = self.dm_api_request("/deploy_info?id={}".format(task_id), append_service_specification=True)
            if r_status.ok and r_status.json()['is_finished']:
                if r_status.json()['result_code'] == 'FINISHED':
                    return True
                else:
                    logging.error("DM task %s failed", task_id)
                    return False
            sleep_delay = min(sleep_delay + 1, 60)
            time.sleep(sleep_delay)

    def deploy_indexerproxy(self, degrade_level=0.15):
        return self.deploy_proxy('indexerproxy', degrade_level)

    def deploy_searchproxy(self, degrade_level=0.015):
        return self.deploy_proxy('searchproxy', degrade_level)

    def deploy_proxies(self, degrade_level=0.015):
        deploy_sp = Thread(target=self.deploy_searchproxy, kwargs={'degrade_level': degrade_level})
        deploy_ip = Thread(target=self.deploy_indexerproxy, kwargs={'degrade_level': degrade_level})

        for thread in (deploy_sp, deploy_ip):
            thread.start()

        for thread in (deploy_sp, deploy_ip):
            thread.join()

    def deploy_proxy(self, proxy_type, degrade_level=0.15):

        assert proxy_type in ['searchproxy', 'indexerproxy', ], 'Proxy type must be either searchproxy or indexerproxy, got {}'.format(proxy_type)

        deploy_url = "/deploy?ctype={}&service={}&may_be_dead_procentage={}&service_type={}".format(
            self.__ctype,
            proxy_type,
            degrade_level,
            proxy_type,
            )
        r = self.dm_api_request(deploy_url)
        assert r.ok, "Deploy {} task creation failed".format(proxy_type)
        deploy_proxy_task = r.text.strip()
        logging.debug('Proxy deploy task id=%s spawned', deploy_proxy_task)
        if not self.wait_for_dm_task(deploy_proxy_task):
            logging.error("Deploy %s incomplete", proxy_type)

    def get_slots_on_hosts(self, hosts):
        """
        :type hosts: List[AnyStr]
        :rtype: List[dict]
        """
        slots = []
        slots_by_interval = self._get_used_slots().json()
        for interval in slots_by_interval:
            shards_min = interval['$shards_min$']
            shards_max = interval['$shards_max$']
            for slot in interval['slots']:
                slot_name = slot['id']
                slot_real_host = slot['$real_host$']
                geo = slot['$datacenter_alias$']
                for host in hosts:
                    if host_specification(host)[0] in slot_real_host:
                        slots.append({
                            "id": slot_name,
                            "geo": geo,
                            "shards_min": shards_min,
                            "shards_max": shards_max,
                        })
        return slots

    def get_unused_slots_on_host(self, host):

        short_host_name = re.findall('(.*).search.yandex.net', host)[0]

        unused_slots = self._get_unused_slots().json()
        result = []
        for saas_ctype in unused_slots:
            for geo in unused_slots[saas_ctype]:
                if short_host_name in unused_slots[saas_ctype][geo]:
                    for slot in unused_slots[saas_ctype][geo][short_host_name]:
                        result.append("{}:{}".format(unused_slots[saas_ctype][geo][short_host_name][slot]['host'], unused_slots[saas_ctype][geo][short_host_name][slot]['port']))
                    return result

    def get_unused_slots(self):
        unused_slots = self._get_unused_slots().json()
        result = []
        for saas_ctype in unused_slots:
            for geo in unused_slots[saas_ctype]:
                for short_host_name in unused_slots[saas_ctype][geo]:
                    for slot in unused_slots[saas_ctype][geo][short_host_name]:
                        result.append("{}:{}".format(unused_slots[saas_ctype][geo][short_host_name][slot]['host'], unused_slots[saas_ctype][geo][short_host_name][slot]['port']))
        return result

    def update_tags_info(self, action='set', nanny='', slots='', use_containers=False):
        return self._update_tags_info(action, nanny, slots, use_containers)


#
# Needs refactoring
#
def replace_all_slots_in_DM(ctype, service, nanny_service_override=None, confirm_dangerous_actions=True, dont_restore=False):

    all_hosts = set()
    dm = DeployManagerSlotManipulator(service, ctype)

    if nanny_service_override:
        dm.update_tags_info(action='set', nanny=nanny_service_override, slots='', use_containers=True)

    slots_by_shard = dm._get_used_slots().json()
    for shard in slots_by_shard:
        for slot in shard['slots']:
            all_hosts.add(slot['$real_host$'])

    replace_hosts_in_DM(ctype, service, list(all_hosts), confirm_dangerous_actions=confirm_dangerous_actions, dont_restore=dont_restore)


def replace_hosts_in_DM(ctype, service, old_hosts, new_hosts=None, clear_cache=False, confirm_dangerous_actions=True, dont_restore=False):

    dm = DeployManagerSlotManipulator(service, ctype)
    saas_service = SaasService(ctype, service)

    # Update tags in DM
    if clear_cache:
        logging.info('Clearing DM cache')
        saas_service.clear_dm_cache()

    dm_cache_clear_retries = 271  # 100 * e
    can_replace_slots = False
    remaining_clear_cache_actions = dm_cache_clear_retries

    # Discover old host slots
    slots_to_replace = saas_service.get_slots_on_hosts(old_hosts)
    LOGGER.info('Going to replace %s slots in DM', slots_to_replace)
    while not can_replace_slots:
        # Discover new host available slots
        free_slots_pool = []
        if new_hosts:
            for new_host in new_hosts:
                unused_slots_on_new_host = dm.get_unused_slots_on_host(new_host)
                if unused_slots_on_new_host is None:
                    free_slots_pool = []
                    can_replace_slots = False
                    break
                else:
                    free_slots_pool += unused_slots_on_new_host
        else:
            free_slots_pool = saas_service.free_slots_pool

        if len(free_slots_pool) < len(slots_to_replace):
            logging.error("Not enough free slots to replace.")
            if clear_cache and remaining_clear_cache_actions > 0:
                time.sleep(1 * dm_cache_clear_retries / remaining_clear_cache_actions)
                logging.info('Clearing DM cache, remaining attempts %s', remaining_clear_cache_actions)
                dm.dm_cache_clear()
                remaining_clear_cache_actions -= 1
            else:
                logging.error("Will not clear dm cache.")
                raise Exception("Not enough free slots to replace")
        else:
            can_replace_slots = True

    replace_hosts_in_saas_services(
        ctype,
        [service],
        old_hosts,
        new_hosts,
        clear_cache,
        confirm_dangerous_actions,
        dont_restore
    )


def replace_all_hosts_in_saas_services(ctype, services, confirm_dangerous_actions=True):
    logging.info("Going to replace all hosts in services %s", services)
    replace_hosts_in_saas_services(ctype, services, 'all', confirm_dangerous_actions)


def replace_hosts_in_saas_services(ctype, services, old_hosts, new_hosts=None, clear_cache=False, confirm_dangerous_actions=True, dont_restore=False):

    ss = [SaasService(ctype, service) for service in services]
    added_slots = {}
    slots_to_replace = {}

    for s in ss:
        service = s.name
        ctype = s.ctype

        if old_hosts in ['all', 'ALL']:
            slots_to_replace[service] = list(s.slots)
        else:
            slots_to_replace[service] = [slot for slot in s.slots if slot.physical_host in old_hosts or slot.host in old_hosts]

        geo_to_replace = set()
        for slot in slots_to_replace[service]:
            if slot.geo not in geo_to_replace:
                geo_to_replace.add(slot.geo)

        added_slots[service] = []

        remaining_clear_cache_actions = 273  # per service
        for geo in geo_to_replace:

            can_replace_hosts = False
            no_dc_disaster = True

            while not can_replace_hosts:
                while no_dc_disaster:
                    no_dc_disaster = False
                    local_alive_free_slots_pool = [slot for slot in s.get_free_slots_in_geo(geo) if not slot.is_down]
                    for slot in local_alive_free_slots_pool:
                        if slot.geo not in ['SAS', 'MAN', 'VLA', 'IVA', 'MYT', 'MSK']:
                            no_dc_disaster = True
                            logging.info('Remaining clear_cache attempts %s, clearing cache', remaining_clear_cache_actions)
                            logging.info('Clots with strange DC : %s', [{slot.id: slot.geo} for slot in local_alive_free_slots_pool])
                            s.clear_dm_cache()
                            remaining_clear_cache_actions -= 1
                            time.sleep(60)

                random.shuffle(local_alive_free_slots_pool)
                local_slots_to_replace = [slot for slot in slots_to_replace[service] if slot.geo == geo]
                logging.debug('Going to add some slots from pool %s to replace %s', local_alive_free_slots_pool, local_slots_to_replace)
                can_replace_hosts = len(local_slots_to_replace) <= len(local_alive_free_slots_pool)
                if not can_replace_hosts:
                    logging.info('Remaining clear_cache attempts %s, clearing cache', remaining_clear_cache_actions)
                    s.clear_dm_cache()
                    remaining_clear_cache_actions -= 1
                    time.sleep(60)

                assert remaining_clear_cache_actions > 0, "Seem to have not enough free slots"

            logging.info('Going to add some slots from pool %s to replace %s', local_alive_free_slots_pool, local_slots_to_replace)

            logging.info('Adding slots %s', local_alive_free_slots_pool)
            local_added_slots = s.duplicate_slots(local_slots_to_replace, local_alive_free_slots_pool)
            logging.info('Enable indexing for slots %s in searchmap', local_added_slots)
            s.modify_searchmap(local_added_slots, 'enable_indexing')
            added_slots[service].extend(local_added_slots)

        logging.debug('New slots in service %s allocated', s)

    # Deploy indexerproxy
    #
    logging.info("Deploy indexerproxy")
    ss[0]._dm.deploy_indexerproxy(ss[0].ctype, degrade_level=0.5)

    for s in ss:
        service = s.name

        restore_tasks_to_wait = []
        slot_names_to_restore = []

        if not dont_restore:
            logging.info('Deploying new slots')
            for slot in added_slots[service]:
                logging.info('Deploying %s', slot)
                try:
                    slot.disable_indexing()
                    slot.disable_search()
                except:
                    logging.exception(
                        "Failed to deploy %s in service %s/%s",
                        slot.id,
                        service,
                        ctype
                    )

                slot_names_to_restore.append(slot)

            # Restore replicas
            #
            restore_task = s._dm.restore_replica(s, added_slots[service])
            restore_tasks_to_wait.append((restore_task, slot_names_to_restore))

            logging.error("Restoring slots {} in paralel".format(added_slots))

            for task, slot in restore_tasks_to_wait:
                if s._dm.wait_for_dm_task(task):
                    logging.info("Restore task for slot %s OK", slot)
                else:
                    logging.error("Restore task for slot %s failed", slot)

        else:
            logging.info("Restoring index from backup...")
            try:
                s.restore_index_from_backup(num_docs_tolerance_factor=0.9, time_interval=60, degrade_level=len(added_slots[service]), slot_id_list=[slot.id for slot in added_slots[service]])
                logging.info("...seems to be completed")
            except:
                pass
            while not ask_yes_or_no('Press Y after the backend get ready (Y/N)'):
                pass

        logging.info("Deploy %s one more time", added_slots[service])
        for sl in added_slots[service]:
            sl.deploy(wait=60)

        logging.info("Enable search on slots %s in searchmap", added_slots[service])
        s.set_searchmap(added_slots[service], search_enabled=True, indexing_enabled=True, deploy_backends=True)
        logging.info("Disable search on slots %s in searchmap", added_slots[service])
        s.set_searchmap(slots_to_replace[service], search_enabled=False)

    # Ask user : to explode or not to explode
    # -- explode : release instances, deploy proxies, deploy nanny, drop caches
    # -- not explode : commit sp; commit nanny

    deploy_proxies_warning_message = "Do you want to deploy searchproxy in {}? (Y/n)".format(ctype)
    if not confirm_dangerous_actions or ask_yes_or_no(deploy_proxies_warning_message):

        logging.info("Deploy proxies")
        ss[0]._dm.deploy_proxies(ctype)

        for s in ss:
            service = s._service

            logging.info('Will release old slots %s in service %s/%s now', slots_to_replace[service], ctype, service)
            release_old_slots_warning_message = 'Do you want to release old slots in {}/{} (Y/n)'.format(ctype, service)
            if not confirm_dangerous_actions or ask_yes_or_no(release_old_slots_warning_message):
                s.release_slots(slots_to_replace[service])
            else:
                logging.info("Skip old slots release because of explicit user choice")
    else:
        logging.info("Skip proxies deployment because of explicit user choice")


def replace_saas_hosts(saas_service, old_hosts, nanny_service=None, group=None, startrek_ticket=None, skip_gencfg_phase=False, skip_nanny_phase=False,
                       confirm_dangerous_actions=True, dont_restore=False):
    """
    :type saas_service: SaasService
    :type old_hosts: List[AnyStr]
    :type nanny_service: AnyStr
    :type group: AnyStr
    :type startrek_ticket: AnyStr
    :type skip_gencfg_phase: bool
    :type skip_nanny_phase: bool
    :type confirm_dangerous_actions: bool
    :type dont_restore: bool
    """
    new_tag = None
    if not skip_nanny_phase:  # if nanny phase skipped gencfg phase is useless
        if not skip_gencfg_phase:
            if not group:
                old_hosts_set = set(old_hosts)
                for group in saas_service.get_gencfg_groups(names_only=False):
                    hosts_to_replace = set(group.hosts).intersection(old_hosts_set)
                    if hosts_to_replace:
                        (new_tag, _) = GencfgGroupRequest.replace_hosts(group.name, old_hosts=old_hosts, ticket=startrek_ticket)
            else:
                (new_tag, _) = GencfgGroupRequest.replace_hosts(group, old_hosts=old_hosts, ticket=startrek_ticket)

            logging.info('Tag %s with new hosts is ready', new_tag)

        new_tag = new_tag or GencfgGroupRequest.get_latest_tag()
        phase_one_snapshot = recluster_phase_one(nanny_service.name, "tags/{}".format(new_tag), dry_run=False, old_snapshot_check=False)
        wait_for_snaphsot_active(nanny_service.name, phase_one_snapshot)

    # Replace slot in DM
    replace_hosts_in_DM(saas_service.ctype, saas_service.name, old_hosts, clear_cache=True, confirm_dangerous_actions=confirm_dangerous_actions, dont_restore=dont_restore)

    # Get rid of old topology
    remove_topology_warning_message = "Remove old topology from nanny service? (Y/n)"
    if (not skip_nanny_phase) and confirm_dangerous_actions and ask_yes_or_no(remove_topology_warning_message):
        recluster_phase_two(nanny_service.name, "tags/{}".format(new_tag), dry_run=False, old_snapshot_check=False)


if __name__ == '__main__':
    pass
