#
# Copyright (c) Daniel Sheffield 2023
# All rights reserved
#
# THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY

import os
from time import time
from typing import Dict
import cherrypy

from .hash_util import blake, bytes_to_hash, hash_to_base32
from .CachedLoadingPage import STALE, CachedLoadingPage

def delete_page(name: str, root: str = 'app/rest/static/files'):
    directory = f'{root}/{name}'
    try:
        os.remove(f'{directory}/{name}.file')
    except FileNotFoundError:
        pass


def save_page(name: str, content: bytes, tool: str, root='app/rest/static/files') -> str:
    directory = f'{root}/{name}'
    try:
        os.mkdir(directory, mode=0o700, dir_fd=None)
    except FileExistsError:
        pass

    fd = os.open(f'{directory}/{name}.file', os.O_WRONLY | os.O_TRUNC | os.O_CREAT, 0o600)
    with open(fd, "wb") as f:
        f.write(content)


def get_page(name: str, root: str = 'app/rest/static/files') -> str:
    directory = f'{root}/{name}'

    try:
        mtime = os.stat(f'{directory}/{name}.file').st_mtime
    except:
        mtime = None
    
    if mtime is None:
        return None
    
    if mtime and time() - mtime > STALE:
        delete_page(name)
        return None

    # todo: store file hash and validate it
    fd = os.open(f'{directory}/{name}.file', os.O_RDONLY, 0o600)
    with open(fd, "rb") as f:
        f.seek(0)
        page = f.read()
    return page.decode('utf-8')


def key_to_hash(key):

    if isinstance(key, tuple):
        orig, _hash = key
    else:
        if isinstance(key, int):
            orig, _hash = None, key
        else:
            orig, _hash = key, None

    if None not in (orig, _hash):
        if get_hash(orig) != _hash:
            raise KeyError(f"Invalid key: {key}")

        return _hash

    if (_hash, orig) is (None, None):
        raise KeyError(f"Invalid key: {key}")
    
    return get_hash(orig) if _hash is None else _hash


def get_hash(key):
    _bytes = blake(key.encode('utf-8'), person='grocery'.encode('utf-8'))
    return bytes_to_hash(_bytes)


class PageCache:
    def __init__(self, limit) -> None:
        self._cache: Dict[str, CachedLoadingPage] = dict()
        self._limit = limit

    def __delitem__(self, key):
        key = key_to_hash(key)
        self._cache.pop(key, None)
        delete_page(hash_to_base32(key))

    def __getitem__(self, key):
        return self.get(key)

    def __setitem__(self, key, value):
        self._cache[key_to_hash(key)] = value

    def get(self, key: str) -> CachedLoadingPage:
        key = key_to_hash(key)

        if key not in self._cache:
            try:
                existing = get_page(hash_to_base32(key))
            except:
                existing = None
            if existing is None:
                return None
            
            return self.add(key, CachedLoadingPage(existing, lambda q: q.put(None), incremental=True))
        
        page = self._cache[key]
        if page.stale:
            del self._cache[key]
            delete_page(hash_to_base32(key))
            return None

        if not page.loaded:
            page.update()

        if page.loaded:
            try:
                existing = get_page(hash_to_base32(key))
            except:
                existing = None
            if existing is None:
                content = ''.join(page.value) if isinstance(page.value, list) else page.value
                save_page(hash_to_base32(key), content.encode('utf-8'), tool='grocery')

        return page

    def _enforce_limit(self, limit):
        for idx, (_, k) in enumerate(sorted([
                (v.age, k) for k, v in self._cache.items()
            ])):
            if idx >= limit: del self[k]

    def _clear_stale(self):
        for k in [k for k, v in self._cache.items() if v.stale]:
            del self[k]
    
    def add(self, key: str, page: CachedLoadingPage) -> CachedLoadingPage:
        self._clear_stale()
        self._enforce_limit(self._limit)
        self._cache[key_to_hash(key)] = page
        return page
    
    def remove(self, key: str):
        del self[key]
