# -*- coding: utf-8 -*-
import murmurhash
from collections import namedtuple
from yt.wrapper import ypath_join, ypath_split, TablePath
from datacloud.dev_utils.yt import yt_config_table, yt_utils


CachesStorageRec = namedtuple('CachesStorageRec', ['cache_key', 'cache_path', 'additional'])


class BaseTablesCacher(object):
    def __init__(self, yt_client=None):
        self._yt_client = yt_client or yt_utils.get_yt_client()
        self._storage = self._make_storage()

    def _make_storage(self):
        raise NotImplementedError()

    def _make_cache_path(self, cache_key):
        raise NotImplementedError()

    def _resolve_table_path(self, table_path):
        table_path = TablePath(table_path).to_yson_string()
        while self._yt_client.get_attribute(table_path + '&', 'type') == 'link':
            table_path = self._yt_client.get_attribute(table_path + '&', 'target_path')
        return table_path

    def cache_table(self, table_path):
        table_path = self._resolve_table_path(table_path)
        cache_key = self._make_cache_key(table_path=table_path)
        cache_path = self._make_cache_path(cache_key=cache_key)

        cache_path_exists = self._yt_client.exists(cache_path)

        cache_rec = self.read_cache(cache_key)
        if cache_rec is None:
            assert not cache_path_exists, 'Yt cache found, while log record was not!'
            self._yt_client.copy(table_path, cache_path)
            cache_rec = self._storage.write_cache(cache_key=cache_key, cache_path=cache_path)
        else:
            assert cache_path_exists, 'No yt table found, but cached!'

        return cache_rec

    def _make_cache_key(self, table_path):
        raise NotImplementedError()

    def read_cache(self, cache_key):
        if cache_key is None:
            return None
        return self._storage.read_cache(cache_key)

    def _is_cache(self, table_path):
        raise NotImplementedError()

    def cache_and_translate(self, table_path):
        table_path = self._resolve_table_path(table_path)
        if self._is_cache(table_path=table_path):
            return table_path
        return self.cache_table(table_path=table_path).cache_path

    def remove_cache(self, cache_key):
        if cache_key is None:
            return
        cache_rec = self.read_cache(cache_key)
        if cache_rec is None:
            return
        self._yt_client.remove(cache_rec.cache_path)
        self._storage.remove_cache(cache_key)


class BaseCachesStorage(object):
    def _write_cache(self, cache_key, cache_path, additional=None):
        raise NotImplementedError()

    def write_cache(self, cache_key, cache_path, additional=None):
        additional = additional or {}
        assert self.read_cache(cache_key=cache_key) is None, 'Already cached!'
        self._write_cache(cache_key=cache_key, cache_path=cache_path, additional=additional)
        return CachesStorageRec(cache_key=cache_key, cache_path=cache_path, additional=additional)

    def _read_cache(self, cache_key):
        raise NotImplementedError()

    def read_cache(self, cache_key):
        return self._read_cache(cache_key)

    def _remove_cache(self, cache_key):
        raise NotImplementedError()

    def remove_cache(self, cache_key):
        self._remove_cache(cache_key)


YtCacheKey = namedtuple('CacheKey', ['hash', 'original_path', 'modification_time'])
CACHES_STORAGE_PATH = '//home/x-products/penguin-dvier-dev/cache/caches_storage'


class DynTableCachesStorage(BaseCachesStorage):
    class YtTable(yt_config_table.ConfigTable):
        def __init__(self, table_path, yt_client):
            schema = [
                {'name': 'hash', 'type': 'uint64', 'sort_order': 'ascending'},
                {'name': 'original_path', 'type': 'string'},
                {'name': 'modification_time', 'type': 'string'},
                {'name': 'cache_path', 'type': 'string'},
                {'name': 'additional', 'type': 'any'},
            ]
            super(DynTableCachesStorage.YtTable, self).__init__(
                table_path=table_path, schema=schema, yt_client=yt_client
            )

    def __init__(self, table_path=CACHES_STORAGE_PATH, yt_client=None):
        self._yt_table = DynTableCachesStorage.YtTable(table_path=table_path, yt_client=yt_client)

    def _write_cache(self, cache_key, cache_path, additional=None):
        additional = additional or {}
        record = {
            'hash': cache_key.hash,
            'original_path': cache_key.original_path,
            'modification_time': cache_key.modification_time,
            'cache_path': cache_path,
            'additional': additional
        }
        self._yt_table.insert_records([record])

    def _read_cache(self, cache_key):
        record = self._yt_table.get_record_by_params({'hash': cache_key.hash})
        if record is None:
            return None
        return CachesStorageRec(
            cache_key=YtCacheKey(
                hash=record['hash'],
                original_path=record['original_path'],
                modification_time=record['modification_time']
            ),
            cache_path=record['cache_path'],
            additional=record['additional']
        )

    def _remove_cache(self, cache_key):
        self._yt_table.remove_records([{'hash': cache_key.hash}])


