import asyncio
from datetime import datetime, timedelta
from functools import wraps
from pathlib import Path
from typing import Any, Final

from . import bfile, bfunc, bpath, btime
from .bdefine import END_DATETIME
from .bfunc import crc_str, to_any
from .btype import Null


class BStoreManager:
    def __init__(self, cache_path: Path) -> None:
        self._cache_path = cache_path.absolute()
        self._deadline_file = self._cache_path / 'deadline.dat'
        self._updating = False
        self._deadline: dict[Path, datetime] = {}
        if self._deadline_file.exists():
            try:
                self._deadline = asyncio.run(bfile.read_pickle(self._deadline_file))
            except Exception:
                raise Exception('BCacheManager 缓存文件异常', self._deadline_file)

    async def put(self, key: str, data: Any, duration: timedelta = Null):
        file = self._get_file(key)
        try:
            await bfile.write_pickle(file, data)
            await self._update_deadline(
                file,
                btime.datetime() + duration if duration else END_DATETIME,
            )
            return True
        except Exception:
            bpath.remove(file)
            if file in self._deadline:
                await self._update_deadline(file, None)
            return False

    async def get(self, key: str):
        file = self._get_file(key)
        if file in self._deadline:
            if btime.datetime() > self._deadline[file]:
                await self._update_deadline(file, None)
                bpath.remove(file)
                return
            if file.is_file():
                return await bfile.read_pickle(file)
            else:
                await self._update_deadline(file, None)
        else:
            bpath.remove(file)

    async def clear(self, key: str):
        bpath.remove(self._get_file(key))

    def _get_file(self, key: str):
        return self._cache_path / bfunc.crc_str(key)

    async def _update_deadline(self, file: Path, deadline: datetime | None):
        if deadline:
            self._deadline[file] = deadline
        else:
            del self._deadline[file]
        if not self._updating:
            self._updating = True
            asyncio.create_task(self._write_deadline_file())

    async def _write_deadline_file(self):
        await asyncio.sleep(2)
        self._updating = False
        try:
            await bfile.write_pickle(self._deadline_file, self._deadline)
        except Exception:
            pass

    def cache(self, key: str, duration: timedelta = Null):
        def func(func: bfunc.AsyncFuncType) -> bfunc.AsyncFuncType:
            @wraps(func)
            async def wrapper(*args: Any, **kwargs: Any):
                try:
                    cache_result = await self.get(key)
                    if cache_result is not None:
                        return cache_result
                    else:
                        result = await func(*args, **kwargs)
                        await self.put(key, result, duration)
                        return result
                except Exception:
                    await self.clear(key)
                    raise

            return to_any(wrapper)

        return func


# ------------------------------------------------------------------------------------------


async def get(key: str, default: Any = None):
    store_file = _get_store_file(key)
    if store_file.is_file():
        return await bfile.read_pickle(store_file)
    else:
        return default


async def set(key: str, value: Any):
    store_file = _get_store_file(key)
    await bfile.write_pickle(store_file, value)


async def clear(*keys: str):
    bpath.remove(*[_get_store_file(key) for key in keys])


async def clear_all():
    files = bpath.list_file(_store_path)
    bpath.remove(*files)


_store_path: Final = bpath.workspace('.store')


def _get_store_file(key: str):
    return bpath.get(_store_path, f'{crc_str(key)}.dat')
