# -*- coding: utf-8 -*-
# plugin.video.alkoflix
# resources/lib/modules/cache.py

from modules import kodi_utils
# logger = kodi_utils.logger

ls = kodi_utils.local_string
navigator_db = kodi_utils.navigator_db
watched_db = kodi_utils.watched_db
favourites_db = kodi_utils.favourites_db
views_db = kodi_utils.views_db
trakt_db = kodi_utils.trakt_db
mdbl_db = kodi_utils.mdbl_db
maincache_db = kodi_utils.maincache_db
metacache_db = kodi_utils.metacache_db
debridcache_db = kodi_utils.debridcache_db
external_db = kodi_utils.external_db
current_dbs = kodi_utils.current_dbs
databases_path = kodi_utils.databases_path
packages_path = kodi_utils.packages_path
database_connect = kodi_utils.database_connect

# Création et validation des bases SQLite utilisées par alkoFlix.
def check_databases():
	# Crée le dossier des bases de données si celui-ci n'existe pas encore.
	if not kodi_utils.path_exists(databases_path): kodi_utils.make_directory(databases_path)
	dbcon = database_connect(navigator_db) # Navigateur
	dbcon.execute("""CREATE TABLE IF NOT EXISTS navigator
					(list_name text, list_type text, list_contents text, unique(list_name, list_type))""")
	dbcon.close()
	dbcon = database_connect(watched_db) # Statut de visionnement
	dbcon.execute("""CREATE TABLE IF NOT EXISTS watched_status
					(db_type text, media_id text, season integer, episode integer, last_played text, title text, unique(db_type, media_id, season, episode))""")
	dbcon.execute("""CREATE TABLE IF NOT EXISTS progress
					(db_type text, media_id text, season integer, episode integer, resume_point text, curr_time text,
					last_played text, resume_id integer, title text, unique(db_type, media_id, season, episode))""")
	dbcon.close()
	dbcon = database_connect(favourites_db) # Favoris
	dbcon.execute("""CREATE TABLE IF NOT EXISTS favourites (db_type text, tmdb_id text, title text, added_at text, unique (db_type, tmdb_id))""")
	dbcon.execute("""CREATE TABLE IF NOT EXISTS favourite_tombstones (db_type text, tmdb_id text, title text, deleted_at text, PRIMARY KEY (db_type, tmdb_id))""")
	try:
		columns = [i[1] for i in dbcon.execute("""PRAGMA table_info(favourites)""").fetchall()]
		if 'added_at' not in columns:
			dbcon.execute("""ALTER TABLE favourites ADD COLUMN added_at text""")
			dbcon.execute("""UPDATE favourites SET added_at = datetime('now', 'localtime') WHERE added_at IS NULL OR added_at = ''""")
	except: pass
	dbcon.close()
	dbcon = database_connect(views_db) # Vues
	dbcon.execute("""CREATE TABLE IF NOT EXISTS views (view_type text, view_id text, unique (view_type))""")
	dbcon.close()
	dbcon = database_connect(maincache_db) # Cache principal
	dbcon.execute("""CREATE TABLE IF NOT EXISTS maincache (id text unique, data text, expires integer)""")
	dbcon.close()
	dbcon = database_connect(metacache_db) # Cache des métadonnées
	dbcon.execute("""CREATE TABLE IF NOT EXISTS metadata
					(db_type text not null, tmdb_id text not null, imdb_id text, tvdb_id text, meta text, expires integer, unique (db_type, tmdb_id))""")
	dbcon.execute("""CREATE TABLE IF NOT EXISTS season_metadata (tmdb_id text not null unique, meta text, expires integer)""")
	dbcon.execute("""CREATE TABLE IF NOT EXISTS function_cache (string_id text not null, data text, expires integer)""")
	dbcon.execute("""CREATE INDEX IF NOT EXISTS alkoflix_select_id_media ON metadata (tmdb_id, db_type)""")
	dbcon.close()
	dbcon = database_connect(debridcache_db) # Cache Debrid
	dbcon.execute("""CREATE TABLE IF NOT EXISTS debrid_data (hash text not null, debrid text not null, cached text, expires integer, unique (hash, debrid))""")
	dbcon.close()
	dbcon = database_connect(external_db) # Cache des providers externes
	dbcon.execute("""CREATE TABLE IF NOT EXISTS results_data
					(provider text, db_type text, tmdb_id text, title text, year integer, season text, episode text, results text,
					expires integer, unique (provider, db_type, tmdb_id, title, year, season, episode))""")
	dbcon.execute("""CREATE TABLE IF NOT EXISTS provider_hashes
					(provider text not null, hash text not null, result text, expires integer, last_seen integer,
					unique(provider, hash))""")
	dbcon.execute("""CREATE TABLE IF NOT EXISTS provider_hash_map
					(provider text not null, db_type text not null, media_id text, title text, year text, season text,
					episode text, hash text not null, expires integer,
					unique(provider, db_type, media_id, title, year, season, episode, hash))""")
	dbcon.execute("""CREATE INDEX IF NOT EXISTS alkoflix_provider_hash_map_media ON provider_hash_map (provider, db_type, media_id, season, episode)""")
	dbcon.execute("""CREATE INDEX IF NOT EXISTS alkoflix_provider_hash_map_title ON provider_hash_map (provider, db_type, title, year, season, episode)""")
	dbcon.execute("""CREATE INDEX IF NOT EXISTS alkoflix_provider_hashes_expire ON provider_hashes (expires)""")
	dbcon.execute("""CREATE INDEX IF NOT EXISTS alkoflix_provider_hash_map_expire ON provider_hash_map (expires)""")
	dbcon.close()
	dbcon = database_connect(trakt_db) # Données Trakt
	dbcon.execute("""CREATE TABLE IF NOT EXISTS trakt_data (id text unique, data text)""")
	dbcon.execute("""CREATE TABLE IF NOT EXISTS watched_status
					(db_type text, media_id text, season integer, episode integer, last_played text, title text, unique(db_type, media_id, season, episode))""")
	dbcon.execute("""CREATE TABLE IF NOT EXISTS progress
					(db_type text, media_id text, season integer, episode integer, resume_point text, curr_time text,
					last_played text, resume_id integer, title text, unique(db_type, media_id, season, episode))""")
	dbcon.close()
	dbcon = database_connect(mdbl_db) # Données MDBList
	dbcon.execute("""CREATE TABLE IF NOT EXISTS mdbl_data (id text unique, data text)""")
	dbcon.execute("""CREATE TABLE IF NOT EXISTS watched_status
					(db_type text, media_id text, season integer, episode integer, last_played text, title text, unique(db_type, media_id, season, episode))""")
	dbcon.execute("""CREATE TABLE IF NOT EXISTS progress
					(db_type text, media_id text, season integer, episode integer, resume_point text, curr_time text,
					last_played text, resume_id integer, title text, unique(db_type, media_id, season, episode))""")
	dbcon.close()