CACHES_ROOT = '//home/x-products/penguin-dvier-dev/cache'


class YtTablesCacher(BaseTablesCacher):
    def __init__(self, root=CACHES_ROOT, separator='_', storage_path=CACHES_STORAGE_PATH, **kwargs):
        self._root = root
        self._separator = separator
        self._storage_path = storage_path
        super(YtTablesCacher, self).__init__(**kwargs)

    @property
    def root(self):
        return self._root

    def _make_storage(self):
        return DynTableCachesStorage(table_path=self._storage_path, yt_client=self._yt_client)

    def _make_cache_path(self, cache_key):
        table_name = ypath_split(cache_key.original_path)[-1]
        return ypath_join(self.root, table_name + self._separator + cache_key.modification_time)

    def _make_cache_key(self, table_path):
        assert self._yt_client.exists(table_path)
        modification_time = self._yt_client.get_attribute(table_path, 'modification_time')
        return YtCacheKey(
            hash=murmurhash.hash64(table_path + modification_time),
            original_path=table_path,
            modification_time=modification_time,
        )

    def _is_cache(self, table_path):
        return ypath_split(table_path)[0] == self.root


AliasesStorageRec = namedtuple('AliasesStorageRec', ['alias', 'cache_key', 'additional'])


class BaseAliasesStorage(object):
    def _add_translation(self, alias, cache_key):
        raise NotImplementedError()

    def add_translation(self, alias, cache_key, force=False):
        assert force or self.translate(alias) is None, 'Already have alias for "{}"'.format(alias)
        self._add_translation(alias, cache_key)

    def _translate(self, alias):
        raise NotImplementedError()

    def translate(self, alias):
        return self._translate(alias)

    def _remove_translation(self, alias):
        raise NotImplementedError()

    def remove_translation(self, alias):
        self._remove_translation(alias)


ALIASES_STORAGE_PATH = '//home/x-products/penguin-dvier-dev/cache/aliases_storage'


class YtAliasesStorage(BaseAliasesStorage):
    class YtTable(yt_config_table.ConfigTable):
        def __init__(self, table_path, yt_client):
            schema = [
                {'name': 'alias', 'type': 'string', 'sort_order': 'ascending'},
                {'name': 'hash', 'type': 'uint64'},
                {'name': 'original_path', 'type': 'string'},
                {'name': 'modification_time', 'type': 'string'},
                {'name': 'additional', 'type': 'any'},
            ]
            super(YtAliasesStorage.YtTable, self).__init__(
                table_path=table_path, schema=schema, yt_client=yt_client
            )

    def __init__(self, table_path=ALIASES_STORAGE_PATH, yt_client=None):
        self._yt_table = YtAliasesStorage.YtTable(table_path=table_path, yt_client=yt_client)

    def _add_translation(self, alias, cache_key, additional=None):
        additional = additional or {}
        record = {
            'alias': alias,
            'hash': cache_key.hash,
            'original_path': cache_key.original_path,
            'modification_time': cache_key.modification_time,
            'additional': additional
        }
        self._yt_table.insert_records([record])

    def _translate(self, alias):
        record = self._yt_table.get_record_by_params({'alias': alias})
        if record is None:
            return None
        return AliasesStorageRec(
            alias=alias,
            cache_key=YtCacheKey(
                hash=record['hash'],
                original_path=record['original_path'],
                modification_time=record['modification_time']
            ),
            additional=record['additional']
        )

    def _remove_translation(self, alias):
        self._yt_table.remove_records([{'alias': alias}])
