# coding: utf-8
from itertools import chain, count
import logging
import requests
import time
import json

from six.moves.urllib.parse import urljoin

from saas.library.python.token_store.token_store import TokenStore

from google.protobuf import text_format
from yweb.querydata.querydata_indexer_saas.ferryman.idl import config_pb2

from saas.library.python.deploy_manager_api.saas_service import SaasService
from saas.library.python.awacs.namespace import NamespaceManager
from saas.library.python.nanny_rest import NannyServiceBase
from saas.library.python.nanny_rest.service_mutable_proxy.environment_variable import LiteralEnv
import library.python.retry as retry


EMPTY_TABLE_PATH = {
    'hahn': '//home/saas/empty',
    'arnold': '//home/saas/empty'
}


def _retry_only_non_400(exc):
    if isinstance(exc, requests.RequestException) and exc.response is not None:
        response: requests.Response = exc.response
        return response.status_code < 400 or response.status_code >= 500
    return True


class FerrymanHTTPAPI(object):

    """
    Ferryman basic API implementation.
    Refs : https://wiki.yandex-team.ru/jandekspoisk/SaaS/Ferryman/#httpapi

    /add-table?path =$path & namespace =$namespace×tamp =$timestamp & delta =$delta & cluster =$
    cluster[ & nomirror = true][ & format =$format]

    /add-full - tables?tables =$tables[ & nomirror =$nomirror][ & format =$format]

    /get-batch - status?batch =$batch

    /get-all - timestamps

    /get-states?from=$from & to =$to[ & hr =$hr] и / get - states?n =$n[ & hr =$hr]

    /get-namespaces[?hr =$hr]

    /pause-cooking?pause=$pause
    """

    LOGGER = logging.getLogger(__name__)

    def __init__(self, base_url):
        self.base_url = base_url
        self.session = requests.session()
        self.reqlog = None

    @retry.retry(retriable=_retry_only_non_400, max_times=5)
    def _api_request(self, request, method='GET', data=None, params=None, dry_run=False, reqinfo=None):
        if params is None:
            params = {}

        if reqinfo:
            params["reqinfo"] = reqinfo

        url = urljoin(self.base_url, request)
        self.LOGGER.debug('Executing method %s on url %s with params %s', method, url, params)
        if not dry_run:
            result = self.session.request(method, url, data=data, params=params)
            self.LOGGER.debug('Response %s', result)

            if self.reqlog is not None:
                self.reqlog.append({'url': url, 'params': params, 'data': data, 'result': result})

        else:
            result = requests.Response()

        return result

    def get_config_raw(self, reqinfo=None):
        """
        /get-config
        :return:
        """
        return self._api_request('/get-config', reqinfo=reqinfo).text

    def pause_cooking(self, reqinfo=None):
        """
        /pause-cooking?pause=$pause
        """
        return self._api_request('/pause-cooking', params={'pause': True}, reqinfo=reqinfo).json()

    def resume_cooking(self, reqinfo=None):
        """
        /pause-cooking?pause=$pause
        """
        return self._api_request('/pause-cooking', params={'pause': False}, reqinfo=reqinfo).json()

    def add_table(self, path, namespace, is_delta, cluster=None, timestamp=None, nomirror=None, format_=None, dry_run=False, reqinfo=None):
        """
        /add-table?path=$path&namespace=$namespace&timestamp=$timestamp&delta=$delta&cluster=$cluster
        [&nomirror=true][&format=$format]
        """
        params = {
            'path': path,
            'namespace': namespace,
            'delta': is_delta,
            'timestamp':  timestamp if timestamp is not None else int(time.time() * 1000000)
        }

        if cluster is not None:
            params['cluster'] = cluster

        if nomirror is not None:
            params['nomirror'] = nomirror

        if format_ is not None:
            params['format'] = format_
        return self._api_request('/add-table', params=params, dry_run=dry_run, reqinfo=reqinfo).json()

    def add_full_tables(self, tables, nomirror=None, format_=None, reqinfo=None):
        """
        /add-full-tables?tables=$tables
        [&nomirror=$nomirror][&format=$format]
        """
        params = {
            'tables': tables if isinstance(tables, str) else json.dumps(tables)
        }

        if nomirror is not None:
            params['nomirror'] = nomirror

        if format_ is not None:
            params['format'] = format_

        return self._api_request('/add-full-tables', params=params, reqinfo=reqinfo).json()

    def get_batch_status(self, batch_id, reqinfo=None):
        """
        /get-batch-status?batch=$batch
        """
        return self._api_request('/get-batch-status', params={'batch': batch_id}, reqinfo=reqinfo).json()

    def get_all_timestamps(self, reqinfo=None):
        """
        /get-all-timestamps
        """
        return self._api_request('/get-all-timestamps', reqinfo=reqinfo).json()

    def get_states(self, n=None, _from=None, _to=None, reqinfo=None):
        """
        /get-states?from=$from&to=$to[&hr=$hr]
        /get-states?n=$n[&hr=$hr]
        """
        if n is not None:
            return self._api_request('/get-states', params={'n': n}, reqinfo=reqinfo).json()
        elif _from is not None and _to is not None:
            return self._api_request('/get-states', params={'from': _from, 'to': _to}, reqinfo=reqinfo).json()
        else:
            raise Exception('Need to know either N or (from, to) to get states')

    def get_namespaces(self, reqinfo=None):
        """
        /get-namespaces[?hr=$hr]
        """
        return self._api_request('/get-namespaces', reqinfo=reqinfo).json()


