# -*- coding: utf-8 -*-
# plugin.video.alkoflix
# resources/lib/magneto/alkopaste.py

import ast
import hashlib
import re
import sqlite3
import time
import unicodedata
from datetime import timedelta
import requests

from caches.main_cache import MainCache
from magneto.hosters.common import BaseHosterSource, clean_text, normalize_quality, language_code, hoster_key
from modules import kodi_utils


# ---------------------------------------------------------------------------
# Configuration interne alkoPaste
# ---------------------------------------------------------------------------
# alkoPaste peut lire :
# - des codes de contenu directs ;
# - des codes index ;
# - des index imbriqués sur plusieurs niveaux.
#
# Exemple d'index :
#   # Films - wanda (Index)
#   c1bf3ab0
#
# Si c1bf3ab0 contient lui-même :
#   # Films
#   a2ccde62
#   ae211c52
#
# alkoFlix descend automatiquement jusqu'aux pastes de contenu.
# Pour ajouter des pastes ensuite, le plus simple est donc de modifier les
# index alkoPaste publics, sans republier l'addon.
ALKOPASTE_BASE_URL = 'https://paste.lesalkodiques.com/raw/%s'

# Codes de contenu directs conservés comme base/fallback.
ALKOPASTE_FILM_CODES = ('a2ccde62',)
ALKOPASTE_SERIES_CODES = ('45dfc4dc',)

# Codes index principaux. Pour ajouter un index racine dans le code :
#   'Nouvelle catégorie': ('codeindex',),
# Les codes contenus dans ces index peuvent eux-mêmes être des index.
ALKOPASTE_INDEX_CODES = {
	'Films': ('1eda79b1',),
	'Séries': ('6730992c',),
	'Docus': ('00c0fdec',),
	'Replay': ('4fcd74af',),
	'Québec': ('5da625b7',),
	'Concerts': ('017cea8a',),
	'Spectacles': ('8545d720',),
	'Dessins animés': ('fff5fe57',),
	'Animés japonais': ('e227f398',),
}

# Catégories index à parcourir selon le type demandé.
# Les index peuvent être mixtes (films + séries dans le même arbre).
# On parcourt donc tous les index publics, puis on filtre les lignes finales
# avec la colonne CAT du paste de contenu.
ALKOPASTE_ALL_INDEX_CATEGORIES = tuple(ALKOPASTE_INDEX_CODES.keys())
ALKOPASTE_MOVIE_INDEX_CATEGORIES = ALKOPASTE_ALL_INDEX_CATEGORIES
ALKOPASTE_SERIES_INDEX_CATEGORIES = ALKOPASTE_ALL_INDEX_CATEGORIES

# Codes supplémentaires mixtes. Chaque entrée peut être :
# {'code': 'xxxxxxxx', 'types': ('film', 'serie')}
ALKOPASTE_EXTRA_CODES = ()

ALLDEBRID_DEFAULT_BASE_URL = 'https://alldebrid.com/f/'
PASTE_CACHE_HOURS = 6
INDEX_CACHE_HOURS = 24
DB_SYNC_INTERVAL_HOURS = 24
INDEX_MAX_DEPTH = 8
# Version des clés de cache : à incrémenter quand la logique de parsing change.
# Cela force un nouveau chargement des raws sans demander aux utilisateurs de vider les caches.
ALKOPASTE_CACHE_VERSION = 'v4'
ALKOPASTE_DB_VERSION = 3
ALKOPASTE_DB = 'special://profile/addon_data/plugin.video.alkoflix/alkopaste.db'

COL_CAT = 0
COL_TMDB = 1
COL_TITLE = 2
COL_SAISON = 3
COL_YEAR = 8
COL_RES = 10
COL_URLS = 11

SERIES_URL_PATTERN = re.compile(r"(\d+)\s*:\s*(?:([\'\"])(.*?)\2|([^,\]\}]+))")
CODE_LINE_PATTERN = re.compile(r'^[A-Za-z0-9]{4,32}$')

