# -*- coding: utf-8 -*-
# plugin.video.alkoflix
# resources/lib/indexers/subtitles.py
#
# Gestion des sous-titres
#   1. Prioriser un sous-titre local/intégré correspondant à la langue choisie.
#   2. Réutiliser un sous-titre Wyzie déjà téléchargé dans special://temp/.
#   3. Interroger l’API Wyzie pour proposer/télécharger un sous-titre externe.
#
# Les sous-titres téléchargés restent dans special://temp/.
# alkoFlix nettoie ce dossier au démarrage afin d’éviter l’accumulation de fichiers .srt.

import json
import os
import requests
from datetime import timedelta
from urllib.parse import urlsplit, urlunsplit, parse_qsl, urlencode
from caches.main_cache import MainCache
from modules.meta_lists import language_choices
from modules import kodi_utils
# from modules.kodi_utils import logger

ls, get_setting = kodi_utils.local_string, kodi_utils.get_setting
logger = kodi_utils.logger
# Icône utilisée pour les notifications du module sous-titres.
addon_icon = 'special://home/addons/plugin.video.alkoflix/alkoflix.png'

# Point d’entrée utilisé pour rechercher les sous-titres via Wyzie.
search_url = 'https://sub.wyzie.io/search'

# Log dédié aux sous-titres. Tous les messages passent par “alkoFlix Subtitles”
# pour être faciles à repérer dans kodi.log.
def _sub_log(message):
	try: logger('alkoFlix Subtitles', message)
	except: pass

# Masque les clés/API tokens avant d’écrire une URL dans le log.
# Important : la clé Wyzie ne doit jamais apparaître en clair dans kodi.log.
def _sanitize_url(url):
	try:
		parts = urlsplit(url)
		query = []
		for key, value in parse_qsl(parts.query, keep_blank_values=True):
			if key.lower() in ('key', 'apikey', 'api_key', 'token'):
				value = '***'
			query.append((key, value))
		return urlunsplit((parts.scheme, parts.netloc, parts.path, urlencode(query), parts.fragment))
	except: return url

# Masque les paramètres sensibles avant journalisation.
# On indique seulement si la clé est présente ou absente.
def _sanitize_params(params):
	try:
		safe = dict(params or {})
		for key in ('key', 'apikey', 'api_key', 'token'):
			if key in safe:
				safe[key] = 'présente' if safe.get(key) else 'absente'
		return safe
	except: return {}

# Timeout réseau pour Wyzie et les téléchargements de sous-titres.
# 20 secondes évite les faux échecs lorsque certaines sources répondent lentement.
timeout = 20.0
# Sources demandées à Wyzie. Wyzie agrège plusieurs fournisseurs de sous-titres.
api_sources = ('opensubtitles', 'subdl', 'subf2m', 'gestdown', 'yify')

# Wrapper HTTP commun : centralise requests.get(), les logs,
# le masquage des données sensibles et le retry simple en cas de réponse temporaire.
def _get(url, params=None, stream=False, retry=False):
	_sub_log('HTTP GET -> %s | params=%s | stream=%s | retry=%s' % (_sanitize_url(url), _sanitize_params(params), stream, retry))
	try:
		response = requests.get(url, params=params, stream=stream, timeout=timeout)
	except Exception as e:
		_sub_log('HTTP GET exception -> %s: %s' % (type(e).__name__, e))
		raise
	_sub_log('HTTP GET <- status=%s | reason=%s | url=%s' % (response.status_code, response.reason, _sanitize_url(getattr(response, 'url', url))))
	if retry and response.status_code in (403, 429):
		_sub_log('HTTP GET rate/permission response -> pause 10s before retry')
		kodi_utils.notification(32740, icon=addon_icon)
		kodi_utils.sleep(10000)
		return _get(url, params=params, stream=stream)
	if retry and response.status_code in (502, 503, 504):
		_sub_log('HTTP GET temporary server response -> pause 2s before retry')
		kodi_utils.sleep(2000)
		return _get(url, params=params, stream=stream)
	return response

# Télécharge le sous-titre choisi. Retourne soit la réponse HTTP,
# soit un texte d’erreur si le téléchargement échoue.
def subtitles_download(url):
	_sub_log('Download subtitle requested -> %s' % _sanitize_url(url))
	response = _get(url, stream=True, retry=True)
	if not response.ok:
		_sub_log('Download subtitle failed -> %s %s' % (response.status_code, response.reason))
		return response.reason
	_sub_log('Download subtitle OK -> bytes=%s' % len(getattr(response, 'content', b'')))
	return response