def key_value_sequence_to_dict(sequence, key="Key", value="Value", dict_constructor=dict):
    return dict_constructor({a.Key: a.Value for a in sequence})


class Ferryman(object):

    LOGGER = logging.getLogger(__name__)

    @classmethod
    def from_saas_service(cls, saas_service=None, service_name=None, service_ctype=None):
        saas_service = saas_service if saas_service is not None else SaasService(service_ctype, service_name)
        ferrymans = saas_service.sla_info.get('ferrymans')
        if ferrymans:
            return cls(nanny_service=ferrymans[0])
        else:
            return cls()

    @staticmethod
    def resolve_fqdn_by_nanny(nanny_service_name):
        upstream = NamespaceManager().upstream_manager.get_upstream("ferryman", nanny_service_name)
        if upstream:
            fqdn = upstream.spec.yandex_balancer.config.l7_upstream_macro.matcher.host_re
        else:
            nanny_token = TokenStore.get_token_from_store_or_env('nanny')
            r = requests.get(
                'https://nanny.yandex-team.ru/api/repo/GetBalancersForService/?serviceId={}'.format(nanny_service_name),
                headers={'Authorization': 'Oauth: {}'.format(nanny_token)}
            )
            balancer_id = r.json()['balancerRefs'][0]['balancerId']
            section_id = r.json()['balancerRefs'][0]['sectionId']

            r2 = requests.get(
                'http://nanny.yandex-team.ru/v2/services_balancers/{}/config/sections/{}/'.format(
                    balancer_id,
                    section_id
                ),
                headers={'Authorization': 'Oauth: {}'.format(nanny_token)}
            )
            fqdn = r2.json()['content']['matcher']['host']

        if not fqdn.startswith('http://') and not fqdn.startswith('https://'):
            fqdn = 'http://' + fqdn

        return fqdn

    def __init__(self, fqdn=None, nanny_service=None):
        self.nanny_service = nanny_service
        self.fqdn = None

        if not fqdn and nanny_service is not None:
            try:
                self.fqdn = Ferryman.resolve_fqdn_by_nanny(nanny_service)
            except:
                pass

        self.api = FerrymanHTTPAPI(self.fqdn)

    def __repr__(self):
        kwargs = {}
        if self.fqdn:
            kwargs['fqdn'] = self.fqdn
        if self.nanny_service:
            kwargs['nanny_service'] = self.nanny_service

        return "Ferryman({})".format(", ".join(["{}={}".format(repr(a), repr(kwargs[a])) for a in kwargs]))

    def __str__(self):
        return 'Ferryman at {}'.format(self.fqdn)

    def get_running_config(self):
        return self.api.get_config_raw()

    def get_env_vars(self):
        if not self.nanny_service:
            return None  # may raise some Exception here
        else:
            nanny_service = NannyServiceBase(self.nanny_service)
            ferryman_container = nanny_service.instance_spec.containers['ferryman_server']
            return ferryman_container.env

    def update_env_vars(self, vars, comment):
        if not self.nanny_service:
            raise RuntimeError('Unknown nanny service')
        else:
            env_dict = {var.name: var for var in vars}
            nanny_service = NannyServiceBase(self.nanny_service)
            with nanny_service.runtime_attrs_transaction(comment) as mutable_nanny_service:
                ferryman_container = mutable_nanny_service.instance_spec.containers['ferryman_server']
                old_env_dict = {var.name: var for var in ferryman_container.env}
                old_env_dict.update(env_dict)
                ferryman_container.env = old_env_dict.values()

    def get_env_var(self, name):
        env_dict = {var.name: var for var in self.get_env_vars()}
        if name in env_dict:
            return env_dict[name]
        else:
            return None

    def get_literal_env_value(self, name):
        env = self.get_env_var(name)
        if env:
            return env.value
        else:
            return None

    @property
    def yt_pool(self):
        return self.get_literal_env_value('YT_POOL')

    @yt_pool.setter
    def yt_pool(self, value):
        env_var = LiteralEnv('YT_POOL', value)
        self.update_env_vars([env_var], 'Use {} as YT_POOL'.format(value))

    @property
    def ferryman_cooker_yt_pool(self):
        return self.get_literal_env_value('FERRYMAN_COOKER_YT_POOL')

    @ferryman_cooker_yt_pool.setter
    def ferryman_cooker_yt_pool(self, value):
        env_var = LiteralEnv('FERRYMAN_COOKER_YT_POOL', value)
        self.update_env_vars([env_var], 'Use {} as FERRYMAN_COOKER_YT_POOL'.format(value))

    @property
    def json_converter_ram_gb(self):
        return self.get_literal_env_value('JSON_CONVERTER_RAM_GB')

    @json_converter_ram_gb.setter
    def json_converter_ram_gb(self, value):
        env_var = LiteralEnv('JSON_CONVERTER_RAM_GB', value)
        self.update_env_vars([env_var], 'Use {} as JSON_CONVERTER_RAM_GB'.format(value))

    @property
    def json_merger_ram_gb(self):
        return self.get_literal_env_value('JSON_MERGER_RAM_GB')

    @json_merger_ram_gb.setter
    def json_merger_ram_gb(self, value):
        env_var = LiteralEnv('JSON_MERGER_RAM_GB', value)
        self.update_env_vars([env_var], 'Use {} as JSON_MERGER_RAM_GB'.format(value))

    @property
    def env_row_weight_limit(self):
        return self.get_literal_env_value('ENV_ROW_WEIGHT_LIMIT')

    @env_row_weight_limit.setter
    def env_row_weight_limit(self, value):
        env_var = LiteralEnv('ENV_ROW_WEIGHT_LIMIT', value)
        self.update_env_vars([env_var], 'Use {} as ENV_ROW_WEIGHT_LIMIT'.format(value))

    def get_target_config(self):
        pass

    @property
    def running_config(self):
        return text_format.Parse(self.get_running_config(), config_pb2.TConfig())

    def _send_empty_table_to_nonexistent_namespace(self, cluster='hahn', dry_run=False):
        namespaces = [ns['namespace'] for ns in chain.from_iterable(self.api.get_namespaces().values())]
        for ns in count(1):
            if ns not in namespaces:
                return self.api.add_table(
                    cluster=cluster,
                    path=EMPTY_TABLE_PATH[cluster],
                    namespace=ns,
                    is_delta=False,
                    dry_run=dry_run
                )