# Supprime les anciennes bases qui ne font plus partie de la liste active.
def remove_old_databases():
	files = kodi_utils.list_dirs(databases_path)[1]
	for item in files:
		if item not in current_dbs:
			try: kodi_utils.delete_file(databases_path + item)
			except: pass

# Nettoie les anciens paquets ZIP alkoFlix laissés dans le dossier packages de Kodi.
def remove_old_packages():
	files = kodi_utils.list_dirs(packages_path)[1]
	for item in files:
		if '.alkoflix' in item and item.endswith('zip'):
			try: kodi_utils.delete_file(packages_path + item)
			except: pass

# Supprime les entrées expirées dans les caches et compacte les bases au besoin.
def clean_databases(current_time=None, database_check=True, silent=False):
	remove_old_packages()
	remove_old_databases()
	if database_check: check_databases()
	current_time = current_time or get_current_time()
	command_base = 'DELETE from %s WHERE CAST(%s AS INT) <= ?'
	for db, sql in (
		(external_db, command_base % ('results_data', 'expires')),
		(external_db, command_base % ('provider_hashes', 'expires')),
		(external_db, command_base % ('provider_hash_map', 'expires')),
		(debridcache_db, command_base % ('debrid_data', 'expires')),
		(maincache_db, command_base % ('maincache', 'expires')),
		(metacache_db, command_base % ('metadata', 'expires')),
		(metacache_db, command_base % ('function_cache', 'expires')),
		(metacache_db, command_base % ('season_metadata', 'expires'))
	):
		try:
			dbcon = database_connect(db)
			dbcur = dbcon.cursor()
			# Accélère les opérations de nettoyage; acceptable ici puisque les données sont du cache.
			dbcur.execute("""PRAGMA synchronous = OFF""")
			dbcur.execute("""PRAGMA journal_mode = OFF""")
			dbcur.execute(sql, (current_time,))
			dbcon.commit()
			# Réduit la taille du fichier SQLite après suppression des données expirées.
			dbcur.execute("""VACUUM""")
		except: pass
	limit_metacache_database()
	if not silent: kodi_utils.notification(32576, 1500)