# Recherche des sous-titres via Wyzie.
# imdb_id est obligatoire. Pour les séries, season/episode ciblent le bon épisode.
def subtitles_search(imdb_id, season=None, episode=None, apikey='', wanted_language=''):
	_sub_log('Wyzie search requested -> imdb_id=%s | season=%s | episode=%s | wanted_language=%s | api_key=%s' % (imdb_id, season, episode, wanted_language, 'présente' if apikey else 'absente'))
	if not imdb_id:
		_sub_log('Wyzie search aborted -> imdb_id manquant')
		return []
	cache_name = 'subtitles_wyzie_%s' % imdb_id
	params = {
		'encoding': 'utf-8',
		'format': 'srt',

		# Pendant la bêta, on demande à Wyzie d’interroger toutes les sources
		# disponibles plutôt que seulement une liste limitée.
		# Cela augmente les chances de récupérer les sous-titres FR lorsqu’ils
		# sont présents chez un fournisseur non inclus dans api_sources.
		'source': 'all',

		# Demande à Wyzie de contourner son cache serveur afin de récupérer
		# des résultats frais. Utile lorsque les résultats varient pour un même média.
		'refresh': 'true',

		'key': apikey,
		'id': imdb_id
	}
	if season:
		cache_name += '_%s_%s' % (season, episode)
		params.update({'season': season, 'episode': episode})
	# Cache positif uniquement :
	# - si une recherche Wyzie retourne au moins un sous-titre dans la langue choisie,
	#   on conserve temporairement cette bonne réponse;
	# - si Wyzie ne retourne rien, seulement de l’anglais/autres langues, une erreur
	#   ou un timeout, rien n’est conservé.
	#
	# Cela évite de bloquer l’addon avec un “mauvais cache”, tout en gardant les bons
	# résultats FR lorsque Wyzie les retourne une fois puis devient capricieux ensuite.
	wanted_language = (wanted_language or '').lower()
	wanted_short = (kodi_utils.convert_language(wanted_language, format='short') or '').lower()
	cache_lang = wanted_language or wanted_short or 'default'
	cache_name = '%s_%s' % (cache_name, cache_lang)
	maincache = MainCache()
	cache = maincache.get(cache_name)
	if cache:
		_sub_log('Wyzie positive cache hit -> cache_name=%s | results=%s' % (cache_name, len(cache) if isinstance(cache, list) else 'inconnu'))
		return cache
	_sub_log('Wyzie positive cache miss -> live API search | cache_name=%s' % cache_name)
	response = _get(search_url, params=params, retry=True)
	if not response.ok:
		try: preview = response.text[:300]
		except: preview = ''
		_sub_log('Wyzie HTTP error -> status=%s | reason=%s | body=%s' % (response.status_code, response.reason, preview))
		if response.status_code in (502, 503, 504):
			return 'Wyzie temporairement indisponible'
		return response.reason
	try:
		response = response.json()
	except Exception as e:
		_sub_log('Wyzie JSON error -> %s: %s' % (type(e).__name__, e))
		return 'Invalid JSON'
	if isinstance(response, dict):
		_sub_log('Wyzie returned dict -> %s' % response)
		message = response.get('message') or response.get('error') or response.get('detail') or 'Réponse Wyzie inattendue'
		return message
	_sub_log('Wyzie API results -> %s résultat(s)' % (len(response) if isinstance(response, list) else 'type inattendu'))

	def _positive_lang_match(item):
		lang = (item.get('language') or '').lower() if isinstance(item, dict) else ''
		if not lang: return False
		candidates = {wanted_language, wanted_short}
		if wanted_short == 'fr' or wanted_language in ('fre', 'fra'):
			candidates.update(('fr', 'fre', 'fra'))
		elif wanted_short == 'en' or wanted_language == 'eng':
			candidates.update(('en', 'eng'))
		elif wanted_short == 'es' or wanted_language == 'spa':
			candidates.update(('es', 'spa'))
		elif wanted_short == 'pt' or wanted_language in ('por', 'pob'):
			candidates.update(('pt', 'por', 'pob', 'pt-br', 'pt_br', 'ptbr'))
		candidates = {i for i in candidates if i}
		return lang in candidates

	if isinstance(response, list) and any(_positive_lang_match(i) for i in response):
		_sub_log('Wyzie positive cache set -> cache_name=%s | expiration=24h' % cache_name)
		maincache.set(cache_name, response, expiration=timedelta(hours=24))
	else:
		_sub_log('Wyzie positive cache skipped -> no subtitle matched wanted_language=%s/%s' % (wanted_language, wanted_short))
	return response

# Résout la langue configurée dans alkoFlix.
# L'interface expose uniquement Français et Anglais pour Wyzie.
# Les anciennes installations peuvent encore contenir une autre langue ou un ancien index.
def _resolve_language_setting():
	raw_value = get_setting('subtitles.language') or 'French'
	raw_text = str(raw_value).strip()
	raw_lower = raw_text.lower()
	try:
		# Nouveaux choix de l'interface : 0 = Français, 1 = Anglais.
		if raw_text == '0':
			return language_choices.get('French', 'fre'), 'French', raw_value
		if raw_text == '1':
			return language_choices.get('English', 'eng'), 'English', raw_value

		# Compatibilité avec les anciennes valeurs textuelles ou localisées.
		if raw_lower in ('french', 'français', 'francais', 'fr', 'fr-fr', 'fr_fr', 'fre', 'fra'):
			if raw_text not in ('French', '0'):
				kodi_utils.set_setting('subtitles.language', 'French')
			return language_choices.get('French', 'fre'), 'French', raw_value
		if raw_lower in ('english', 'anglais', 'en', 'en-us', 'en_us', 'en-gb', 'en_gb', 'eng'):
			if raw_text not in ('English', '1'):
				kodi_utils.set_setting('subtitles.language', 'English')
			return language_choices.get('English', 'eng'), 'English', raw_value

		# Compatibilité avec l'ancien long select Kodi : English=16, French=20.
		# Toute autre ancienne langue est ramenée vers Français pour éviter un réglage invisible.
		if raw_text.isdigit():
			old_index = int(raw_text)
			if old_index == 16:
				kodi_utils.set_setting('subtitles.language', 'English')
				return language_choices.get('English', 'eng'), 'English', raw_value
			if old_index == 20:
				kodi_utils.set_setting('subtitles.language', 'French')
				return language_choices.get('French', 'fre'), 'French', raw_value

		if raw_value in language_choices and raw_value in ('French', 'English'):
			return language_choices[raw_value], raw_value, raw_value

		_sub_log('Language setting fallback -> ancienne valeur non supportée=%s, retour Français' % raw_value)
	except Exception as e:
		_sub_log('Language setting fallback exception -> %s: %s' % (type(e).__name__, e))

	try: kodi_utils.set_setting('subtitles.language', 'French')
	except: pass
	return language_choices.get('French', 'fre'), 'French', raw_value