SERIES_CAT_KEYS = (
	'serie', 'series', 'tvshow', 'tvshows', 'episode', 'episodes',
	'anime', 'animes', 'animejaponais', 'animesjaponais',
	'daserie', 'daseries', 'dessinsanimeserie', 'dessinsanimesserie',
	'dessinsanimeseries', 'animationserie', 'animationseries'
)
MOVIE_CAT_KEYS = (
	'film', 'films', 'movie', 'movies', 'docu', 'docus', 'documentaire',
	'documentaires', 'documentary', 'replay', 'quebec', 'concert', 'concerts',
	'spectacle', 'spectacles', 'dafilm', 'dafilms', 'dessinsanimesfilm',
	'dessinsanimesfilms', 'animationfilm', 'animationfilms'
)


def _cat_key(value):
	try:
		text = unicodedata.normalize('NFKD', str(value or '').lower())
		text = ''.join(ch for ch in text if not unicodedata.combining(ch))
		return re.sub(r'[^a-z0-9]+', '', text)
	except:
		return ''


def _normalize_cat(value):
	key = _cat_key(value)
	if key in SERIES_CAT_KEYS: return 'serie'
	if key in MOVIE_CAT_KEYS: return 'film'
	return key


def _safe_int(value, default=None):
	try:
		if value in ('', None): return default
		return int(str(value).strip())
	except:
		return default



def _target_tmdb_ids(data):
	ids = []
	try:
		for key in ('tmdb', 'tmdb_id', 'tmdbid'):
			value = str((data or {}).get(key) or '').strip()
			if value and value not in ('0', 'None', 'none', 'null') and value not in ids:
				ids.append(value)
	except:
		pass
	return ids

def _code_url(code):
	code = str(code or '').strip()
	if not code: return ''
	if code.startswith('http://') or code.startswith('https://'): return code
	return ALKOPASTE_BASE_URL % code


def _content_cache_key(url):
	try:
		return 'alkoflix_alkopaste_%s_raw_%s' % (ALKOPASTE_CACHE_VERSION, hashlib.md5(url.encode('utf-8'), usedforsecurity=False).hexdigest())
	except TypeError:
		return 'alkoflix_alkopaste_%s_raw_%s' % (ALKOPASTE_CACHE_VERSION, hashlib.md5(url.encode('utf-8')).hexdigest())


def _index_cache_key(code):
	try:
		return 'alkoflix_alkopaste_%s_index_%s' % (ALKOPASTE_CACHE_VERSION, hashlib.md5(str(code).encode('utf-8'), usedforsecurity=False).hexdigest())
	except TypeError:
		return 'alkoflix_alkopaste_%s_index_%s' % (ALKOPASTE_CACHE_VERSION, hashlib.md5(str(code).encode('utf-8')).hexdigest())


def _fetch_text(url):
	if not url: return ''
	cache_key = _content_cache_key(url)
	cache = MainCache()
	cached = cache.get(cache_key)
	if cached: return cached
	try:
		response = requests.get(url, timeout=12)
		if response.status_code != 200: return ''
		text = response.text or ''
		if text:
			cache.set(cache_key, text, expiration=timedelta(hours=PASTE_CACHE_HOURS))
		return text
	except Exception as e:
		try: kodi_utils.logger('alkoPaste', 'Lecture raw impossible | url=%s | erreur=%s' % (url, type(e).__name__))
		except: pass
		return ''


def _base_url_from_header(header):
	try:
		parts = str(header or '').split(';')
		if len(parts) > COL_URLS:
			value = parts[COL_URLS].strip()
			if '=' in value:
				base = value.split('=', 1)[1].strip()
				if base: return base
	except:
		pass
	return ALLDEBRID_DEFAULT_BASE_URL


def _full_url(base_url, suffix):
	try:
		suffix = str(suffix or '').strip()
		if not suffix: return ''
		if suffix.startswith('http://') or suffix.startswith('https://') or suffix.startswith('magnet:'):
			return suffix
		base_url = str(base_url or ALLDEBRID_DEFAULT_BASE_URL).strip()
		# Les pastes historiques stockent les suffixes AllDebrid à concaténer directement.
		return '%s%s' % (base_url, suffix)
	except:
		return ''



def _literal_list(value):
	try:
		result = ast.literal_eval(value or '[]')
		return result if isinstance(result, list) else []
	except:
		return []


