import pickle
import traceback
from math import ceil
from threading import RLock
from time import time
from typing import Optional

from app.runtime.cache import FileCache, TTLCache
from app.runtime.settings import RuntimeSettingsCompat

settings = RuntimeSettingsCompat()
from app.domain.context import MusicInfo
from app.domain.meta.metamusic import MetaMusic
from app.runtime.log import logger
from app.schemas.types import MUSIC_ENTITY_RECORDING
from app.foundation.singleton import WeakSingleton

lock = RLock()
PERSISTENCE_VERSION = 1
PERSISTENCE_REGION = "recognize"
PERSISTENCE_KEY = "musicbrainz"


class MusicBrainzCache(metaclass=WeakSingleton):
    """
    MusicBrainz识别缓存数据
    {
        "source": '',
        "media_id": '',
        "title": '',
        "artists": [],
        "album": '',
        "year": '',
        "music_type": ''
    }
    """

    def __init__(self):
        """初始化音乐识别缓存并恢复未过期的持久化数据。"""
        self.maxsize = settings.CONF.musicbrainz
        self.ttl = settings.CONF.meta
        self.region = "__musicbrainz_cache__"
        self._cache = TTLCache(region=self.region, maxsize=self.maxsize, ttl=self.ttl)
        self._expires_at: dict[str, float] = {}
        self._dirty = False
        self._file_cache = None
        if not self._cache.is_redis():
            self._file_cache = FileCache(base=settings.CACHE_PATH, ttl=self.ttl)
            self._restore()

    def _restore(self) -> None:
        """从统一文件缓存恢复仍在有效期内的音乐识别数据。"""
        try:
            content = self._file_cache.get(PERSISTENCE_KEY, region=PERSISTENCE_REGION)
            if not content:
                return
            payload = pickle.loads(content)
            now = time()
            if (
                    not isinstance(payload, dict)
                    or payload.get("version") != PERSISTENCE_VERSION
                    or not isinstance(payload.get("items"), dict)
            ):
                return

            for key, item in payload["items"].items():
                if not isinstance(item, dict):
                    self._dirty = True
                    continue
                value = item.get("value")
                expires_at = item.get("expires_at")
                if not isinstance(value, dict) or not isinstance(expires_at, (int, float)):
                    self._dirty = True
                    continue
                remaining_ttl = expires_at - now
                if remaining_ttl <= 0:
                    self._dirty = True
                    continue
                self._cache.set(key, value, ttl=ceil(remaining_ttl))
                self._expires_at[key] = expires_at
        except Exception as err:
            logger.error(f"加载音乐识别缓存失败：{str(err)} - {traceback.format_exc()}")

    def _set(self, key: str, value: dict) -> None:
        """写入单条音乐识别缓存并记录其独立过期时间。"""
        self._cache.set(key, value)
        if not self._cache.is_redis():
            self._expires_at[key] = time() + self.ttl
            self._dirty = True

    def clear(self):
        """
        清空所有音乐识别缓存
        """
        with lock:
            self._cache.clear()
            self._expires_at.clear()
            self._dirty = True
            self.save(force=True)

    def list_items(self) -> list[dict]:
        """
        返回可供管理界面展示的音乐识别缓存列表。
        """
        with lock:
            cache_items = []
            for key, value in self._cache.items():
                if not isinstance(value, dict):
                    continue
                cache_items.append({
                    "key": key,
                    "media_id": value.get("media_id") or "",
                    "title": value.get("title") or "",
                    "artists": value.get("artists") or [],
                    "album": value.get("album") or "",
                    "year": value.get("year") or "",
                    "music_type": value.get("music_type") or MUSIC_ENTITY_RECORDING,
                    "cover_url": value.get("cover_url") or "",
                })
            return sorted(cache_items, key=lambda item: item["key"])

    @staticmethod
    def __get_key(meta: MetaMusic) -> str:
        """
        获取缓存KEY，携带数据源原生 ID 时以 ID 为准身份
        """
        artists = "/".join(meta.artists or [])
        return f"[音乐]{meta.media_id or meta.title}-{artists}-{meta.album}-{meta.year}"

    def get(self, meta: MetaMusic) -> Optional[MusicInfo]:
        """
        根据元数据获取缓存的音乐识别结果
        @param meta: 音乐元数据
        @return: 缓存命中的音乐信息，未命中返回 None
        """
        key = self.__get_key(meta)
        with lock:
            cache_data = self._cache.get(key)
            if not cache_data and self._expires_at.pop(key, None) is not None:
                self._dirty = True
        if not cache_data:
            return None
        try:
            return MusicInfo.from_dict(cache_data)
        except Exception as err:
            logger.error(f"解析音乐识别缓存失败：{str(err)}")
            return None

    def delete(self, key: str) -> dict:
        """
        删除缓存信息
        @param key: 缓存key
        @return: 被删除的缓存内容
        """
        with lock:
            cache_data = self._cache.get(key)
            if cache_data:
                self._cache.delete(key)
                self._expires_at.pop(key, None)
                self._dirty = True
                self.save(force=True)
                return cache_data
            return {}

    def update(self, meta: MetaMusic, info: Optional[MusicInfo]) -> None:
        """
        新增或更新缓存条目，无远端身份的兜底结果也写入内存负缓存，
        避免批量识别时反复请求 MusicBrainz 触发限流
        """
        if not meta or not info:
            return
        key = self.__get_key(meta)
        cache_data = info.to_dict()
        # 上游原始响应体积大且不参与身份恢复，不入缓存
        cache_data.pop("raw_data", None)
        with lock:
            self._set(key, cache_data)

    def save(self, force: bool = False) -> None:
        """
        使用统一文件缓存保存未过期的音乐识别数据。
        """
        if self._cache.is_redis():
            return
        if not self._file_cache:
            return
        with lock:
            now = time()
            cache_items = dict(self._cache.items())
            active_keys = set(cache_items)
            stale_keys = set(self._expires_at) - active_keys
            if stale_keys:
                for key in stale_keys:
                    self._expires_at.pop(key, None)
                self._dirty = True

            persisted_items = {}
            for key, value in cache_items.items():
                expires_at = self._expires_at.get(key)
                if expires_at is None:
                    expires_at = now + self.ttl
                    self._expires_at[key] = expires_at
                    self._dirty = True
                # 负缓存只留在内存，重启后允许重新尝试识别
                if expires_at <= now or not value.get("media_id"):
                    continue
                persisted_items[key] = {
                    "value": value,
                    "expires_at": expires_at,
                }

            if not force and not self._dirty:
                return

            try:
                if persisted_items:
                    payload = {
                        "version": PERSISTENCE_VERSION,
                        "items": persisted_items,
                    }
                    self._file_cache.set(
                        PERSISTENCE_KEY,
                        pickle.dumps(payload, pickle.HIGHEST_PROTOCOL),
                        region=PERSISTENCE_REGION,
                    )
                else:
                    self._file_cache.delete(PERSISTENCE_KEY, region=PERSISTENCE_REGION)
                self._dirty = False
            except Exception as err:
                logger.error(f"保存音乐识别缓存失败：{str(err)} - {traceback.format_exc()}")

    def __del__(self):
        """实例释放前保存非 Redis 缓存。"""
        try:
            self.save()
        except Exception:
            pass
