import os
import pickle
import re
import sqlite3
import sys
import threading
import traceback
from base64 import b64decode, b64encode
from datetime import datetime, timedelta
from hashlib import sha256

import xbmc
import xbmcaddon
import xbmcgui

from lib.jacktook.utils import kodilog

PY3 = sys.version_info.major >= 3
if PY3:
    from xbmcvfs import translatePath
else:
    from xbmc import translatePath

ADDON_DATA = translatePath(xbmcaddon.Addon("plugin.video.jacktook").getAddonInfo("profile"))
ADDON_ID = xbmcaddon.Addon().getAddonInfo("id")

if not PY3:
    ADDON_DATA = ADDON_DATA.decode("utf-8")

if not os.path.exists(ADDON_DATA):
    os.makedirs(ADDON_DATA)

SQLITE_SETTINGS = {
    "journal_mode": "wal",
    "auto_vacuum": "full",
    "cache_size": 8 * 1024,
    "mmap_size": 64 * 1024 * 1024,
    "synchronous": "normal",
}


def pickle_hash(obj):
    data = pickle.dumps(obj)
    h = sha256()
    h.update(data)
    return h.hexdigest()


class _BaseCache:
    __instance = None

    _load_func = staticmethod(pickle.loads)
    _dump_func = staticmethod(pickle.dumps)
    _hash_func = staticmethod(pickle_hash)

    @classmethod
    def get_instance(cls):
        if cls.__instance is None:
            cls.__instance = cls()
        return cls.__instance

    def _get(self, key, default=None, hashed_key=False, identifier=""):
        result = self.get(self._generate_key(key, hashed_key, identifier))
        return result if result is not None else default

    def _set(self, key, data, expiry_time, hashed_key=False, identifier=""):
        if expiry_time == timedelta(0):
            return  # Do nothing, as it will expire immediately

        self.set(
            self._generate_key(key, hashed_key, identifier),
            data,
            expiry_time,
        )

    def close(self):
        pass

    def _generate_key(self, key, hashed_key=False, identifier=""):
        if not hashed_key:
            key = self._hash_func(key)
        if identifier:
            key += identifier
        return key

    def _process(self, obj):
        return obj

    def _prepare(self, s):
        return s


class RuntimeCache:
    _store = {}

    @classmethod
    def set(cls, key, value):
        cls._store[key] = value

    @classmethod
    def get(cls, key):
        return cls._store.get(key)

    @classmethod
    def delete(cls, key):
        if key in cls._store:
            del cls._store[key]

    @classmethod
    def clear(cls):
        cls._store.clear()


class MemoryCache(_BaseCache):
    def __init__(self, database=ADDON_ID):
        self._window = xbmcgui.Window(10000)
        self._database = database + "."

    def get(self, key):
        data = self._window.getProperty(self._database + key)
        if data:
            try:
                decoded_data = self._load_func(b64decode(data))
                # Check for (data, expires) tuple format
                if isinstance(decoded_data, tuple) and len(decoded_data) == 2:
                    val, expires = decoded_data
                    if isinstance(expires, datetime):
                        if expires > datetime.utcnow():
                            return val
                        else:
                            self.delete(key)
                            return None
                return decoded_data
            except Exception:
                return None

    def set(self, key, data, expires=timedelta(hours=24)):
        self._add_key_to_index(key)
        try:
            # Wrap data with expiry
            expiry_dt = datetime.utcnow() + expires
            val_to_store = (data, expiry_dt)

            blob = self._dump_func(val_to_store)
            self._window.setProperty(self._database + key, b64encode(blob).decode())
        except Exception as e:
            kodilog(
                f"[MemoryCache] Error storing key {key!r}: {e!s}",
                level=xbmc.LOGERROR,
            )
            kodilog(traceback.format_exc(), level=xbmc.LOGERROR)

    def delete(self, key):
        self._remove_key_from_index(key)
        self._window.clearProperty(self._database + key)

    def delete_like(self, pattern):
        # Convert SQL LIKE pattern to regex
        regex_pattern = (
            re.escape(pattern)
            .replace(r"\%", ".*")
            .replace("%", ".*")
            .replace(r"\_", ".")
            .replace("_", ".")
        )
        regex = re.compile(f"^{regex_pattern}$")

        keys = self._get_key_index()
        to_delete = [k for k in keys if regex.match(k)]

        for k in to_delete:
            self.delete(k)

    def clean_all(self):
        keys = self._get_key_index()
        for k in keys:
            self._window.clearProperty(self._database + k)
        self._window.clearProperty(self._database + "_KEY_INDEX_")

    def _get_key_index(self):
        try:
            data = self._window.getProperty(self._database + "_KEY_INDEX_")
            if data:
                return self._load_func(b64decode(data))
        except Exception:
            pass
        return set()

    def _save_key_index(self, index):
        try:
            blob = self._dump_func(index)
            self._window.setProperty(self._database + "_KEY_INDEX_", b64encode(blob).decode())
        except Exception:
            pass

    def _add_key_to_index(self, key):
        index = self._get_key_index()
        if key not in index:
            index.add(key)
            self._save_key_index(index)

    def _remove_key_from_index(self, key):
        index = self._get_key_index()
        if key in index:
            index.remove(key)
            self._save_key_index(index)


