from typing import Callable, Union
import logging
import time
from datetime import date, datetime
from dateutil import tz

from algo.data.cache.dynamo import DynamoCache
from algo.data.common import PriceStream, Symbol

from .dynamo import DynamoCache
from .memory import MemoryCache
from .redis import RedisCache

CachableDataSource = Callable[[str, Symbol], PriceStream]

log = logging.getLogger(__name__)

memory_cache = MemoryCache()
redis_cache = RedisCache()
# dynamo  cache creates table here, might worth making its external communication lazy
# and it takes startup time ...
dynamo_cache = DynamoCache()

default_cache_instance = dynamo_cache


def has_day_data(symbol: Symbol, day: date) -> bool:
    # a little bit of issues caused by my own problem - historically using full datetime, but it isn't really needed as we talking about day
    full_day = datetime.combine(day, datetime.min.time(), tz.tzutc())
    meta_key_for_day = meta_key(
        day_data_cache_key(
            symbol.value, full_day.isoformat()
        )
    )
    return default_cache_instance.has_meta(meta_key_for_day)


def day_data_cache_key(symbol_str: str, date_str: str):
    return f'day-data-{symbol_str}-{date_str}'


def lock_key(cache_key: str) -> str:
    """lock key format"""
    return f'{cache_key}-lock'


def meta_key(cache_key: str) -> str:
    """meta key format"""
    return f'{cache_key}-meta'


def cached_factory(cache_instance: Union[MemoryCache, RedisCache,  DynamoCache]):
    """
    TBD: think when and how release the lock
    """
    def _cached(cache_key: str, data_source: CachableDataSource) -> PriceStream:
        log.debug('starting _cached')

        lock_key_name = lock_key(cache_key)
        meta_key_name = meta_key(cache_key)
        if cache_instance.has(cache_key):
            log.info('hitting cache', extra={'cache-key': cache_key})
            yield from cache_instance.get(cache_key)

        elif cache_instance.has_lock(lock_key_name):
            # I guess this locking mechanics not reliable - google on how to work with locks in redis
            # as multiple processes can set this lock
            log.info('found lock, waiting for data')
            while cache_instance.has_lock(lock_key_name):
                time.sleep(0.1)
                continue
            yield from _cached(cache_key, data_source)

        else:
            log.debug('no cache reading data')
            cache_instance.set_lock(lock_key_name)
            # no cache
            all_data = []
            data_counter = 0
            for data_portion in data_source:
                all_data.append(data_portion)
                data_counter += 1
                yield data_portion
            cache_instance.set(cache_key, all_data)
            cache_instance.set_meta(meta_key_name, {
                'data_counter': data_counter
            })
            cache_instance.clear_lock(lock_key_name)
    return _cached


cached = cached_factory(
    # RedisCache()
    default_cache_instance
)
