# coding: utf-8

from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals

import re
import copy
import logging
from datadiff import diff

from functools import partial
from contextlib import contextmanager
from cached_property import cached_property

from infra.nanny.nanny_services_rest.nanny_services_rest.client import ServiceRepoClient
from infra.nanny.nanny_services_rest.nanny_services_rest.errors import (
    NotFoundError, ModificationConflictError, ServiceRepoRequestError
)

from saas.library.python.token_store import PersistentTokenStore

from saas.library.python.nanny_rest.cms_snapshot_info import CmsSnapshotInfo
from saas.library.python.nanny_rest.enums import AllocationType, ServiceSummaryStatus, SnapshotStatus
from saas.library.python.nanny_rest.shard_resource import sandbox_bsc_shard
from saas.library.python.nanny_rest.service_mutable_proxy import (
    NannyServiceBaseRuntimeProxy, NannyServiceBaseInfoProxy, NannyServiceBaseAuthProxy
)
from saas.library.python.nanny_rest.service_mutable_proxy.runtime_attrs_proxy import _RESOURCE_TYPE_MAPPING
from saas.library.python.nanny_rest.service_mutable_proxy.instance_spec import ReadonlyInstanceSpec
from saas.library.python.nanny_rest.recipe import AlemateRecipe
from saas.library.python.nanny_rest.errors import (
    NannyApiError, NannyServiceNotFoundError, NannyServiceAlreadyExists, ConcurrentModificationError, InvalidRecipe
)