class HybridCache(_BaseCache):
    def __init__(self):
        self.memory = MemoryCache()
        self.db = SQLiteCache()

    def get(self, key):
        # L1: Memory
        data = self.memory.get(key)
        if data is not None:
            return data

        # L2: Database
        data = self.db.get(key)
        if data is not None:
            self.memory.set(key, data, expires=timedelta(minutes=30))
            return data
        return None

    def set(self, key, data, expires=timedelta(hours=24)):
        # Write to both
        self.memory.set(key, data, expires)
        self.db.set(key, data, expires)

    def delete(self, key):
        self.memory.delete(key)
        self.db.delete(key)

    def add_to_list(self, key, item, expires):
        existing_data = self.get_list(key)
        if item in existing_data:
            existing_data.remove(item)
        existing_data.append(item)
        self.set(key, existing_data, expires)

    def get_list(self, key):
        result = self.get(key)
        if result:
            seen = set()
            deduped = []
            for item in reversed(result):
                t = tuple(item)
                if t not in seen:
                    seen.add(t)
                    deduped.append(t)
            deduped.reverse()
            return deduped
        return []

    def clear_list(self, key):
        self.set(key, [], expires=timedelta(seconds=1))

    def clean_all(self):
        self.memory.clean_all()
        self.db.clean_all()

    def delete_like(self, pattern):
        self.memory.delete_like(pattern)
        self.db.delete_like(pattern)


class SQLiteCache(_BaseCache):
    def __init__(
        self,
        database=os.path.join(ADDON_DATA, ADDON_ID + ".cached.sqlite"),
        cleanup_interval=timedelta(minutes=15),
    ):
        self._conn = sqlite3.connect(
            database,
            detect_types=sqlite3.PARSE_DECLTYPES,
            isolation_level=None,
            check_same_thread=False,
        )
        self._conn.execute(
            "CREATE TABLE IF NOT EXISTS `cached` ("
            "key TEXT PRIMARY KEY NOT NULL, "
            "data BLOB NOT NULL, "
            "expires TIMESTAMP NOT NULL"
            ")"
        )
        for k, v in SQLITE_SETTINGS.items():
            self._conn.execute(f"PRAGMA {k}={v}")
        self._cleanup_interval = cleanup_interval
        self._last_cleanup = datetime.utcnow()
        self.clean_up()
        self._object_store = {}  # store raw objects that can't be pickled
        self._lock = threading.Lock()

    def _process(self, obj):
        return self._load_func(obj)

    def _prepare(self, s):
        return self._dump_func(s)

    def add_to_list(self, key, item, expires):
        """Append an item to a list stored under the given key."""
        existing_data = self.get_list(key)
        if item in existing_data:
            existing_data.remove(item)
        existing_data.append(item)
        self.set(
            key,
            existing_data,
            expires,
        )

    def get_list(self, key):
        """Retrieve the list stored under the given key."""
        result = self.get(key)
        if result:
            seen = set()
            deduped = []
            for item in reversed(result):
                t = tuple(item)
                if t not in seen:
                    seen.add(t)
                    deduped.append(t)
            deduped.reverse()
            return deduped
        return []

    def get(self, key):
        with self._lock:
            if key in self._object_store:
                data, expires = self._object_store[key]
                if expires > datetime.utcnow():
                    return data
                else:
                    del self._object_store[key]
                    return None
            self.check_clean_up()
            try:
                result = self._conn.execute(
                    "SELECT data, expires FROM `cached` WHERE key = ?", (key,)
                ).fetchone()
            except Exception as e:
                kodilog(f"SQL error for key {key!r}: {e}", level=xbmc.LOGERROR)
                kodilog(traceback.format_exc(), level=xbmc.LOGERROR)
                return None
            if result:
                data, expires = result
                if expires > datetime.utcnow():
                    return self._process(data)
            return None

    def set(self, key, data, expires):
        with self._lock:
            try:
                self.check_clean_up()
                self._conn.execute(
                    "INSERT OR REPLACE INTO `cached` (key, data, expires) VALUES(?, ?, ?)",
                    (
                        key,
                        sqlite3.Binary(self._prepare(data)),
                        datetime.utcnow() + expires,
                    ),
                )
                kodilog(
                    f"Set cache for key '{key}' with expiry {expires}",
                    level=xbmc.LOGDEBUG,
                )
            except Exception as e:
                kodilog(
                    f"Failed to set cache for key '{key}': {e!s}",
                    level=xbmc.LOGERROR,
                )
                # fallback to raw in‑memory store
                self._object_store[key] = (data, expires)

    def clear_list(self, key):
        """Clear the list stored under the given key."""
        self.set(
            key,
            self._prepare([]),
            datetime.utcnow(),  # Set an immediate expiry to clear the list
        )

    def delete(self, key):
        """Remove a single key from the SQLite store."""
        self._conn.execute("DELETE FROM `cached` WHERE key = ?", (key,))

    def delete_like(self, pattern):
        """Remove keys matching a pattern from the SQLite store."""
        self._conn.execute("DELETE FROM `cached` WHERE key LIKE ?", (pattern,))

    def _set_version(self, version):
        self._conn.execute(f"PRAGMA user_version={version}")

    @property
    def version(self):
        return self._conn.execute("PRAGMA user_version").fetchone()[0]

    @property
    def needs_cleanup(self):
        return self._last_cleanup + self._cleanup_interval < datetime.utcnow()

    def clean_up(self):
        self._conn.execute(
            "DELETE FROM `cached` WHERE expires <= STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')"
        )
        self._last_cleanup = datetime.utcnow()

    def clean_all(self):
        self._conn.execute("DELETE FROM cached")
        self._object_store.clear()

    def check_clean_up(self):
        clean_up = self.needs_cleanup
        if clean_up:
            self.clean_up()
        return clean_up

    def close(self):
        self._conn.close()


cache = HybridCache()