# Classe principale appelée pendant la lecture.
# Elle gère les pistes locales, les fichiers déjà téléchargés et la recherche Wyzie.
class Subtitles(kodi_utils.xbmc_player):
	# Initialise les réglages liés aux sous-titres : clé API, action choisie,
	# activation automatique Wyzie et langue cible Wyzie.
	def __init__(self):
		kodi_utils.xbmc_player.__init__(self)
		self.apikey = (get_setting('subtitles.apikey') or '').strip()
		self.has_wyzie_api = bool(self.apikey)
		self.subs_action = {'0': 'auto', '1': 'select', '2': 'off'}[get_setting('subtitles.subs_action', '1')]
		# Compatibilité avec les anciennes versions du réglage.
		# La nouvelle interface utilise directement subtitles.subs_action :
		#   auto   = appliquer automatiquement le meilleur résultat Wyzie;
		#   select = proposer une liste triée;
		#   off    = Kodi seulement.
		self.auto_enable = 'true' if self.subs_action == 'auto' else 'false'

		# Langue choisie dans alkoFlix pour le fallback Wyzie.
		# Elle ne remplace pas la préférence globale de Kodi pour les pistes locales/intégrées.
		self.language1, self.language_label, self.language_setting = _resolve_language_setting()
		self.wyzie_language1 = self.language1
		self.wyzie_language_label = self.language_label

		# Préférence globale Kodi. Elle sert uniquement à laisser Kodi appliquer ses propres
		# choix locaux/intégrés en premier. Elle ne doit pas écraser la langue Wyzie.
		self.kodi_subtitle_language_raw = self._get_kodi_setting_value('locale.subtitlelanguage')
		self.kodi_subtitle_preference_disabled = self._is_disabled_kodi_subtitle_preference(self.kodi_subtitle_language_raw)
		self.kodi_language1, self.kodi_language_label = self._resolve_kodi_subtitle_language_preference(self.kodi_subtitle_language_raw)

		# Mémorise si la dernière piste locale sélectionnée est une piste forcée.
		# Cette information sert uniquement aux logs et aux décisions internes.
		self._last_selected_embedded_is_forced = False

		# Indique qu'une piste anglaise locale existe déjà.
		# Si Wyzie cherche une autre langue, cette piste anglaise ne bloque pas la recherche,
		# mais elle évite d'appliquer ensuite un sous-titre anglais externe en doublon.
		self._local_english_available_before_wyzie = False

		_sub_log('Init -> subs_action=%s | auto_enable=%s | wyzie_language=%s (%s) | kodi_language=%s (%s) | kodi_subtitle_disabled=%s | api_key=%s' % (self.subs_action, self.auto_enable, self.language1, self.language_label, self.kodi_language1, self.kodi_subtitle_language_raw, self.kodi_subtitle_preference_disabled, 'présente' if self.has_wyzie_api else 'absente'))

	# Appel JSON-RPC sécurisé.
	def _jsonrpc_call(self, method, params=None, default=None):
		try:
			payload = {"jsonrpc": "2.0", "method": method, "id": 1}
			if params is not None: payload["params"] = params
			response = json.loads(kodi_utils.execJSONRPC(json.dumps(payload)))
			if response.get('error'):
				_sub_log('JSON-RPC error -> method=%s | error=%s' % (method, response.get('error')))
				return default
			return response.get('result', default)
		except Exception as e:
			_sub_log('JSON-RPC exception -> method=%s | %s: %s' % (method, type(e).__name__, e))
			return default

	# Lit un réglage global de Kodi, par exemple locale.subtitlelanguage.
	def _get_kodi_setting_value(self, setting_id, default=None):
		result = self._jsonrpc_call('Settings.GetSettingValue', {'setting': setting_id}, default={})
		if isinstance(result, dict) and 'value' in result:
			return result.get('value')
		return default

	# Détermine si le réglage Kodi indique explicitement aucun sous-titre préféré.
	def _is_disabled_kodi_subtitle_preference(self, value):
		value = (str(value or '')).strip().lower()
		return value in ('', 'none', 'no', 'off', 'disabled', 'aucun', 'désactivé', 'desactive')

	# Essaie de convertir la langue de sous-titres préférée de Kodi.
	# Cette valeur sert seulement à comprendre/laisser agir Kodi pour les pistes locales.
	# Elle ne modifie jamais la langue choisie dans alkoFlix pour Wyzie.
	def _resolve_kodi_subtitle_language_preference(self, raw_value):
		if self.kodi_subtitle_preference_disabled:
			_sub_log('Kodi subtitle language preference -> disabled/none')
			return '', ''
		try:
			if raw_value in language_choices:
				converted = language_choices[raw_value]
				_sub_log('Kodi subtitle language preference detected -> %s=%s' % (raw_value, converted))
				return converted, raw_value
			converted = kodi_utils.convert_language(raw_value)
			if converted:
				_sub_log('Kodi subtitle language preference converted -> %s=%s' % (raw_value, converted))
				return converted, str(raw_value)
		except Exception as e:
			_sub_log('Kodi subtitle language preference exception -> %s: %s' % (type(e).__name__, e))
		_sub_log('Kodi subtitle language preference detected -> mode=%s (Kodi garde la priorité)' % raw_value)
		return '', ''

	# Retourne l’identifiant du lecteur vidéo actif.
	def _get_video_player_id(self):
		result = self._jsonrpc_call('Player.GetActivePlayers', default=[])
		try: return next((p.get('playerid') for p in result if p.get('type') == 'video'), None)
		except: return None

	# Retourne les informations de sous-titres que Kodi expose pour la lecture courante.
	def _get_subtitle_state(self):
		player_id = self._get_video_player_id()
		if player_id is None:
			_sub_log('Kodi subtitle state -> no active video player')
			return None, {}
		props = self._jsonrpc_call('Player.GetProperties', {
			'playerid': player_id,
			'properties': ['subtitles', 'currentsubtitle', 'subtitleenabled']
		}, default={})
		if not isinstance(props, dict): props = {}
		return player_id, props

	# Texte normalisé d’une piste de sous-titres.
	def _subtitle_track_text(self, sub):
		lang = (sub.get('language') or '') if isinstance(sub, dict) else ''
		name = (sub.get('name') or '') if isinstance(sub, dict) else ''
		return ('%s %s' % (lang, name)).lower()

	# Détecte une piste forcée sans décider de l’activer à la place de Kodi.
	def _is_forced_subtitle_track(self, sub):
		text = self._subtitle_track_text(sub)
		return bool(isinstance(sub, dict) and sub.get('isforced')) or any(i in text for i in ('forced', 'forcé', 'force', 'forces', 'forcée', 'forced only'))


	# Construit une liste de valeurs possibles pour comparer les langues.
	# Kodi peut exposer une même langue sous plusieurs formes : fr, fre, fra,
	# French, Français, etc. On accepte donc les équivalences courantes.
	def _subtitle_language_candidates(self, language_code):
		language_code = (language_code or '').lower()
		short_code = (kodi_utils.convert_language(language_code, format='short') or '').lower()
		candidates = {language_code, short_code}
		if language_code in ('fre', 'fra') or short_code == 'fr':
			candidates.update(('fr', 'fre', 'fra', 'french', 'français', 'francais'))
		elif language_code == 'eng' or short_code == 'en':
			candidates.update(('en', 'eng', 'english', 'anglais'))
		elif language_code == 'spa' or short_code == 'es':
			candidates.update(('es', 'spa', 'spanish', 'espagnol'))
		elif language_code in ('por', 'pob') or short_code == 'pt':
			candidates.update(('pt', 'por', 'pob', 'pt-br', 'pt_br', 'ptbr', 'pt-pt', 'pt_pt', 'ptpt', 'portuguese', 'portugais'))
		return {i for i in candidates if i}

	# Vérifie si une piste locale/intégrée correspond à une langue donnée.
	# On vérifie d’abord le champ language exposé par Kodi, puis le nom de la piste
	# parce que certains fichiers indiquent la langue seulement dans le libellé.
	def _subtitle_track_matches_language(self, sub, language_code):
		if not isinstance(sub, dict): return False
		candidates = self._subtitle_language_candidates(language_code)
		if not candidates: return False
		lang = (sub.get('language') or '').lower()
		text = self._subtitle_track_text(sub)
		if lang and lang in candidates: return True
		return any(i in text for i in candidates if len(i) > 2)

	# Indique si un code/libellé de langue correspond à l'anglais.
	def _language_code_is_english(self, language_code):
		candidates = self._subtitle_language_candidates(language_code)
		return bool(candidates.intersection(('en', 'eng', 'english', 'anglais')))

	# Avant d’autoriser Wyzie, on vérifie si le fichier contient déjà une piste locale/intégrée utile.
	# Wyzie doit rester un secours externe : il ne doit pas proposer de doublon quand
	# le fichier contient déjà une piste qui correspond à la langue Kodi ou à la langue
	# choisie pour Wyzie.
	#
	# Important : une piste anglaise locale ne bloque Wyzie que si l'utilisateur cherche
	# justement l'anglais. Si l'utilisateur cherche le français et que le fichier ne
	# contient que de l'anglais, Wyzie doit quand même pouvoir chercher du français.
	def _has_usable_local_subtitle_before_wyzie(self, retries=2, sleep_ms=750):
		self._local_english_available_before_wyzie = False
		wyzie_language_is_english = self._language_code_is_english(self.language1)
		kodi_language_is_english = self._language_code_is_english(self.kodi_language1) if self.kodi_language1 else False

		for attempt in range(max(retries, 0) + 1):
			player_id, props = self._get_subtitle_state()
			if player_id is None:
				if sleep_ms and attempt < retries: kodi_utils.sleep(sleep_ms)
				continue
			subtitles = props.get('subtitles') or []
			current = props.get('currentsubtitle') or {}
			enabled = props.get('subtitleenabled')
			_sub_log('Local subtitle availability check -> attempt=%s/%s | tracks=%s | enabled=%s | current=%s' % (attempt + 1, retries + 1, len(subtitles), enabled, current))
			if not subtitles:
				if sleep_ms and attempt < retries: kodi_utils.sleep(sleep_ms)
				continue

			for track in subtitles:
				reason = ''

				# La langue Kodi reste prioritaire lorsqu'elle est exploitable.
				if self.kodi_language1 and self._subtitle_track_matches_language(track, self.kodi_language1):
					reason = 'kodi_language'

				# La langue choisie pour Wyzie bloque le fallback si elle existe déjà localement.
				elif self._subtitle_track_matches_language(track, self.language1):
					reason = 'wyzie_preferred_language'

				# L'anglais local est noté, mais ne bloque pas une recherche FR/ES/PT/etc.
				# Il bloque seulement si la préférence Kodi ou Wyzie est elle-même anglaise.
				elif self._subtitle_track_matches_language(track, 'eng'):
					self._local_english_available_before_wyzie = True
					if wyzie_language_is_english or kodi_language_is_english:
						reason = 'english_selected_language'

				# Une piste forced sans langue exploitable ne bloque pas Wyzie à elle seule.
				# Si Kodi l'a vraiment choisie, _respect_kodi_current_subtitle() l'a déjà respectée.
				if reason:
					_sub_log('Local subtitle availability check -> usable local track found, Wyzie skipped | reason=%s | track=%s' % (reason, track))
					return True

			if sleep_ms and attempt < retries: kodi_utils.sleep(sleep_ms)
		_sub_log('Local subtitle availability check -> no usable local/embedded subtitle found | local_english_available=%s' % self._local_english_available_before_wyzie)
		return False

	# Vérifie si la piste courante de Kodi correspond réellement à une langue acceptable.
	# Important : le mode Kodi “forced_only” peut parfois sélectionner la première piste forcée
	# disponible dans le fichier, même si elle est en espagnol ou dans une autre langue.
	# alkoFlix ne doit donc pas activer aveuglément n'importe quelle piste forced.
	def _subtitle_track_is_acceptable_for_kodi_respect(self, track):
		if not isinstance(track, dict):
			return False, 'invalid_track'

		# La langue Kodi reste prioritaire lorsqu'elle est réellement connue.
		if self.kodi_language1 and self._subtitle_track_matches_language(track, self.kodi_language1):
			return True, 'kodi_language'

		# La langue choisie dans alkoFlix/Wyzie est l'autre repère fiable.
		# Exemple : préférence Wyzie = Français; une piste forced espagnole ne doit pas être activée.
		if self._subtitle_track_matches_language(track, self.language1):
			return True, 'wyzie_preferred_language'

		# L’anglais est accepté seulement si la préférence Kodi ou Wyzie est elle-même anglaise.
		# Sinon, une piste anglaise locale ne doit pas bloquer une recherche française via Wyzie.
		wyzie_language_is_english = self._language_code_is_english(self.language1)
		kodi_language_is_english = self._language_code_is_english(self.kodi_language1) if self.kodi_language1 else False
		if (wyzie_language_is_english or kodi_language_is_english) and self._subtitle_track_matches_language(track, 'eng'):
			return True, 'english_selected_language'

		return False, 'foreign_or_unwanted_language'

	# Si Kodi a déjà choisi/activé un sous-titre, alkoFlix respecte ce choix seulement
	# si la piste correspond à une langue acceptable. Cela évite d'activer une piste
	# forcée étrangère simplement parce qu'elle est marquée “default/forced” dans le fichier.
	# Cas important : Kodi peut sélectionner une piste forced, mais laisser subtitleenabled=False.
	# Dans ce cas précis, alkoFlix peut rendre visible cette piste uniquement si sa langue est cohérente.
	def _respect_kodi_current_subtitle(self, retries=2, sleep_ms=750):
		for attempt in range(max(retries, 0) + 1):
			player_id, props = self._get_subtitle_state()
			if player_id is None:
				if sleep_ms and attempt < retries: kodi_utils.sleep(sleep_ms)
				continue
			subtitles = props.get('subtitles') or []
			current = props.get('currentsubtitle') or {}
			enabled = props.get('subtitleenabled')
			_sub_log('Kodi subtitle preference check -> attempt=%s/%s | tracks=%s | enabled=%s | current=%s' % (attempt + 1, retries + 1, len(subtitles), enabled, current))

			track = current
			if isinstance(current, dict) and current.get('index') is not None:
				current_index = current.get('index')
				track = next((s for s in subtitles if s.get('index') == current_index), current)

			if isinstance(track, dict) and track:
				acceptable, reason = self._subtitle_track_is_acceptable_for_kodi_respect(track)

				if enabled:
					if acceptable:
						_sub_log('Kodi subtitle preference respected -> current subtitle already enabled | reason=%s | track=%s' % (reason, track))
						return True
					_sub_log('Kodi subtitle preference not respected -> current subtitle enabled but language is not acceptable | reason=%s | track=%s' % (reason, track))

				if self._is_forced_subtitle_track(track):
					if acceptable:
						_sub_log('Kodi subtitle preference respected -> forced track already selected by Kodi, enabling display only | reason=%s | track=%s' % (reason, track))
						self.showSubtitles(True)
						return True
					_sub_log('Kodi subtitle preference not respected -> forced track selected by Kodi but language is not acceptable | reason=%s | track=%s' % (reason, track))

			if sleep_ms and attempt < retries: kodi_utils.sleep(sleep_ms)
		_sub_log('Kodi subtitle preference check -> no current Kodi subtitle to respect')
		return False


	# Vérifie une seule fois les sous-titres locaux/intégrés exposés par Kodi.
	# Si une piste correspond à la langue choisie, elle est sélectionnée.
	def _select_embedded_subtitle_once(self):
		_sub_log('Local subtitle check -> start')
		self._last_selected_embedded_is_forced = False
		# Helpers pour distinguer les variantes portugaises lorsque c’est possible.
		# Exemple : portugais Portugal vs portugais Brésil.
		def _is_brazil_text(text):
			return any(i in text for i in ('brazil', 'brasil', 'brazilian', 'pt-br', 'ptbr', 'pt_br'))
		def _is_portugal_text(text):
			return any(i in text for i in ('portugal', 'iberian', 'european', 'pt-pt', 'ptpt', 'pt_pt'))
		try:
			# Récupération du lecteur vidéo actif via JSON-RPC Kodi.
			player_query = {"jsonrpc": "2.0", "method": "Player.GetActivePlayers", "id": 1}
			player_resp = json.loads(kodi_utils.execJSONRPC(json.dumps(player_query)))
			player_id = next((p.get('playerid') for p in player_resp.get('result', []) if p.get('type') == 'video'), None)
			if player_id is None:
				_sub_log('Local subtitle check -> no active video player')
				return False
			# Récupération des pistes de sous-titres disponibles,
			# de la piste courante et de l’état d’affichage.
			props_query = {
				"jsonrpc": "2.0",
				"method": "Player.GetProperties",
				"params": {"playerid": player_id, "properties": ["subtitles", "currentsubtitle", "subtitleenabled"]},
				"id": 1
			}
			props_resp = json.loads(kodi_utils.execJSONRPC(json.dumps(props_query)))
			props = props_resp.get('result', {})
			subtitles = props.get('subtitles') or []
			_sub_log('Local subtitle check -> %s subtitle track(s) detected | enabled=%s | current=%s' % (len(subtitles), props.get('subtitleenabled'), props.get('currentsubtitle')))
			if not subtitles:
				_sub_log('Local subtitle check -> no local/embedded subtitles found')
				return False
			# Conversion vers le format court utilisé par Kodi afin de comparer les langues.
			search_lang = (kodi_utils.convert_language(self.language1, format='short') or '').lower()
			def _sub_text(sub):
				lang = (sub.get('language') or '')
				name = (sub.get('name') or '')
				return ('%s %s' % (lang, name)).lower()
			def _lang_matches(sub):
				lang = (sub.get('language') or '').lower()
				if lang:
					candidates = {search_lang, (self.language1 or '').lower()}
					if search_lang == 'pt' or (self.language1 or '') in ('por', 'pob'):
						candidates.update(('pt', 'por', 'pob', 'pt-br', 'pt_br', 'ptbr'))
					return lang in candidates
				return 'portuguese' in _sub_text(sub)
			if self.language1 in ('por', 'pob') or search_lang == 'pt':
				if self.language1 == 'por':
					idx = next((s.get('index') for s in subtitles if _lang_matches(s) and _is_portugal_text(_sub_text(s))), None)
					if idx is None:
						idx = next((s.get('index') for s in subtitles if _lang_matches(s) and not _is_brazil_text(_sub_text(s))), None)
				elif self.language1 == 'pob':
					idx = next((s.get('index') for s in subtitles if _lang_matches(s) and _is_brazil_text(_sub_text(s))), None)
				else:
					idx = next((s.get('index') for s in subtitles if _lang_matches(s)), None)
			else:
				idx = next((s.get('index') for s in subtitles if _lang_matches(s)), None)
			if idx is None:
				_sub_log('Local subtitle check -> no matching track for language=%s' % self.language1)
				return False

			# Identifie la piste sélectionnée afin de savoir si elle est marquée “forced”.
			# Kodi expose parfois ce statut via isforced=True, et parfois seulement
			# dans le nom de la piste. On tient compte des deux cas.
			selected_track = next((s for s in subtitles if s.get('index') == idx), {})
			selected_text = _sub_text(selected_track)
			self._last_selected_embedded_is_forced = bool(selected_track.get('isforced')) or 'forced' in selected_text or 'forcé' in selected_text or 'forces' in selected_text
			_sub_log('Local subtitle check -> selected track forced=%s | track=%s' % (self._last_selected_embedded_is_forced, selected_track))

			current = props.get('currentsubtitle')
			if isinstance(current, dict) and current.get('index') == idx:
				_sub_log('Local subtitle check -> matching track already selected index=%s' % idx)
				return True
			set_query = {"jsonrpc": "2.0", "method": "Player.SetSubtitle", "params": {"playerid": player_id, "subtitle": idx}, "id": 1}
			kodi_utils.execJSONRPC(json.dumps(set_query))
			_sub_log('Local subtitle check -> selected local track index=%s' % idx)
			return True
		except Exception as e:
			_sub_log('Local subtitle check exception -> %s: %s' % (type(e).__name__, e))
			return False

	# Sélection locale avec retries optionnels.
	# Utile car Kodi peut mettre un court délai avant d’exposer les pistes.
	def select_embedded_subtitle(self, retries=0, sleep_ms=0):
		retries = max(retries, 0)
		_sub_log('Local subtitle selection -> retries=%s | sleep_ms=%s | auto_enable=%s' % (retries, sleep_ms, self.auto_enable))
		for attempt in range(retries + 1):
			_sub_log('Local subtitle selection -> attempt %s/%s' % (attempt + 1, retries + 1))

			# Si la piste audio courante correspond déjà à la langue choisie,
			# on ne touche pas aux sous-titres locaux.
			#
			# Exemple : fichier MULTi avec audio français sélectionné automatiquement.
			# Dans ce cas, Kodi doit garder la main sur ses préférences,
			# notamment “sous-titres forcés seulement”.
			if self._current_audio_matches_language():
				_sub_log('Local subtitle selection skipped -> audio already matches selected language, Kodi keeps subtitle preferences')
				return True

			if self._select_embedded_subtitle_once():
				# alkoFlix active une piste locale seulement si l’option dédiée est activée.
				# Sinon, la sélection peut être préparée, mais Kodi garde la main sur
				# l’affichage réel des sous-titres.
				if self.auto_enable == 'true':
					_sub_log('Local subtitle selection -> showSubtitles(True) | forced=%s | auto_enable=%s' % (self._last_selected_embedded_is_forced, self.auto_enable))
					self.showSubtitles(True)
				else:
					_sub_log('Local subtitle selection -> selected only, Kodi display setting respected')
				return True
			if sleep_ms and attempt < retries: kodi_utils.sleep(sleep_ms)
		_sub_log('Local subtitle selection -> no local subtitle selected')
		return False

	# Point d’entrée du module.
	# Ordre volontaire : local/intégré -> fichier déjà téléchargé -> recherche Wyzie.

	# Vérifie la langue de la piste audio actuellement lue.
	# Objectif : si le média joue déjà dans la langue choisie pour les sous-titres
	# (ex. audio français + sous-titres français), on évite de lancer Wyzie inutilement
	# lorsqu’aucun sous-titre local adapté n’a été trouvé.
	def _current_audio_matches_language(self):
		_sub_log('Audio language check -> start')
		try:
			player_query = {"jsonrpc": "2.0", "method": "Player.GetActivePlayers", "id": 1}
			player_resp = json.loads(kodi_utils.execJSONRPC(json.dumps(player_query)))
			player_id = next((p.get('playerid') for p in player_resp.get('result', []) if p.get('type') == 'video'), None)
			if player_id is None:
				_sub_log('Audio language check -> no active video player')
				return False

			props_query = {
				"jsonrpc": "2.0",
				"method": "Player.GetProperties",
				"params": {"playerid": player_id, "properties": ["currentaudiostream"]},
				"id": 1
			}
			props_resp = json.loads(kodi_utils.execJSONRPC(json.dumps(props_query)))
			props = props_resp.get('result', {})
			audio = props.get('currentaudiostream') or {}
			audio_language = (audio.get('language') or '').lower()
			audio_name = (audio.get('name') or '').lower()
			audio_text = '%s %s' % (audio_language, audio_name)

			target_language = (self.language1 or '').lower()
			target_short = (kodi_utils.convert_language(self.language1, format='short') or '').lower()
			candidates = {target_language, target_short}

			# Équivalences fréquentes entre ISO 639-1, ISO 639-2 et libellés Kodi.
			# Kodi peut retourner "fre", "fra", "fr" ou parfois un nom de piste en texte libre.
			if target_language in ('fre', 'fra') or target_short == 'fr':
				candidates.update(('fre', 'fra', 'fr', 'french', 'français', 'francais'))
			elif target_language == 'eng' or target_short == 'en':
				candidates.update(('eng', 'en', 'english', 'anglais'))
			elif target_language == 'spa' or target_short == 'es':
				candidates.update(('spa', 'es', 'spanish', 'espagnol'))
			elif target_language in ('por', 'pob') or target_short == 'pt':
				candidates.update(('por', 'pob', 'pt', 'pt-br', 'pt_br', 'ptbr', 'pt-pt', 'pt_pt', 'ptpt', 'portuguese', 'portugais'))

			candidates = {i for i in candidates if i}
			match = audio_language in candidates or any(i in audio_text for i in candidates if len(i) > 2)

			_sub_log('Audio language check -> current=%s | name=%s | expected=%s/%s | match=%s' % (audio_language, audio_name, target_language, target_short, match))
			return match
		except Exception as e:
			_sub_log('Audio language check exception -> %s: %s' % (type(e).__name__, e))
			return False


	def run(self, query, imdb_id, season, episode, poster):
		_sub_log('Run -> query=%s | imdb_id=%s | season=%s | episode=%s | action=%s | wyzie_language=%s | kodi_language=%s | api_key=%s' % (query, imdb_id, season, episode, self.subs_action, self.language1, self.kodi_language1, 'présente' if self.has_wyzie_api else 'absente'))

		# Délai volontaire : laisse Kodi démarrer la lecture et exposer ses pistes locales/intégrées.
		# Kodi doit toujours avoir la priorité avant toute logique alkoFlix/Wyzie.
		kodi_utils.sleep(2500)

		# Emplacement temporaire volontaire.
		# alkoFlix nettoie ses sous-titres temporaires au démarrage.
		subtitle_path = 'special://temp/'
		sub_filename = 'alkoFlixSubs_%s_%s_%s' % (imdb_id, season, episode) if season else 'alkoFlixSubs_%s' % imdb_id
		search_filename = sub_filename + '_%s.srt' % self.language1

		# Étape 1 : respecter Kodi en premier, sans exception.
		# Si Kodi a déjà appliqué une piste locale/intégrée, alkoFlix ne remplace rien.
		# Si Kodi a sélectionné une piste forced mais que l’affichage est encore fermé,
		# alkoFlix se limite à rendre visible cette piste déjà choisie par Kodi.
		if self._respect_kodi_current_subtitle(retries=3, sleep_ms=750):
			_sub_log('Run finished -> Kodi subtitle preference respected before Wyzie fallback')
			return

		# Sans clé API Wyzie, alkoFlix n’intervient pas.
		# Les réglages Kodi restent donc les seuls maîtres à bord pour les sous-titres locaux/intégrés.
		if not self.has_wyzie_api:
			_sub_log('Run finished -> no Wyzie API key, Kodi-only subtitle mode')
			return

		# Si Kodi est réglé sur aucun sous-titre, Wyzie ne doit pas contourner ce choix.
		if self.kodi_subtitle_preference_disabled:
			_sub_log('Run finished -> Kodi subtitle preference disabled/none, Wyzie fallback skipped')
			return

		# Si l’action sous-titres d’alkoFlix est désactivée, Wyzie ne sert pas de fallback.
		# Kodi a déjà eu sa chance plus haut, donc on s’arrête ici.
		if not self.subs_action in ('auto', 'select'):
			_sub_log('Run finished -> Wyzie action is off after Kodi preference check')
			return

		# Avant d’autoriser Wyzie, on vérifie toute la liste des pistes locales/intégrées.
		# Même si Kodi n’a rien activé automatiquement, Wyzie ne doit pas proposer
		# de doublon lorsqu’un sous-titre utilisable est déjà présent dans le fichier.
		if self._has_usable_local_subtitle_before_wyzie(retries=2, sleep_ms=750):
			_sub_log('Run finished -> usable local/embedded subtitle already exists, Wyzie fallback skipped')
			return

		# À partir d’ici, Kodi n’a rien appliqué et aucune piste locale/intégrée utile
		# n’a été trouvée dans le fichier. Wyzie devient alors seulement un plan B.
		_sub_log('Wyzie fallback allowed -> no usable local/embedded subtitle found')

		# Étape 2 : sous-titre Wyzie déjà téléchargé dans special://temp/.
		# On ne l’applique automatiquement qu’en mode Wyzie automatique.
		# En mode proposition, on repasse par la liste afin que l’utilisateur choisisse.
		def _downloaded_subs():
			if self.subs_action != 'auto':
				_sub_log('Step 2 -> downloaded Wyzie subtitle skipped because Wyzie mode is not automatic')
				return False
			_sub_log('Step 2 -> checking already downloaded Wyzie subtitle file=%s' % search_filename)
			files = kodi_utils.list_dirs(subtitle_path)[1]
			final_match = next((i for i in files if i == search_filename), None)
			if not final_match:
				_sub_log('Step 2 -> no downloaded Wyzie subtitle found')
				return False
			subtitle = '%s%s' % (subtitle_path, final_match)
			_sub_log('Step 2 -> downloaded Wyzie subtitle found -> %s' % subtitle)
			kodi_utils.notification(32792, icon=addon_icon)
			return subtitle

		# Étape 3 : recherche externe via l’API Wyzie.
		# Mode auto   -> alkoFlix applique automatiquement le meilleur résultat selon la langue Wyzie.
		# Mode select -> alkoFlix propose une liste, triée avec la langue Wyzie en premier.
		def _searched_subs():
			_sub_log('Step 3 -> Wyzie search start')
			search_language = kodi_utils.convert_language(self.language1, format='short')
			_sub_log('Step 3 -> Wyzie preferred language=%s/%s' % (self.language1, search_language))
			result = subtitles_search(imdb_id, season, episode, self.apikey, self.language1)
			if isinstance(result, str):
				_sub_log('Step 3 -> Wyzie error returned -> %s' % result)
				if result == 'Wyzie temporairement indisponible':
					return kodi_utils.notification(result, icon=addon_icon)
				return kodi_utils.notification('Subtitles Error: %s' % result, icon=addon_icon)
			if not result:
				_sub_log('Step 3 -> Wyzie returned no result')
				return kodi_utils.notification(32793, icon=addon_icon)
			_sub_log('Step 3 -> Wyzie returned %s result(s)' % len(result))

			# Helpers de tri pour les variantes portugaises dans les résultats Wyzie.
			def _is_brazil(item):
				display = (item.get('display') or '').lower()
				flag = (item.get('flagUrl') or '').lower()
				text = '%s %s' % (display, flag)
				return any(i in text for i in ('brazil', 'brasil', 'brazilian', 'pt-br', 'ptbr', 'pt_br'))
			def _is_portugal(item):
				display = (item.get('display') or '').lower()
				flag = (item.get('flagUrl') or '').lower()
				text = '%s %s' % (display, flag)
				return any(i in text for i in ('portugal', 'iberian', 'european', 'pt-pt', 'ptpt', 'pt_pt'))
			def _lang_matches(item):
				lang = (item.get('language') or '').lower()
				if not lang: return False
				search_lang = (search_language or '').lower()
				candidates = {search_lang, (self.language1 or '').lower()}
				if search_lang == 'fr' or (self.language1 or '') in ('fre', 'fra'):
					candidates.update(('fr', 'fre', 'fra'))
				elif search_lang == 'en' or (self.language1 or '') == 'eng':
					candidates.update(('en', 'eng'))
				elif search_lang == 'es' or (self.language1 or '') == 'spa':
					candidates.update(('es', 'spa'))
				elif search_lang == 'pt' or (self.language1 or '') in ('por', 'pob'):
					candidates.update(('pt', 'por', 'pob', 'pt-br', 'pt_br', 'ptbr'))
				candidates = {i for i in candidates if i}
				return lang in candidates
			def _is_english(item):
				# L’anglais reste un plan B utile lorsque la langue choisie n’est pas disponible chez Wyzie.
				lang = (item.get('language') or '').lower()
				display = (item.get('display') or '').lower()
				flag = (item.get('flagUrl') or '').lower()
				text = '%s %s %s' % (lang, display, flag)
				return lang in ('en', 'eng') or 'english' in text or 'anglais' in text

			def _priority_rank(item):
				# Ordre voulu dans la fenêtre de sélection : langue Wyzie d’abord, anglais ensuite,
				# puis les autres langues en dernier.
				if _lang_matches(item): return 0
				if _is_english(item): return 1
				return 2

			# Tri stable : la langue préférée dans alkoFlix/Wyzie remonte en haut,
			# mais l’utilisateur peut toujours choisir autre chose en mode proposition.
			result.sort(key=lambda k: ((k.get('display') or '').lower(), (k.get('source') or '').lower(), (k.get('media') or '').lower()), reverse=False)
			result.sort(key=_priority_rank, reverse=False)

			def _auto_choice_index():
				# En mode automatique, on choisit d’abord la langue configurée pour Wyzie.
				# Si elle est absente, on permet l’anglais comme plan B.
				# Les autres langues ne sont pas choisies automatiquement.
				if search_language == 'pt' or (self.language1 or '') in ('por', 'pob'):
					if self.language1 == 'por':
						idx = next((i for i, _ in enumerate(result) if _lang_matches(_) and _is_portugal(_)), -1)
						if idx >= 0: return idx
						idx = next((i for i, _ in enumerate(result) if _lang_matches(_) and not _is_brazil(_)), -1)
						if idx >= 0: return idx
					elif self.language1 == 'pob':
						idx = next((i for i, _ in enumerate(result) if _lang_matches(_) and _is_brazil(_)), -1)
						if idx >= 0: return idx

				idx = next((i for i, _ in enumerate(result) if _lang_matches(_)), -1)
				if idx >= 0: return idx

				if self._local_english_available_before_wyzie and not self._language_code_is_english(self.language1):
					_sub_log('Step 3 -> English Wyzie fallback skipped because a local English subtitle already exists')
					return -1

				idx = next((i for i, _ in enumerate(result) if _is_english(_)), -1)
				if idx >= 0:
					_sub_log('Step 3 -> no selected-language subtitle found, English fallback selected index=%s' % idx)
					return idx

				return -1

			if self.subs_action == 'auto':
				chosen_sub = _auto_choice_index()
				_sub_log('Step 3 -> Wyzie automatic mode, selected index=%s' % chosen_sub)
			else:
				_sub_log('Step 3 -> Wyzie proposal mode, manual dialog opened')
				try: video_path = self.getPlayingFile()
				except: video_path = ''
				if '|' in video_path: video_path = video_path.split('|')[0]
				video_path = os.path.basename(video_path)
				heading = '%s - %s' % (ls(32246).upper(), video_path)
				def _builder():
					for i in result:
						listitem = kodi_utils.make_listitem()
						listitem.setLabel('%s%s' % (i['display'].upper(), ' (SDH)' if i['isHearingImpaired'] else ''))
						listitem.setLabel2('%s[CR]%s' % (i['source'].upper(), i['media']))
						listitem.setArt({'icon': i['flagUrl'].replace('24.png', '64.png')})
						yield listitem
				self.pause()
				chosen_sub = kodi_utils.dialog.select(heading, list(_builder()), useDetails=True)
				self.pause()

			if chosen_sub < 0:
				if self.subs_action == 'auto':
					_sub_log('Step 3 -> Wyzie returned results, but none matched selected language=%s or English fallback' % self.language1)
					return kodi_utils.notification('Aucun sous-titre trouvé dans la langue choisie ou en anglais', icon=addon_icon)
				_sub_log('Step 3 -> Wyzie proposal canceled by user')
				return False

			chosen_sub = result[chosen_sub]
			_sub_log('Step 3 -> selected Wyzie subtitle -> language=%s | source=%s | display=%s | media=%s' % (chosen_sub.get('language'), chosen_sub.get('source'), chosen_sub.get('display'), chosen_sub.get('media')))
			try: lang = kodi_utils.convert_language(chosen_sub['language'])
			except: lang = chosen_sub['language']
			encoding = chosen_sub['encoding'].lower()
			final_filename = sub_filename + '_%s.%s' % (lang, chosen_sub['format'])
			final_path = '%s%s' % (subtitle_path, final_filename)

			# Téléchargement du sous-titre choisi, puis écriture du fichier .srt temporaire.
			response = subtitles_download(chosen_sub['url'])
			if isinstance(response, str): return kodi_utils.notification('Subtitles Error: %s' % response, icon=addon_icon)
			try:
				if not 'utf-8' in encoding:
					import codecs
					content = codecs.decode(response.content, encoding=encoding)
				else: content = response.text
			except: content = response.content
			with kodi_utils.open_file(final_path, 'w') as file: file.write(content)
			_sub_log('Step 3 -> Wyzie subtitle written -> %s' % final_path)
			kodi_utils.sleep(1000)
			return final_path

		subtitle = _downloaded_subs()
		if subtitle:
			_sub_log('Run finished -> setting already downloaded Wyzie subtitle')
			return self.setSubtitles(subtitle)

		subtitle = _searched_subs()
		if subtitle:
			_sub_log('Run finished -> setting Wyzie subtitle')
			return self.setSubtitles(subtitle)

		_sub_log('Run finished -> no subtitle set')