class FerrymanBatchWaiterError(Exception):
    pass


class FerrymanBatchWaiter:

    LOGGER = logging.getLogger(__name__)

    def __init__(self, batch_id, ferryman, max_minutes_in_same_state=4*60, reqinfo=None):
        self.ferryman = ferryman
        self.batch_id = batch_id
        self.max_minutes_in_same_state = max_minutes_in_same_state*60
        self.status_history = []
        self.reqinfo = reqinfo

    def obtain_ferryman_batch_status(self, batch_id):
        try:
            batch_status = self.ferryman.api.get_batch_status(batch_id, reqinfo=self.reqinfo).get("status", "unknown")
        except:
            logging.exception("Looks like ferryman is temporary unavaliable")
            return None

        return batch_status

    def wait_batch_status(self, desired_states=None):
        if desired_states is None:
            desired_states = ['searchable']

        batch_status = self.obtain_ferryman_batch_status(self.batch_id)
        self.check_bad_status(batch_status)

        self.status_history.append({"status": batch_status, "ts": int(time.time())})

        while batch_status not in desired_states:
            time.sleep(30)
            batch_status = self.obtain_ferryman_batch_status(self.batch_id)
            self.LOGGER.info("Batch {} status: {}".format(self.batch_id, batch_status))
            self.check_bad_status(batch_status)

            if batch_status != self.status_history[-1]['status']:
                self.status_history.append({"status": batch_status, "ts": int(time.time())})
        return self.status_history

    def check_bad_status(self, batch_status):
        if batch_status == "unknown":
            raise FerrymanBatchWaiterError("Can't track status of unknown batches")
        if batch_status == "error":
            raise FerrymanBatchWaiterError("Batch {batch_id} processing finished with error".format(batch_id=self.batch_id))
        if self.status_history:
            if time.time() - self.status_history[-1]['ts'] > self.max_minutes_in_same_state:
                raise FerrymanBatchWaiterError("Can't track batch state more because time is exceed")