def _series_urls(urls_raw):
	try:
		items = []
		for match in SERIES_URL_PATTERN.findall(urls_raw or ''):
			# match = (episode, quote, quoted_suffix, unquoted_suffix)
			suffix = match[2] or match[3] or ''
			suffix = str(suffix).strip().strip('\"\'')
			if suffix:
				items.append((int(match[0]), suffix))
		return items
	except:
		return []


def _looks_like_content_paste(content):
	try:
		lines = [line.strip() for line in (content or '').splitlines() if line.strip()]
		if not lines: return False
		header_parts = lines[0].split(';')
		if len(header_parts) > COL_URLS:
			return True
		# Fallback pour un raw sans header mais contenant des lignes structurées.
		for line in lines[:5]:
			parts = line.split(';')
			if len(parts) > COL_URLS and _normalize_cat(parts[COL_CAT].strip()) in ('film', 'serie'):
				return True
	except:
		pass
	return False


def _extract_index_codes(content):
	codes = []
	seen = set()
	try:
		for raw_line in (content or '').splitlines():
			line = (raw_line or '').strip().lstrip('\ufeff')
			if not line or line.startswith('#'): continue
			# Accepte les commentaires inline simples : abcd1234 # note
			line = line.split('#', 1)[0].strip()
			if not line: continue
			# Accepte aussi une URL raw complète.
			if line.startswith('http://') or line.startswith('https://'):
				code = line
			else:
				match = CODE_LINE_PATTERN.match(line)
				if not match: continue
				code = line
			if code not in seen:
				seen.add(code)
				codes.append(code)
	except:
		pass
	return codes


def _resolve_index_code(code, depth=0, visited=None):
	visited = visited or set()
	code = str(code or '').strip()
	if not code or code in visited or depth > INDEX_MAX_DEPTH: return []
	visited.add(code)
	cache = MainCache()
	cache_key = _index_cache_key('%s:%s' % (code, depth))
	cached = cache.get(cache_key)
	if cached:
		try:
			return list(cached)
		except:
			pass

	url = _code_url(code)
	content = _fetch_text(url)
	if not content: return []

	if _looks_like_content_paste(content):
		resolved = [code]
	else:
		resolved = []
		for child_code in _extract_index_codes(content):
			for leaf_code in _resolve_index_code(child_code, depth + 1, visited):
				if leaf_code not in resolved:
					resolved.append(leaf_code)

	try:
		cache.set(cache_key, resolved, expiration=timedelta(hours=INDEX_CACHE_HOURS))
	except:
		pass
	return resolved


def _codes_from_categories(categories):
	codes = []
	for category in categories:
		for code in ALKOPASTE_INDEX_CODES.get(category, ()): 
			if code not in codes: codes.append(code)
	return codes


def _code_entries_for_media(media_type):
	root_codes = []
	wanted = 'serie' if media_type == 'episode' else 'film'
	if wanted == 'film':
		root_codes.extend(ALKOPASTE_FILM_CODES)
		root_codes.extend(_codes_from_categories(ALKOPASTE_MOVIE_INDEX_CATEGORIES))
	else:
		root_codes.extend(ALKOPASTE_SERIES_CODES)
		root_codes.extend(_codes_from_categories(ALKOPASTE_SERIES_INDEX_CATEGORIES))
	for item in ALKOPASTE_EXTRA_CODES:
		try:
			code = item.get('code') if isinstance(item, dict) else item
			types = item.get('types', ('film', 'serie')) if isinstance(item, dict) else ('film', 'serie')
			if wanted in types and code not in root_codes: root_codes.append(code)
		except:
			pass

	content_codes = []
	for code in root_codes:
		for leaf_code in _resolve_index_code(code):
			if leaf_code not in content_codes:
				content_codes.append(leaf_code)
	return content_codes, wanted


def _release_language(value):
	try:
		text = str(value or '')
		if re.search(r'(?i)(?:^|[ ._\-])(?:vostfr|subfrench)(?:$|[ ._\-])', text): return 'fr'
		if re.search(r'(?i)(?:^|[ ._\-])(?:multi|french|truefrench|true[ ._\-]?french|vfq|vff|vf2|vfi|vf|fr|fre|fra)(?:$|[ ._\-])', text): return 'fr'
	except:
		pass
	return language_code(value)



def _alkopaste_db_related_files():
	return (ALKOPASTE_DB, ALKOPASTE_DB + '-journal', ALKOPASTE_DB + '-wal', ALKOPASTE_DB + '-shm')