# Limite la taille du cache des métadonnées pour éviter une base trop lourde.
def limit_metacache_database(max_size=50):
	with kodi_utils.open_file(metacache_db) as f: fsize = f.size()
	size = round(float(fsize)/1048576, 1)
	if size < max_size: return
	dbcon = database_connect(metacache_db)
	dbcur = dbcon.cursor()
	dbcur.execute("""PRAGMA synchronous = OFF""")
	dbcur.execute("""PRAGMA journal_mode = OFF""")
	# Conserve les entrées les plus récentes et retire l'excédent.
	dbcur.execute("""DELETE FROM metadata WHERE ROWID IN (SELECT ROWID FROM metadata ORDER BY ROWID DESC LIMIT -1 OFFSET 4000)""")
	dbcur.execute("""DELETE FROM function_cache WHERE ROWID IN (SELECT ROWID FROM function_cache ORDER BY ROWID DESC LIMIT -1 OFFSET 100)""")
	dbcur.execute("""DELETE FROM season_metadata WHERE ROWID IN (SELECT ROWID FROM season_metadata ORDER BY ROWID DESC LIMIT -1 OFFSET 100)""")
	dbcon.commit()
	dbcur.execute("""VACUUM""")

# Retourne l'heure courante en timestamp local pour comparer les expirations de cache.
def get_current_time():
	import time, datetime
	return int(time.mktime(datetime.datetime.now().timetuple()))

# Supprime les entrées du cache principal qui correspondent à un préfixe précis.
def _clear_maincache_like(pattern):
	try:
		dbcon = database_connect(maincache_db)
		dbcur = dbcon.cursor()
		dbcur.execute("""PRAGMA synchronous = OFF""")
		dbcur.execute("""PRAGMA journal_mode = OFF""")
		dbcur.execute("""SELECT id FROM maincache WHERE id LIKE ?""", (pattern,))
		remove_list = [str(i[0]) for i in dbcur.fetchall()]
		if remove_list:
			dbcur.execute("""DELETE FROM maincache WHERE id LIKE ?""", (pattern,))
			dbcon.commit()
			dbcur.execute("""VACUUM""")
			for item in remove_list: kodi_utils.clear_property(item)
		dbcon.close()
		return True
	except: return False

# Nettoie les fichiers temporaires créés par le service de sous-titres Wyzie.
def _clear_wyzie_temp_subtitles():
	try:
		subtitle_path = 'special://temp/'
		for item in kodi_utils.list_dirs(subtitle_path)[1]:
			item_lower = item.lower()
			if item.startswith('alkoFlixSubs_') and item_lower.endswith('.srt'):
				kodi_utils.delete_file('%s%s' % (subtitle_path, item))
		return True
	except: return False