class NannyServiceBase(object):
    LOGGER = logging.getLogger(__name__)
    _NANNY = None
    _attrs_regexp = re.compile(r'\A((?P<action>get|set)_)?(?P<kind>info|runtime|auth)_attrs\Z')
    _cached_property_map = {'runtime': ('allocation_type', 'resources', 'files'), 'info': tuple(), 'auth': tuple()}

    @classmethod
    def initialize_nanny_client(cls):
        if cls._NANNY is None:
            cls.LOGGER.debug('Initializing nanny service repo client')
            cls._NANNY = ServiceRepoClient(
                'https://nanny.yandex-team.ru', token=PersistentTokenStore.get_token_from_store_env_or_file('nanny')
            )

    @classmethod
    def create(cls, name, info_attrs, runtime_attrs=None, auth_attrs=None):
        cls.initialize_nanny_client()

        request_data = {
            'id': name,
            'info_attrs': info_attrs
        }
        if runtime_attrs:
            request_data['runtime_attrs'] = runtime_attrs
        if auth_attrs:
            request_data['auth_attrs'] = auth_attrs

        try:
            res = cls._NANNY.create_service(request_data)
        except ServiceRepoRequestError as e:
            raise NannyServiceAlreadyExists(
                'Failed to create service {} due to {}'.format(name, e),
                inner_exception=e
            )
        return cls(name, runtime_attrs=res['runtime_attrs'], info_attrs=res['info_attrs'], auth_attrs=res['auth_attrs'])

    def __init__(self, name, runtime_attrs=None, info_attrs=None, auth_attrs=None, history_runtime_attrs=None):
        self.initialize_nanny_client()
        self.name = name
        self._info_attrs = info_attrs
        self._runtime_attrs = runtime_attrs
        self._auth_attrs = auth_attrs
        self._history_runtime_attrs = history_runtime_attrs

    def __eq__(self, other):
        if isinstance(other, NannyServiceBase):
            return self.name == other.name
        else:
            return NotImplemented

    def __ne__(self, other):
        return not self.__eq__(other)

    def __hash__(self):
        return hash(self.name)

    def __repr__(self):
        return 'NannyServiceBase({})'.format(self.name)

    def __getattr__(self, item):
        match = self._attrs_regexp.match(item)
        if match:
            if match.group('action') is None:
                return object.__getattribute__(self, '_get_attrs')(match.group('kind'))
            elif match.group('action') == 'get':
                return partial(object.__getattribute__(self, '_get_attrs'), match.group('kind'))
            elif match.group('action') == 'set':
                return partial(object.__getattribute__(self, '_set_attrs'), match.group('kind'))
            else:
                raise RuntimeError('{} is invalid attr action'.format(match.group('action')))
        else:
            return object.__getattribute__(self, item)

    def _get_attrs(self, kind):
        attr_name = '_{}_attrs'.format(kind)
        try:
            if self.__getattr__(attr_name) is None:
                self.__setattr__(attr_name, self._NANNY.__getattribute__('get_{}_attrs'.format(kind))(self.name))
            return copy.deepcopy(self.__getattr__(attr_name)['content'])
        except NotFoundError as e:
            self.LOGGER.critical('Service %s not found in Nanny service repo', self.name)
            raise NannyServiceNotFoundError(e)

    def _set_attrs(self, kind, content, comment, ticket_id=None):
        attr_name = '_{}_attrs'.format(kind)
        put_content = {
            'comment': comment, 'snapshot_id': self.__getattr__(attr_name)['_id'], 'content': content,
        }

        if kind == 'runtime' and ticket_id:
            put_content['startrek_tickets'] = ticket_id

        self.invalidate_cache(kind)
        try:
            result = self._NANNY.__getattribute__('put_{}_attrs'.format(kind))(self.name, put_content)
            self.__setattr__(attr_name, result)
            return copy.deepcopy(result)['content']
        except ModificationConflictError as e:
            raise ConcurrentModificationError(inner_exception=e)

    def invalidate_cache(self, kind):
        for prop in self._cached_property_map[kind]:
            if prop in self.__dict__:
                del self.__dict__[prop]
        self.__setattr__('_{}_attrs'.format(kind), None)

    @contextmanager
    def info_attrs_transaction(self, comment):
        original_info_attrs = self.info_attrs
        mutable_proxy = NannyServiceBaseInfoProxy(copy.deepcopy(original_info_attrs))
        try:
            yield mutable_proxy
            result_info_attrs = mutable_proxy.info_attrs_content
            changes = diff(original_info_attrs, result_info_attrs)
            self.LOGGER.info('Commit snapshot with diff {} to {}'.format(changes, self.name))
            self.set_info_attrs(content=result_info_attrs, comment=comment)
        except ConcurrentModificationError as e:
            self.LOGGER.error(
                'ConcurrentModificationError on commit transactional changes to %s. Details: %s',
                self.name, e.inner_exception
            )
            raise e

    @contextmanager
    def runtime_attrs_transaction(self, comment, ticket=None):
        original_runtime_attrs = self.runtime_attrs
        mutable_proxy = NannyServiceBaseRuntimeProxy(self, copy.deepcopy(original_runtime_attrs))
        try:
            yield mutable_proxy
            result_runtime_attrs = mutable_proxy.runtime_attrs_content
            changes = diff(original_runtime_attrs, result_runtime_attrs,
                           context=1, fromfile=self._runtime_attrs['_id'], tofile='current')
            self.LOGGER.info('Commit snapshot with diff {} to {}'.format(changes, self.name))
            self.set_runtime_attrs(content=result_runtime_attrs, comment=comment, ticket_id=ticket)
        except ConcurrentModificationError as e:
            self.LOGGER.error(
                'ConcurrentModificationError on commit transactional changes to {}. Details: {}'.format(
                    self.name, e.inner_exception))
            raise e

    @contextmanager
    def auth_attrs_transaction(self, comment):
        original_auth_attrs = self.auth_attrs
        mutable_proxy = NannyServiceBaseAuthProxy(copy.deepcopy(original_auth_attrs))
        try:
            yield mutable_proxy
            result_auth_attrs = mutable_proxy.auth_attrs_content
            changes = diff(original_auth_attrs, result_auth_attrs,
                           context=1, fromfile=self._auth_attrs['_id'], tofile='current')
            self.LOGGER.info('Commit snapshot with diff {} to {}'.format(changes, self.name))
            self.set_auth_attrs(content=result_auth_attrs, comment=comment)
        except ConcurrentModificationError as e:
            self.LOGGER.error(
                'ConcurrentModificationError on commit transactional changes to {}. Details: {}'.format(
                    self.name, e.inner_exception))
            raise e

    @cached_property
    def allocation_type(self):
        try:
            return AllocationType(self.runtime_attrs['instances']['chosen_type'])
        except ValueError as e:
            self.LOGGER.error('Invalid allocation type %s', self.runtime_attrs['instances']['chosen_type'])
            raise NannyApiError(e)

    @property
    def history_runtime_attrs(self):
        try:
            if self._history_runtime_attrs is None:
                self._history_runtime_attrs = self._NANNY.get_history_runtime_attrs(self.name)
            return copy.deepcopy(self._history_runtime_attrs['result'])
        except NotFoundError as e:
            self.LOGGER.critical('Service %s not found in Nanny service repo', self.name)
            raise NannyApiError(e)

    @cached_property
    def resources(self):
        self.LOGGER.debug('GET resources for %s', self.name)
        resources_dict = self.runtime_attrs['resources']

        parsed_fields = {}
        for resource_type, resource_class in _RESOURCE_TYPE_MAPPING.items():
            items = {}
            for resource_dict in resources_dict[resource_type]:
                resource = resource_class(**resource_dict)
                items[resource.local_path] = resource
            parsed_fields[resource_type] = items

        parsed_fields['sandbox_bsc_shard'] = sandbox_bsc_shard(resources_dict.get('sandbox_bsc_shard', None))

        resources_dict.update(parsed_fields)
        return resources_dict

    @property
    def sandbox_files(self):
        return self.resources['sandbox_files']

    @property
    def static_files(self):
        return self.resources['static_files']

    @property
    def url_files(self):
        return self.resources['url_files']

    @property
    def template_set_files(self):
        return self.resources['template_set_files']

    @property
    def shard(self):
        return self.resources.get('sandbox_bsc_shard', None)

    @cached_property
    def files(self):
        files = {}
        for k, v in self.resources.items():
            try:
                files.update(v)
            except TypeError:
                pass  # SandboxShardmap object is not iterable
        return files

    @property
    def recipes(self):
        return {r['id']: AlemateRecipe(**r) for r in self.info_attrs['recipes']['content']}

    @property
    def prepare_recipes(self):
        return {r['id']: AlemateRecipe(**r) for r in self.info_attrs['recipes']['prepare_recipes']}

    @cached_property
    def instance_spec(self):
        return ReadonlyInstanceSpec(**self.runtime_attrs['instance_spec'])

    @property
    def current_state_summary_status(self):
        raw_current_state = self._NANNY.get_current_state(self.name)
        return ServiceSummaryStatus(raw_current_state['content']['summary']['value'])

    @property
    def paused(self):
        raw_current_state = self._NANNY.get_current_state(self.name)
        return raw_current_state['content']['is_paused']['value']

    @property
    def is_online(self):
        return self.current_state_summary_status == ServiceSummaryStatus.ONLINE

    @property
    def current_snapshot_id(self):
        if self._runtime_attrs is None or self._runtime_attrs.get('_id', None) is None:
            self.get_runtime_attrs()
        return self._runtime_attrs['_id']

    @property
    def cms_snapshots_info(self):
        raw_current_state = self._NANNY.get_current_state(self.name)
        return [
            CmsSnapshotInfo(self, **snapshot_info) for snapshot_info in raw_current_state['content']['active_snapshots']
        ]

    @property
    def active_snapshot_id(self):
        active_snapshots = [
            snapshot_info for snapshot_info in self.cms_snapshots_info if snapshot_info.state == SnapshotStatus.ACTIVE
        ]
        if active_snapshots:
            if len(active_snapshots) > 1:
                raise NannyApiError('More than one active snapshot in {}'.format(self))
            else:
                return active_snapshots[0].snapshot_id
        else:
            return None

    def is_current_snapshot_active(self):
        return self.active_snapshot_id == self.current_snapshot_id

    def activate_current_snapshot(self, recipe_name, prepare_recipe_name=None, comment=''):
        recipe_names = self.recipes.keys()
        prepare_recipe_names = self.prepare_recipes.keys()
        if recipe_name not in recipe_names:
            raise InvalidRecipe(
                'Service {} don\'t have recipe {}. Available recipes: {}'.format(self.name, recipe_name, recipe_names)
            )
        event_data = {
            'type': 'SET_TARGET_STATE',
            'content': {
                'is_enabled': True,
                'snapshot_id': self._runtime_attrs['_id'],
                'comment': comment,
                'recipe': recipe_name
            }
        }
        if prepare_recipe_name and self.prepare_recipes.keys():
            if prepare_recipe_name not in prepare_recipe_names:
                raise InvalidRecipe('Service {} don\'t have prepare recipe {}. Available recipes: {}'.format(
                    self.name, prepare_recipe_name, prepare_recipe_names)
                )
            else:
                event_data['content']['prepare_recipe'] = prepare_recipe_name
        self._NANNY.create_event(self.name, event_data)

    @property
    def ui_url(self):
        return '{}/ui/#/services/catalog/{}'.format(self._NANNY._base_url, self.name)