def _delete_alkopaste_database_files():
	# La source ALKO est un index local jetable : aucune migration lourde, aucun journal SQLite
	# à conserver. Lors d'une mise à jour, on supprime tout et on reconstruit une base propre.
	removed = 0
	for db_file in _alkopaste_db_related_files():
		try:
			if kodi_utils.path_exists(db_file):
				kodi_utils.delete_file(db_file)
				removed += 1
		except:
			pass
	return removed


def _db_connect():
	try:
		profile_path = 'special://profile/addon_data/plugin.video.alkoflix/'
		if not kodi_utils.path_exists(profile_path): kodi_utils.make_directorys(profile_path)
	except:
		pass
	dbcon = kodi_utils.database_connect(ALKOPASTE_DB, timeout=30)
	dbcon.row_factory = sqlite3.Row
	try:
		dbcon.execute("""PRAGMA synchronous = OFF""")
		dbcon.execute("""PRAGMA journal_mode = OFF""")
		dbcon.execute("""PRAGMA temp_store = MEMORY""")
		dbcon.execute("""PRAGMA mmap_size = 268435456""")
	except:
		pass
	return dbcon


def _db_init(dbcon=None):
	own = dbcon is None
	if own: dbcon = _db_connect()
	try:
		dbcon.execute("""CREATE TABLE IF NOT EXISTS alko_meta (id TEXT PRIMARY KEY, value TEXT)""")
		try:
			version = _db_meta_get(dbcon, 'schema_version')
			if version != str(ALKOPASTE_DB_VERSION):
				dbcon.execute("""DROP TABLE IF EXISTS alko_links""")
		except:
			pass
		dbcon.execute("""CREATE TABLE IF NOT EXISTS alko_links (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			media_type TEXT NOT NULL,
			tmdb_id TEXT NOT NULL,
			title TEXT,
			year INTEGER,
			season INTEGER,
			quality_text TEXT,
			urls_raw TEXT NOT NULL,
			base_url TEXT,
			paste_code TEXT,
			row_hash TEXT NOT NULL,
			updated_at INTEGER
		)""")
		dbcon.execute("""CREATE INDEX IF NOT EXISTS idx_alko_links_movie ON alko_links(media_type, tmdb_id)""")
		dbcon.execute("""CREATE INDEX IF NOT EXISTS idx_alko_links_series ON alko_links(media_type, tmdb_id, season)""")
		dbcon.execute("""CREATE UNIQUE INDEX IF NOT EXISTS idx_alko_links_unique ON alko_links(row_hash)""")
		_db_meta_set(dbcon, 'schema_version', str(ALKOPASTE_DB_VERSION))
		dbcon.commit()
	finally:
		if own:
			try: dbcon.close()
			except: pass

def _db_meta_get(dbcon, key, default=''):
	try:
		row = dbcon.execute("""SELECT value FROM alko_meta WHERE id=?""", (key,)).fetchone()
		return row['value'] if row else default
	except:
		return default


def _db_meta_set(dbcon, key, value):
	try:
		dbcon.execute("""INSERT OR REPLACE INTO alko_meta(id, value) VALUES(?, ?)""", (key, str(value)))
	except:
		pass


def _all_content_codes():
	codes = []
	root_codes = []
	try:
		root_codes.extend(ALKOPASTE_FILM_CODES)
		root_codes.extend(ALKOPASTE_SERIES_CODES)
		for category_codes in ALKOPASTE_INDEX_CODES.values():
			for code in category_codes:
				if code not in root_codes: root_codes.append(code)
		for item in ALKOPASTE_EXTRA_CODES:
			try:
				code = item.get('code') if isinstance(item, dict) else item
				if code and code not in root_codes: root_codes.append(code)
			except:
				pass
		for code in root_codes:
			for leaf_code in _resolve_index_code(code):
				if leaf_code not in codes: codes.append(leaf_code)
	except Exception as e:
		try: kodi_utils.logger('alkoPaste DB', 'Résolution index impossible | erreur=%s' % type(e).__name__)
		except: pass
	return codes