# Vide un type de cache précis selon l'action demandée par l'utilisateur.
def clear_cache(cache_type, silent=False):
	def _confirm():
		# En mode silencieux, aucune confirmation utilisateur n'est demandée.
		return silent or kodi_utils.confirm_dialog()
	success = True
	if cache_type == 'meta':
		if not _confirm(): return
		from caches.meta_cache import MetaCache
		MetaCache().delete_all()
	elif cache_type == 'internal_scrapers':
		if not _confirm(): return
		items = 'ad_cloud', 'rd_cloud', 'tb_cloud', 'folders'
		for item in items: clear_cache(item, silent=True)
	elif cache_type == 'external_scrapers':
		if not _confirm(): return
		from caches.providers_cache import ExternalProvidersCache
		from caches.debrid_cache import DebridCache
		data = ExternalProvidersCache().delete_cache()
		debrid_cache = DebridCache().clear_database()
		success = (data, debrid_cache) == ('success', 'success')
	elif cache_type == 'easynews':
		if not _confirm(): return
		from debrids.easynews_api import clear_media_results_database
		success = clear_media_results_database() == 'success'
	elif cache_type == 'wyzie':
		if not _confirm(): return
		cache_success = _clear_maincache_like('subtitles_wyzie_%')
		temp_success = _clear_wyzie_temp_subtitles()
		success = cache_success or temp_success
	elif cache_type == 'trakt':
		if not _confirm(): return
		from caches.trakt_cache import clear_all_trakt_cache_data
		success = clear_all_trakt_cache_data(reason='manual_cache_clear')
		try:
			if 'alkoflix' in kodi_utils.get_infolabel('Container.PluginName').lower(): kodi_utils.container_refresh()
		except: pass
	elif cache_type == 'mdblist':
		if not _confirm(): return
		from indexers.mdblist_api import clear_mdbl_cache
		from caches.mdbl_cache import clear_all_mdbl_cache_data
		success = clear_mdbl_cache() and clear_all_mdbl_cache_data()
	elif cache_type == 'tmdblist':
		if not _confirm(): return
		from indexers.tmdb_api import clear_tmdbl_cache
		success = clear_tmdbl_cache()
	elif cache_type == 'tmdb_negative':
		if not _confirm(): return
		from indexers.tmdb_api import clear_tmdb_negative_cache
		success = clear_tmdb_negative_cache()
	elif cache_type == 'imdb':
		if not _confirm(): return
		from indexers.imdb_api import clear_imdb_cache
		success = clear_imdb_cache()
	elif cache_type == 'ad_cloud':
		if not _confirm(): return
		from debrids.alldebrid_api import AllDebridAPI
		success = AllDebridAPI().clear_cache()
	elif cache_type == 'rd_cloud':
		if not _confirm(): return
		from debrids.real_debrid_api import RealDebridAPI
		success = RealDebridAPI().clear_cache()
	elif cache_type == 'tb_cloud':
		if not _confirm(): return
		from debrids.torbox_api import TorBoxAPI
		success = TorBoxAPI().clear_cache()
	elif cache_type == 'folders':
		if not _confirm(): return
		from caches.main_cache import MainCache
		MainCache().delete_all_folderscrapers()
	elif cache_type in ('navigator', 'menus'):
		if not _confirm(): return
		from caches.navigator_cache import navigator_cache
		stats = navigator_cache.clear_menu_cache()
		success = bool(stats)
		try:
			if 'alkoflix' in kodi_utils.get_infolabel('Container.PluginName').lower(): kodi_utils.container_refresh()
		except: pass
	elif cache_type in ('list', 'lists'):
		if not _confirm(): return
		from caches.main_cache import MainCache
		MainCache().delete_all_lists()
	else:
		success = False
		try: kodi_utils.logger('clear cache skipped', 'unknown cache type: %s' % cache_type)
		except: pass
	if not silent and success: kodi_utils.notification(32576, 1500)
	elif not silent: kodi_utils.notification(32574, 1500)

# Liste les caches propres au fonctionnement interne de l'addon.
def _internal_cache_items():
	return [
		('meta', '%s %s' % (ls(32527), ls(32524))),
		('list', '%s %s' % (ls(32815), ls(32524))),
		# navigator.db contient les menus édités et les dossiers perso.
		# Il ne doit jamais être inclus dans le vidage global des caches.
		('external_scrapers', 'Scrapers externes'),
		('folders', 'Dossiers de liens'),
		('tmdb_negative', 'Cache négatif TMDb')
	]