def _row_hash(media_type, tmdb_id, season, quality_text, urls_raw, paste_code):
	try:
		# Le code paste n'entre pas dans l'unicité : si le même média/liste de liens
		# apparaît dans deux index, on garde une seule ligne locale.
		payload = '|'.join((
			str(media_type or '').strip(),
			str(tmdb_id or '').strip(),
			str(season if season is not None else '').strip(),
			clean_text(quality_text),
			str(urls_raw or '').strip()
		))
		try:
			return hashlib.sha1(payload.encode('utf-8'), usedforsecurity=False).hexdigest()
		except TypeError:
			return hashlib.sha1(payload.encode('utf-8')).hexdigest()
	except:
		return str(int(time.time() * 1000))


def _insert_db_row(dbcur, media_type, tmdb_id, title, year, season, quality_text, urls_raw, base_url, paste_code):
	try:
		# Règle volontairement stricte : la DB alkoPaste s'indexe uniquement par TMDb ID.
		# Le titre reste seulement une info d'affichage, jamais une clé de recherche.
		tmdb_id = str(tmdb_id or '').strip()
		if not tmdb_id or tmdb_id in ('0', 'None', 'none', 'null'): return 0
		urls_raw = str(urls_raw or '').strip()
		if not urls_raw: return 0
		media_type = str(media_type or '').strip()
		quality_text = clean_text(quality_text)
		title = clean_text(title)
		row_hash = _row_hash(media_type, tmdb_id, season, quality_text, urls_raw, paste_code)
		dbcur.execute("""INSERT OR IGNORE INTO alko_links
			(media_type, tmdb_id, title, year, season, quality_text, urls_raw, base_url, paste_code, row_hash, updated_at)
			VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
			(media_type, tmdb_id, title, year, season, quality_text, urls_raw,
			str(base_url or ALLDEBRID_DEFAULT_BASE_URL).strip(), str(paste_code or '').strip(),
			row_hash, int(time.time())))
		return dbcur.rowcount or 0
	except Exception:
		return 0


def _import_content_code(dbcur, code, clear_existing=True):
	added = 0
	try:
		url = _code_url(code)
		content = _fetch_text(url)
		if not content or not _looks_like_content_paste(content): return 0
		lines = content.splitlines()
		if not lines: return 0
		base_url = _base_url_from_header(lines[0])
		if clear_existing:
			dbcur.execute("""DELETE FROM alko_links WHERE paste_code=?""", (str(code or '').strip(),))
		for line in lines[1:]:
			line = (line or '').strip()
			if not line: continue
			parts = line.split(';')
			if len(parts) <= COL_URLS: continue
			cat = _normalize_cat(parts[COL_CAT].strip())
			if cat not in ('film', 'serie'): continue
			tmdb_id = parts[COL_TMDB].strip()
			# Aucun fallback titre : sans TMDb ID, la ligne est ignorée.
			if not tmdb_id or tmdb_id in ('0', 'None', 'none', 'null'): continue
			title = parts[COL_TITLE].strip()
			year = _safe_int(parts[COL_YEAR].strip(), None)
			if cat == 'serie':
				season = _safe_int(parts[COL_SAISON].strip(), None)
				added += _insert_db_row(dbcur, 'serie', tmdb_id, title, year, season,
					parts[COL_RES].strip(), parts[COL_URLS].strip(), base_url, code)
			else:
				added += _insert_db_row(dbcur, 'film', tmdb_id, title, year, None,
					parts[COL_RES].strip(), parts[COL_URLS].strip(), base_url, code)
	except Exception as e:
		try: kodi_utils.logger('alkoPaste DB', 'Import paste impossible | code=%s | erreur=%s' % (code, type(e).__name__))
		except: pass
	return added

def _db_sync_due(dbcon):
	try:
		last_sync = _safe_int(_db_meta_get(dbcon, 'last_sync', '0'), 0) or 0
		if not last_sync: return True
		return int(time.time()) - last_sync >= int(DB_SYNC_INTERVAL_HOURS * 3600)
	except:
		return True


def alkopaste_database_status():
	status = {'ready': False, 'total': 0, 'last_sync': 0, 'last_count': 0}
	dbcon = None
	try:
		dbcon = _db_connect()
		_db_init(dbcon)
		status['total'] = int(dbcon.execute("""SELECT COUNT(*) AS total FROM alko_links""").fetchone()['total'] or 0)
		status['last_sync'] = _safe_int(_db_meta_get(dbcon, 'last_sync', '0'), 0) or 0
		status['last_count'] = _safe_int(_db_meta_get(dbcon, 'last_count', '0'), 0) or 0
		status['ready'] = status['total'] > 0
	except Exception:
		pass
	finally:
		if dbcon:
			try: dbcon.close()
			except: pass
	return status


def _db_clear_links(dbcon):
	try:
		dbcon.execute("""DELETE FROM alko_links""")
		dbcon.commit()
	except:
		pass


def _build_fresh_database(codes):
	_delete_alkopaste_database_files()
	dbcon = _db_connect()
	try:
		_db_init(dbcon)
		added = 0
		dbcur = dbcon.cursor()
		dbcur.execute("""BEGIN""")
		for code in codes:
			added += _import_content_code(dbcur, code, clear_existing=False)
		_db_meta_set(dbcon, 'schema_version', str(ALKOPASTE_DB_VERSION))
		_db_meta_set(dbcon, 'last_sync', int(time.time()))
		_db_meta_set(dbcon, 'last_count', added)
		dbcon.commit()
		try:
			for db_file in _alkopaste_db_related_files()[1:]:
				if kodi_utils.path_exists(db_file): kodi_utils.delete_file(db_file)
		except:
			pass
		return added
	except:
		try: dbcon.rollback()
		except: pass
		raise
	finally:
		try: dbcon.close()
		except: pass


def sync_alkopaste_database(force=False, rebuild=False):
	"""Synchronise la source ALKO dans une base SQLite locale.
	La synchronisation automatique est limitée à une fois par jour.
	Quand une mise à jour est requise, alkopaste.db est supprimée puis reconstruite.
	"""
	lock_prop = 'alkoflix.alkopaste.sync.running'
	try:
		if kodi_utils.get_property(lock_prop) == 'true':
			return {'ok': False, 'skipped': True, 'reason': 'running', 'pastes': 0, 'links': 0}
		kodi_utils.set_property(lock_prop, 'true')
	except:
		pass
	dbcon = None
	try:
		dbcon = _db_connect()
		_db_init(dbcon)
		if not force and not _db_sync_due(dbcon):
			status = alkopaste_database_status()
			return {'ok': True, 'skipped': True, 'reason': 'not_due', 'pastes': 0, 'links': status.get('total', 0)}
		try: dbcon.close()
		except: pass
		dbcon = None

		codes = _all_content_codes()
		if not codes:
			# Ne jamais supprimer la dernière base utilisable si aucun code n'est résolu.
			# Sinon une panne réseau/index peut bloquer la source au lieu de garder l'ancien index local.
			try: kodi_utils.logger('alkoPaste DB', 'Synchronisation annulée | aucun code de contenu résolu')
			except: pass
			return {'ok': False, 'skipped': False, 'reason': 'no_codes', 'pastes': 0, 'links': 0}

		# Mise à jour volontairement simple : suppression complète puis reconstruction.
		# Même le service quotidien passe par ce chemin; aucun vidage progressif interminable.
		added = _build_fresh_database(codes)
		try: kodi_utils.logger('alkoPaste DB', 'Base reconstruite | pastes=%s | entrées=%s' % (len(codes), added))
		except: pass
		return {'ok': True, 'skipped': False, 'reason': '', 'pastes': len(codes), 'links': added}
	except Exception as e:
		try: kodi_utils.logger('alkoPaste DB', 'Synchronisation impossible | erreur=%s' % type(e).__name__)
		except: pass
		return {'ok': False, 'skipped': False, 'reason': type(e).__name__, 'pastes': 0, 'links': 0}
	finally:
		try: kodi_utils.clear_property(lock_prop)
		except: pass
		if dbcon:
			try: dbcon.close()
			except: pass


def sync_alkopaste_database_manual():
	try:
		kodi_utils.notification('Mise à jour de la source ALKO en cours...')
	except:
		pass
	result = sync_alkopaste_database(force=True, rebuild=True)
	try:
		if result.get('ok'):
			message = 'Source ALKO mise à jour : %s entrées indexées.' % result.get('links', 0)
			kodi_utils.ok_dialog(heading='alkoFlix', text=message, top_space=True)
		else:
			kodi_utils.ok_dialog(heading='alkoFlix', text='La source ALKO n’a pas pu être mise à jour. Raison : %s' % (result.get('reason') or 'erreur inconnue'), top_space=True)
	except:
		pass
	return result


def _db_ready():
	try:
		dbcon = _db_connect()
		_db_init(dbcon)
		count = dbcon.execute("""SELECT COUNT(*) AS total FROM alko_links""").fetchone()['total']
		dbcon.close()
		return count > 0
	except:
		return False


def _query_database(data, wanted):
	results = []
	seen = set()
	dbcon = None
	try:
		dbcon = _db_connect()
		_db_init(dbcon)
		target_tmdb_ids = _target_tmdb_ids(data)
		# Règle stricte : alkoPaste cherche uniquement par TMDb ID.
		# Aucun matching par titre, même en fallback.
		if not target_tmdb_ids: return []
		target_season = _safe_int(data.get('season'), None)
		target_episode = _safe_int(data.get('episode'), None)
		media_type = 'serie' if wanted == 'serie' else 'film'
		placeholders = ','.join('?' for _ in target_tmdb_ids)
		params = [media_type] + list(target_tmdb_ids)
		if media_type == 'serie':
			query = """SELECT * FROM alko_links WHERE media_type=? AND tmdb_id IN (%s)""" % placeholders
			if target_season is not None:
				query += """ AND (season IS NULL OR season=?)"""
				params.append(target_season)
		else:
			query = """SELECT * FROM alko_links WHERE media_type=? AND tmdb_id IN (%s)""" % placeholders
		rows = dbcon.execute(query, params).fetchall()
		for row in rows:
			if media_type == 'serie':
				for ep_num, suffix in _series_urls(row['urls_raw']):
					if target_episode is not None and int(ep_num) != target_episode: continue
					url = _full_url(row['base_url'] or ALLDEBRID_DEFAULT_BASE_URL, suffix)
					if not url or url in seen: continue
					seen.add(url)
					results.append(_build_db_result(data, row, url, row['quality_text']))
			else:
				res_list = _literal_list(row['quality_text'])
				urls_list = _literal_list(row['urls_raw'])
				if not res_list or not urls_list: continue
				for res_name, suffix in zip(res_list, urls_list):
					url = _full_url(row['base_url'] or ALLDEBRID_DEFAULT_BASE_URL, suffix)
					if not url or url in seen: continue
					seen.add(url)
					results.append(_build_db_result(data, row, url, res_name))
	except Exception as e:
		try: kodi_utils.logger('alkoPaste DB', 'Recherche impossible | erreur=%s' % type(e).__name__)
		except: pass
	finally:
		if dbcon:
			try: dbcon.close()
			except: pass
	return results


def _build_db_result(data, row, url, quality_text):
	quality_text = clean_text(quality_text)
	return {
		'link': url,
		'hoster': 'alldebrid' if hoster_key('', url) == 'alldebrid' else hoster_key('', url),
		'quality': normalize_quality(quality_text),
		'language': _release_language(quality_text),
		'raw_language': quality_text,
		'display_name': _build_db_display_name(data, row, quality_text),
		'size': 0
	}

def _build_db_display_name(data, row, quality_text=''):
	try:
		title = clean_text(row['title']) or clean_text(data.get('tvshowtitle') or data.get('title'))
		res_name = clean_text(quality_text)
		if row['media_type'] == 'serie':
			season = _safe_int(row['season'], _safe_int(data.get('season'), 0)) or 0
			episode = _safe_int(data.get('episode'), 0) or 0
			return clean_text('%s S%02dE%02d %s' % (title, season, episode, res_name))
		return clean_text('%s %s %s' % (title, row['year'] or data.get('year') or '', res_name))
	except:
		return clean_text(quality_text or '')

class source(BaseHosterSource):
	provider_key = 'alkopaste'
	provider_name = 'ALKO'
	priority = 2
	pack_capable = False
	hasMovies = True
	hasEpisodes = True

	def base_url(self):
		# Source interne publique : aucun champ URL dans les settings.
		return ALKOPASTE_BASE_URL

	def _make_raw_result(self, data, url, res_name, paste_title='', paste_year=None):
		res_name = clean_text(res_name)
		paste_title = clean_text(paste_title) or clean_text(data.get('tvshowtitle') or data.get('title'))
		paste_year = paste_year or data.get('year') or ''
		quality = normalize_quality(res_name)
		language = _release_language(res_name)
		size = 0
		if data.get('tvshowtitle'):
			display = '%s S%02dE%02d %s' % (paste_title, int(data.get('season') or 0), int(data.get('episode') or 0), res_name)
		else:
			display = '%s %s %s' % (paste_title, paste_year, res_name)
		display = clean_text(display)
		return {
			'link': url,
			'hoster': 'alldebrid' if hoster_key('', url) == 'alldebrid' else '',
			'quality': quality,
			'language': language,
			'raw_language': res_name,
			'display_name': display,
			'size': size
		}

	def _parse_matching_lines(self, content, data, wanted):
		results = []
		seen = set()
		try:
			lines = (content or '').splitlines()
			if not lines: return []
			base_url = _base_url_from_header(lines[0])
			target_tmdb_ids = _target_tmdb_ids(data)
			if not target_tmdb_ids: return []
			target_season = _safe_int(data.get('season'), None)
			target_episode = _safe_int(data.get('episode'), None)
			rows = []
			for line in lines[1:]:
				line = (line or '').strip()
				if not line: continue
				parts = line.split(';')
				if len(parts) <= COL_URLS: continue
				cat = _normalize_cat(parts[COL_CAT].strip())
				# La colonne CAT du paste final est la référence :
				# film = film, serie/anime = série. Les index peuvent être mixtes.
				if cat != wanted: continue
				tmdb_id = parts[COL_TMDB].strip()
				paste_title = parts[COL_TITLE].strip()
				if tmdb_id in target_tmdb_ids:
					rows.append(parts)
			for parts in rows:
				paste_title = parts[COL_TITLE].strip()
				paste_year = _safe_int(parts[COL_YEAR].strip(), None)
				res_raw = parts[COL_RES].strip()
				urls_raw = parts[COL_URLS].strip()
				if wanted == 'serie':
					row_season = _safe_int(parts[COL_SAISON].strip(), None)
					if target_season is not None and row_season is not None and row_season != target_season: continue
					for ep_num, suffix in _series_urls(urls_raw):
						if target_episode is not None and int(ep_num) != target_episode: continue
						url = _full_url(base_url, suffix)
						if not url or url in seen: continue
						seen.add(url)
						results.append(self._make_raw_result(data, url, res_raw, paste_title, paste_year))
				else:
					res_list = _literal_list(res_raw)
					urls_list = _literal_list(urls_raw)
					if not res_list or not urls_list: continue
					for res_name, suffix in zip(res_list, urls_list):
						url = _full_url(base_url, suffix)
						if not url or url in seen: continue
						seen.add(url)
						results.append(self._make_raw_result(data, url, res_name, paste_title, paste_year))
		except Exception as e:
			try: kodi_utils.logger('alkoPaste', 'Parsing impossible | erreur=%s' % type(e).__name__)
			except: pass
		return results

	def _search_raw_direct(self, data, wanted):
		codes, _wanted = _code_entries_for_media('episode' if data.get('tvshowtitle') else 'movie')
		if not codes: return []
		results = []
		seen_links = set()
		for code in codes:
			url = _code_url(code)
			content = _fetch_text(url)
			if not content: continue
			for item in self._parse_matching_lines(content, data, wanted):
				link = item.get('link')
				if not link or link in seen_links: continue
				seen_links.add(link)
				results.append(item)
		return results

	def search(self, data):
		wanted = 'serie' if data.get('tvshowtitle') else 'film'
		if not _target_tmdb_ids(data): return []
		# La recherche ne lance plus une synchronisation complète.
		# La synchro se fait en arrière-plan une fois par jour, ou manuellement via Maintenance.
		# Cela évite qu'une mise à jour ALKO bloque le sélecteur ou donne l'impression que la source est morte.
		results = _query_database(data, wanted) if _db_ready() else []
		if results: return results
		# Fallback de sécurité : si la base est vide, verrouillée ou pas encore à jour,
		# on garde l'ancien parsing raw direct pour ne pas perdre les résultats.
		return self._search_raw_direct(data, wanted)