# Liste les caches liés aux services connectés ou externes configurés par l'utilisateur.
def _service_cache_items():
	from modules import settings
	from modules.debrid import debrid_enabled
	enabled_debrids = debrid_enabled()
	caches = []
	if kodi_utils.get_setting('trakt_user') not in ('', None):
		caches.append(('trakt', ls(32087)))
	if kodi_utils.get_setting('mdblist.access_token') not in ('', None) or kodi_utils.get_setting('mdblist.token') not in ('', None):
		caches.append(('mdblist', 'MDBList'))
	if kodi_utils.get_setting('tmdb.token') not in ('', None) or kodi_utils.get_setting('tmdb.session_id') not in ('', None):
		caches.append(('tmdblist', 'TMDbLists'))
	if kodi_utils.get_setting('imdb_user') not in ('', None):
		caches.append(('imdb', '%s %s' % (ls(32064), ls(32524))))
	if kodi_utils.get_setting('subtitles.apikey') not in ('', None):
		caches.append(('wyzie', 'Wyzie'))
	if 'alldebrid' in enabled_debrids:
		caches.append(('ad_cloud', ls(32063)))
	if 'real-debrid' in enabled_debrids:
		caches.append(('rd_cloud', ls(32054)))
	if 'torbox' in enabled_debrids:
		caches.append(('tb_cloud', 'TorBox'))
	return caches

# Vide une liste de caches en une seule opération avec affichage de progression Kodi.
def _clear_cache_batch(caches):
	if not kodi_utils.confirm_dialog(): return False
	line = '[CR]%s: [B]%s[/B]'
	if not caches:
		kodi_utils.notification(32576, 1500)
		return True
	completed = True
	kodi_utils.progressDialog.create('alkoFlix', '')
	try:
		for count, (cache_type, cache_label) in enumerate(caches, 1):
			try:
				if kodi_utils.progressDialog.iscanceled():
					completed = False
					break
				kodi_utils.progressDialog.update(int(count / len(caches) * 100), line % (ls(32816), cache_label))
				clear_cache(cache_type, silent=True)
				kodi_utils.sleep(200)
			except:
				completed = False
				kodi_utils.notification(32574, 1500)
	finally:
		kodi_utils.progressDialog.close()
	return completed


def _rebuild_alkopaste_cache_after_clear():
	# La source ALKO est elle aussi un cache local. Après un vidage global,
	# on la reconstruit immédiatement pour éviter un sélecteur vide jusqu'au prochain clic manuel.
	try:
		if str(kodi_utils.get_setting('provider.alkopaste', 'true')).strip().lower() != 'true':
			return
		from magneto.alkopaste import sync_alkopaste_database
		kodi_utils.notification('Reconstruction de la source ALKO en cours...')
		result = sync_alkopaste_database(force=True, rebuild=True)
		if result.get('ok'):
			kodi_utils.notification('Source ALKO reconstruite : %s entrées indexées.' % result.get('links', 0), 2500)
		else:
			kodi_utils.ok_dialog(heading='alkoFlix', text='Les caches ont été vidés, mais la source ALKO n’a pas pu être reconstruite. Raison : %s' % (result.get('reason') or 'erreur inconnue'), top_space=True)
	except Exception as e:
		try: kodi_utils.logger('Source ALKO cache rebuild failed', '%s: %s' % (type(e).__name__, e))
		except: pass


# Vide uniquement les caches internes de l'addon.
def clear_internal_cache():
	if not _clear_cache_batch(_internal_cache_items()): return
	_rebuild_alkopaste_cache_after_clear()

# Vide uniquement les caches des services connectés ou externes.
def clear_services_cache():
	_clear_cache_batch(_service_cache_items())

# Vide les principaux caches en une seule opération avec affichage de progression Kodi.
def clear_all_cache():
	_clear_cache_batch(_internal_cache_items() + _service_cache_items())
