import json
import time
import requests
from threading import Thread
from caches import trakt_cache
from caches.main_cache import cache_object
from indexers.metadata import movie_external_id, tvshow_external_id
from modules import kodi_utils, settings
from modules.cache import check_databases
from modules.utils import sort_list, make_thread_list, jsondate_to_datetime, paginate_list, get_datetime, TaskPool

ls, logger, js2date = kodi_utils.local_string, kodi_utils.logger, jsondate_to_datetime
get_setting, set_setting = kodi_utils.get_setting, kodi_utils.set_setting
EXPIRES_2_DAYS = 48
V2_API_KEY = get_setting('trakt.client_id')
CLIENT_SECRET = get_setting('trakt.client_secret')
REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob'
base_url = 'https://api.trakt.tv/%s'
timeout = 10.05
session = requests.Session()
retry = requests.adapters.Retry(total=None, status=1, status_forcelist=(429, 502, 503, 504))
session.mount('https://api.trakt.tv', requests.adapters.HTTPAdapter(pool_maxsize=100, max_retries=retry))

TRAKT_FAVOURITES_ACCOUNT_LIMIT_PROP = 'alkoflix_trakt_favourites_account_limit_reached'
TRAKT_FAVOURITES_LIMIT_NOTICE_PROP = 'alkoflix_trakt_favourites_account_limit_notice'
TRAKT_FAVOURITES_LIMIT_MESSAGE = 'Limite de favoris Trakt atteinte. Le favori reste local dans alkoFlix.'

ALKOFIX_TRAKT_FAVOURITES_LISTS = {
	'movie': 'alkoFlix - Films favoris',
	'tvshow': 'alkoFlix - Séries favorites',
	'season': 'alkoFlix - Saisons favorites',
	'episode': 'alkoFlix - Épisodes favoris'
}


def _trakt_favourites_add_path(path):
	return str(path or '') == 'sync/favorites'

def _trakt_favourites_remove_path(path):
	return str(path or '') == 'sync/favorites/remove'

def _notify_trakt_favourites_account_limit(force=False):
	try:
		now = time.time()
		last = float(kodi_utils.get_property(TRAKT_FAVOURITES_LIMIT_NOTICE_PROP) or 0)
		# Évite de répéter la notification si plusieurs ajouts sont faits d'affilée.
		if force or now - last >= 60:
			kodi_utils.notification(TRAKT_FAVOURITES_LIMIT_MESSAGE, time=5000)
			kodi_utils.set_property(TRAKT_FAVOURITES_LIMIT_NOTICE_PROP, str(now))
	except Exception as e:
		logger('trakt favorites limit notice failed', '%s: %s' % (type(e).__name__, e))

def _set_trakt_favourites_account_limit():
	try: kodi_utils.set_property(TRAKT_FAVOURITES_ACCOUNT_LIMIT_PROP, 'true')
	except: pass
	_notify_trakt_favourites_account_limit(force=True)

def _clear_trakt_favourites_account_limit():
	try:
		kodi_utils.clear_property(TRAKT_FAVOURITES_ACCOUNT_LIMIT_PROP)
		kodi_utils.clear_property(TRAKT_FAVOURITES_LIMIT_NOTICE_PROP)
	except: pass

def _trakt_favourites_account_limit_active(path):
	try:
		if not _trakt_favourites_add_path(path): return False
		active = kodi_utils.get_property(TRAKT_FAVOURITES_ACCOUNT_LIMIT_PROP) == 'true'
		if active: _notify_trakt_favourites_account_limit()
		return active
	except: return False

def call_trakt(path, params=None, data=None, with_auth=True, method=None, pagination=False, page=1):
	headers = {'trakt-api-key': V2_API_KEY, 'trakt-api-version': '2', 'Content-Type': 'application/json'}
	if with_auth is True and (token := settings.trakt_token()): headers['Authorization'] = 'Bearer %s' % token
	if pagination:
		if params is None: params = {}
		params['page'] = page
	if data is not None and _trakt_favourites_account_limit_active(path):
		logger('trakt favorites sync skipped', 'account limit active | path=%s' % path)
		return {'status': 420, 'account_limit': True, 'rate_limited': True, 'skipped': True, 'path': path}
	last_error = ''
	for attempt in range(1, 4):
		response = None
		try:
			response = session.request(
				'post' if data is not None else method or 'get',
				base_url % path,
				params=None if data is not None else params,
				data=json.dumps(data) if data else None,
				headers=headers,
				timeout=timeout
			)
			# Trakt peut retourner 409 sur scrobble/stop lorsqu'un autre client
			# (ex. l'addon officiel script.trakt) a déjà finalisé ou verrouille
			# temporairement la lecture. Réessayer ne corrige rien et pollue le log.
			# On traite donc ce cas précis comme un conflit non bloquant.
			if response.status_code == 409 and path == 'scrobble/stop':
				body = (response.text or '')[:200].replace('\n', ' ').replace('\r', ' ')
				logger('trakt conflict', 'path=%s | status=409 | ignored=true | body=%s' % (path, body))
				return {'status': 409, 'ignored': True, 'path': path}
			if response.status_code == 420 and _trakt_favourites_add_path(path):
				body = (response.text or '')[:200].replace('\n', ' ').replace('\r', ' ')
				_set_trakt_favourites_account_limit()
				logger('trakt favorites sync skipped', 'account limit reached | status=420 | path=%s | body=%s' % (path, body))
				return {'status': 420, 'account_limit': True, 'rate_limited': True, 'path': path}
			if not response.ok: response.raise_for_status()
			content_type = response.headers.get('Content-Type', '')
			body = response.text or ''
			if response.status_code == 204 or not body.strip():
				result = {}
			elif 'json' in content_type:
				result = response.json()
			else:
				result = body
#			if 'X-Sort-By' in response.headers and 'X-Sort-How' in response.headers:
#				sort_by, sort_how = response.headers['X-Sort-By'], response.headers['X-Sort-How']
#				result = sort_list(sort_by, sort_how, result, settings.ignore_articles())
			if pagination: result = result, response.headers.get('X-Pagination-Page-Count', page)
			return result
		except Exception as e:
			status = 'aucun'
			body = ''
			try: status = response.status_code
			except: pass
			try: body = (response.text or '')[:200].replace('\\n', ' ').replace('\\r', ' ')
			except: pass
			last_error = 'attempt=%s/3 | path=%s | status=%s | %s: %s | body=%s' % (attempt, path, status, type(e).__name__, e, body)
			logger('trakt error', last_error)
			if attempt < 3: kodi_utils.sleep(750 * attempt)
	return None

def get_trakt(params):
	result = call_trakt(
		params['path'] % params.get('path_insert', ''),
		params=params.get('params', {}),
		data=params.get('data'),
		with_auth=params.get('with_auth', False),
		method=params.get('method'),
		pagination=params.get('pagination', True),
		page=params.get('page')
	)
	return result[0] if params.get('pagination', True) else result

def trakt_refresh():
	try:
		data = {'client_id': V2_API_KEY, 'client_secret': CLIENT_SECRET, 'redirect_uri': REDIRECT_URI}
		data.update({'refresh_token': get_setting('trakt.refresh'), 'grant_type': 'refresh_token'})
		response = call_trakt('oauth/token', data=data, with_auth=False)
		expires = int(response['created_at']) + int(response['expires_in'])
		refresh, token = response['refresh_token'], response['access_token']
		set_setting('trakt.token', token)
		set_setting('trakt.refresh', refresh)
		set_setting('trakt.expires', str(expires))
	except Exception as e: logger('trakt_refresh error', str(e))
	else: return True
	return False

def trakt_expires(func):
	def wrapper(*args, **kwargs):
		if get_setting('trakt.refresh', ''):
			interval = settings.trakt_sync_interval()[1]
			expires = float(get_setting('trakt.expires', '0'))
			refresh = ((-1 * time.time() // 1 * -1) + interval) >= expires
			if refresh and trakt_refresh(): kodi_utils.sleep(1000)
		return func(*args, **kwargs)
	return wrapper

def trakt_recommendations(media_type):
	string = 'trakt_recommendations_%s' % media_type
	url = {'path': '/recommendations/%s', 'path_insert': media_type, 'params': {'limit': 50}, 'with_auth': True, 'pagination': False}
	return trakt_cache.cache_trakt_object(get_trakt, string, url)

def trakt_movies_trending(page_no):
	string = 'trakt_movies_trending_%s' % page_no
	url = {'path': 'movies/trending/%s', 'params': {'limit': 20}, 'page': page_no}
	return cache_object(get_trakt, string, url, json=False, expiration=EXPIRES_2_DAYS)

def trakt_movies_trending_recent(page_no):
	year = get_datetime().year
	years = '%s-%s' % (year-1, year)
	string = 'trakt_movies_trending_recent_%s' % page_no
	url = {'path': 'movies/trending/%s', 'params': {'years': years, 'limit': 20}, 'page': page_no}
	return cache_object(get_trakt, string, url, json=False, expiration=EXPIRES_2_DAYS)

def trakt_movies_most_watched(page_no):
	string = 'trakt_movies_most_watched_%s' % page_no
	url = {'path': 'movies/watched/weekly/%s', 'params': {'limit': 20}, 'page': page_no}
	return cache_object(get_trakt, string, url, json=False, expiration=EXPIRES_2_DAYS)

def trakt_tv_trending(page_no):
	string = 'trakt_tv_trending_%s' % page_no
	url = {'path': 'shows/trending/%s', 'params': {'limit': 20}, 'page': page_no}
	return cache_object(get_trakt, string, url, json=False, expiration=EXPIRES_2_DAYS)

def trakt_tv_trending_recent(page_no):
	year = get_datetime().year
	years = '%s-%s' % (year-1, year)
	string = 'trakt_tv_trending_recent_%s' % page_no
	url = {'path': 'shows/trending/%s', 'params': {'years': years, 'limit': 20}, 'page': page_no}
	return cache_object(get_trakt, string, url, json=False, expiration=EXPIRES_2_DAYS)

def trakt_tv_most_watched(page_no):
	string = 'trakt_tv_most_watched_%s' % page_no
	url = {'path': 'shows/watched/weekly/%s', 'params': {'limit': 20}, 'page': page_no}
	return cache_object(get_trakt, string, url, json=False, expiration=EXPIRES_2_DAYS)

def trakt_moviesanime_trending(page_no):
	string = 'trakt_moviesanime_trending_%s' % page_no
	url = {'path': 'movies/trending/%s', 'params': {'limit': 100, 'genres': 'anime'}, 'page': page_no}
	return cache_object(get_trakt, string, url, json=False, expiration=EXPIRES_2_DAYS)

def trakt_moviesanime_most_watched(page_no):
	string = 'trakt_moviesanime_most_watched_%s' % page_no
	url = {'path': 'movies/watched/all/%s', 'params': {'limit': 20, 'genres': 'anime'}, 'page': page_no}
	return cache_object(get_trakt, string, url, json=False, expiration=EXPIRES_2_DAYS)

def trakt_tvanime_trending(page_no):
	string = 'trakt_tvanime_trending_%s' % page_no
	url = {'path': 'shows/trending/%s', 'params': {'limit': 20, 'genres': 'anime'}, 'page': page_no}
	return cache_object(get_trakt, string, url, json=False, expiration=EXPIRES_2_DAYS)

def trakt_tvanime_most_watched(page_no):
	string = 'trakt_tvanime_most_watched_%s' % page_no
	url = {'path': 'shows/watched/all/%s', 'params': {'limit': 20, 'genres': 'anime'}, 'page': page_no}
	return cache_object(get_trakt, string, url, json=False, expiration=EXPIRES_2_DAYS)

def trakt_get_hidden_items(list_type):
	def _get_trakt_ids(item):
		tmdb_id = get_trakt_tvshow_id(item['show']['ids'])
		results_append(tmdb_id)
	def _process(url):
		hidden_data = get_trakt(url)
		threads = list(make_thread_list(_get_trakt_ids, hidden_data, Thread))
		[i.join() for i in threads]
		return results
	results = []
	results_append = results.append
	string = 'trakt_hidden_items_%s' % list_type
	url = {'path': 'users/hidden/%s', 'path_insert': list_type, 'params': {'limit': 1000, 'type': 'show'}, 'with_auth': True, 'pagination': False}
	return trakt_cache.cache_trakt_object(_process, string, url)

def trakt_droplist(media_type, page_no, letter):
	results = trakt_get_hidden_items('dropped')
	return [{'media_ids': {'tmdb': i}} for i in results], 1

def trakt_watched_unwatched(action, media, media_id, tvdb_id=0, season=None, episode=None, key='tmdb'):
	if action == 'mark_as_watched': url, result_key = 'sync/history', 'added'
	else: url, result_key = 'sync/history/remove', 'deleted'
	if media == 'movies':
		success_key = 'movies'
		data = {'movies': [{'ids': {key: media_id}}]}
	else:
		success_key = 'episodes'
		if media == 'episode': data = {'shows': [{'seasons': [{'episodes': [{'number': int(episode)}], 'number': int(season)}], 'ids': {key: media_id}}]}
		elif media =='shows': data = {'shows': [{'ids': {key: media_id}}]}
		else: data = {'shows': [{'ids': {key: media_id}, 'seasons': [{'number': int(season)}]}]}#season
	result = call_trakt(url, data=data)
	success = False
	try: success = result[result_key][success_key] > 0
	except Exception as e: logger('trakt watched error', 'action=%s | media=%s | key=%s | id=%s | %s: %s' % (action, media, key, media_id, type(e).__name__, e))
	if not success:
		if media != 'movies' and tvdb_id != 0 and key != 'tvdb': return trakt_watched_unwatched(action, media, tvdb_id, 0, season, episode, 'tvdb')
	return success

def trakt_progress(action, media, media_id, percent, season=None, episode=None, resume_id=None, refresh_trakt=False):
	if action == 'clear_progress':
		url = 'sync/playback/%s' % resume_id
		call_trakt(url, method='delete')
	else:
		scrobble_action = {
			'set_progress': 'pause',
			'pause_progress': 'pause',
			'start_progress': 'start',
			'stop_progress': 'stop'
		}.get(action, 'pause')
		url = 'scrobble/%s' % scrobble_action
		if media in ('movie', 'movies'): data = {'movie': {'ids': {'tmdb': media_id}}, 'progress': float(percent)}
		else: data = {'show': {'ids': {'tmdb': media_id}}, 'episode': {'season': int(season), 'number': int(episode)}, 'progress': float(percent)}
		call_trakt(url, data=data)
	if refresh_trakt: trakt_sync_activities()

def trakt_collection_lists(media_type, param1, param2):
	# param1 = the type of list to be returned (from 'new_page' param), param2 is currently not used
	limit = 20
	string_insert = 'movie' if media_type in ('movie', 'movies') else 'tvshow'
	window_property_name = 'alkoflix_trakt_collection_%s' % string_insert
	try: data = json.loads(kodi_utils.get_property(window_property_name))
	except: data = trakt_fetch_collection_watchlist('collection', media_type)
	if param1 == 'recent':
		data.sort(key=lambda k: k['collected_at'], reverse=True)
	elif param1 == 'random':
		import random
		random.shuffle(data)
	data = data[:limit]
	return data, 1

def trakt_collection(media_type, page_no, letter, list_sort='', list_random_seed=''):
	string_insert = 'movie' if media_type in ('movie', 'movies') else 'tvshow'
	original_list = trakt_fetch_collection_watchlist('collection', media_type)
	if list_sort:
		from modules.list_sorting import apply_simple_context_sort, normalise_list_sort_key
		if normalise_list_sort_key(list_sort) != 'original':
			original_list = apply_simple_context_sort(original_list, {'list_sort': list_sort, 'list_random_seed': list_random_seed}, added_key='collected_at')
	if settings.paginate():
		limit = settings.page_limit()
		final_list, total_pages = paginate_list(original_list, page_no, letter, limit)
	else: final_list, total_pages = original_list, 1
	return final_list, total_pages

def trakt_watchlist(media_type, page_no, letter, list_sort='', list_random_seed=''):
	string_insert = 'movie' if media_type in ('movie', 'movies') else 'tvshow'
	original_list = trakt_fetch_collection_watchlist('watchlist', media_type)
	if not settings.show_unaired_watchlist():
		current_date = get_datetime()
		str_format = '%Y-%m-%d' if media_type in ('movie', 'movies') else '%Y-%m-%dT%H:%M:%S.%fZ'
		original_list = [i for i in original_list if i.get('premiered') and js2date(i.get('premiered'), str_format, remove_time=True) <= current_date]
	effective_sort = list_sort or settings.default_list_sort('watchlist')
	if effective_sort:
		from modules.list_sorting import apply_simple_context_sort, normalise_list_sort_key
		if normalise_list_sort_key(effective_sort) != 'original':
			original_list = apply_simple_context_sort(original_list, {'list_sort': effective_sort, 'list_random_seed': list_random_seed}, added_key='collected_at')
	if settings.paginate():
		limit = settings.page_limit()
		final_list, total_pages = paginate_list(original_list, page_no, letter, limit)
	else: final_list, total_pages = original_list, 1
	return final_list, total_pages

def trakt_favorites(media_type, page_no, letter, list_sort='', list_random_seed=''):
	string_insert = 'movie' if media_type in ('movie', 'movies') else 'tvshow'
	original_list = trakt_fetch_favorites(media_type)
	effective_sort = list_sort or settings.default_list_sort('favorites')
	if effective_sort:
		from modules.list_sorting import apply_simple_context_sort, normalise_list_sort_key
		if normalise_list_sort_key(effective_sort) != 'original':
			original_list = apply_simple_context_sort(original_list, {'list_sort': effective_sort, 'list_random_seed': list_random_seed}, added_key='favorited_at')
	if settings.paginate():
		limit = settings.page_limit()
		final_list, total_pages = paginate_list(original_list, page_no, letter, limit)
	else: final_list, total_pages = original_list, 1
	return final_list, total_pages

def trakt_fetch_all_pages(params):
	path = params['path'] % params['path_insert'] if 'path_insert' in params else params['path']
	base_params = dict(params.get('params') or {})
	base_params['limit'] = int(base_params.get('limit') or 250)
	with_auth, method = params.get('with_auth', False), params.get('method')
	page, results = 1, []
	while True:
		page_params = dict(base_params)
		response = call_trakt(path, params=page_params, with_auth=with_auth, method=method, pagination=True, page=page)
		if response is None:
			return None if not results else results
		if not response: break
		data, pages = response
		if data is None:
			return None if not results else results
		if not data: break
		if isinstance(data, list): results.extend(data)
		else: results.append(data)
		try: pages = int(pages or page)
		except: pages = page
		if page >= pages: break
		page += 1
	return results

def trakt_fetch_collection_watchlist(list_type, media_type):
	if not get_setting('trakt_user', ''): return []
	if media_type in ('movie', 'movies'): key, string_insert, path_insert = ('movie', 'movie', 'movies')
	else: key, string_insert, path_insert = ('show', 'tvshow', 'shows')
	collected_at = 'listed_at' if list_type == 'watchlist' else 'collected_at' if media_type in ('movie', 'movies') else 'last_collected_at'
	premiered = 'released' if key == 'movie' else 'first_aired'
	string = 'trakt_%s_%s' % (list_type, string_insert)
	url = {'path': 'sync/%s/%s', 'path_insert': (list_type, path_insert), 'params': {'extended': 'full', 'limit': 250}, 'with_auth': True}
	data = trakt_cache.cache_trakt_object(trakt_fetch_all_pages, string, url)
	if data is None: return []
	if list_type == 'watchlist': data = [i for i in data if i and i.get('type') == key and i.get(key)]
	else: data = [i for i in data if i and i.get(key)]
	result = [
		{'media_ids': i[key]['ids'], 'title': i[key]['title'], 'collected_at': i.get(collected_at), 'premiered': i[key].get(premiered) or ''}
		for i in data
	]
	return result

def trakt_fetch_favorites(media_type):
	if not get_setting('trakt_user', ''): return []
	if media_type in ('movie', 'movies'): key, string_insert, premiered = ('movie', 'movie', 'released')
	else: key, string_insert, premiered = ('show', 'tvshow', 'first_aired')
	string = 'trakt_favorites_%s' % string_insert
	url = {'path': 'sync/favorites', 'params': {'extended': 'full', 'limit': 250}, 'with_auth': True}
	data = trakt_cache.cache_trakt_object(trakt_fetch_all_pages, string, url)
	if data is None: return []
	data = data or []
	try:
		# Si l'utilisateur a fait du ménage côté Trakt, on permet de nouveau la sauvegarde externe.
		if len(data) < 100: _clear_trakt_favourites_account_limit()
	except: pass
	data = [i for i in data if i.get('type') == key and i.get(key)]
	result = [
		{'media_ids': i[key]['ids'], 'title': i[key]['title'], 'collected_at': i.get('listed_at') or i.get('favorited_at') or '', 'favorited_at': i.get('favorited_at') or i.get('listed_at') or '', 'premiered': i[key].get(premiered) or ''}
		for i in data
	]
	return result


def trakt_prewarm_personal_cache(reason='manual'):
	# Après une purge/restauration, on reconstruit immédiatement les listes personnelles
	# les plus visibles afin d'éviter les watchlists vides jusqu'au prochain cycle service.
	if not get_setting('trakt_user', ''): return True
	logger('trakt cache rebuild', 'START | reason=%s' % (reason or 'manual'))
	ok = True
	for list_type in ('watchlist', 'collection'):
		for media_type in ('movie', 'tvshow'):
			try:
				trakt_fetch_collection_watchlist(list_type, media_type)
			except Exception as exc:
				ok = False
				logger('trakt cache rebuild', 'FAILED | %s/%s | %s: %s' % (list_type, media_type, type(exc).__name__, exc))
	for media_type in ('movie', 'tvshow'):
		try:
			trakt_fetch_favorites(media_type)
		except Exception as exc:
			ok = False
			logger('trakt cache rebuild', 'FAILED | favorites/%s | %s: %s' % (media_type, type(exc).__name__, exc))
	logger('trakt cache rebuild', 'FINISHED | reason=%s | success=%s' % (reason or 'manual', ok))
	return ok


def _trakt_valid_id(value):
	return not value in ('', 'None', None)

def _trakt_ids_from_params(params):
	ids = {}
	for key in ('imdb', 'tmdb', 'tvdb'):
		value = params.get('%s_id' % key)
		if not _trakt_valid_id(value): continue
		try: ids[key] = value if key == 'imdb' else int(value)
		except: ids[key] = value
	return ids

def _trakt_rating_payload(params, rating=None):
	media_type = params.get('media_type')
	ids = _trakt_ids_from_params(params)
	if not ids: return None, None
	if media_type == 'movie':
		item = {'ids': ids}
		if rating is not None: item['rating'] = int(rating)
		return {'movies': [item]}, 'movies'
	show = {'ids': ids}
	if media_type == 'episode':
		episode = {'number': int(params['episode'])}
		if rating is not None: episode['rating'] = int(rating)
		show['seasons'] = [{'number': int(params['season']), 'episodes': [episode]}]
		return {'shows': [show]}, 'episodes'
	if media_type == 'season':
		season = {'number': int(params['season'])}
		if rating is not None: season['rating'] = int(rating)
		show['seasons'] = [season]
		return {'shows': [show]}, 'seasons'
	if rating is not None: show['rating'] = int(rating)
	return {'shows': [show]}, 'shows'

def trakt_set_rating(params, rating):
	data, success_key = _trakt_rating_payload(params, rating)
	if not data: return False
	result = call_trakt('sync/ratings', data=data)
	try: success = bool(result) and result.get('added', {}).get(success_key, 0) + result.get('updated', {}).get(success_key, 0) > 0
	except: success = bool(result)
	if success: trakt_sync_activities()
	return success

def trakt_remove_rating(params):
	data, success_key = _trakt_rating_payload(params)
	if not data: return False
	result = call_trakt('sync/ratings/remove', data=data)
	try: success = bool(result) and result.get('deleted', {}).get(success_key, 0) + result.get('removed', {}).get(success_key, 0) > 0
	except: success = bool(result)
	if success: trakt_sync_activities()
	return success

def _ids_match(ids, params):
	ids = ids or {}
	for key in ('tmdb', 'imdb', 'tvdb'):
		value = params.get('%s_id' % key)
		if _trakt_valid_id(value) and str(ids.get(key)) == str(value): return True
	return False

def trakt_get_rating(params):
	media_type = params.get('media_type')
	if media_type == 'movie': trakt_type, key = 'movies', 'movie'
	elif media_type == 'episode': trakt_type, key = 'episodes', 'episode'
	elif media_type == 'season': trakt_type, key = 'seasons', 'season'
	else: trakt_type, key = 'shows', 'show'
	data = trakt_fetch_all_pages({'path': 'sync/ratings/%s', 'path_insert': trakt_type, 'params': {'limit': 250}, 'with_auth': True}) or []
	for item in data:
		try:
			if media_type == 'episode':
				episode = item.get('episode') or {}
				show = item.get('show') or episode.get('show') or {}
				if int(episode.get('season')) == int(params.get('season')) and int(episode.get('number')) == int(params.get('episode')) and _ids_match(show.get('ids'), params):
					return item.get('rating')
			elif media_type == 'season':
				season = item.get('season') or {}
				show = item.get('show') or season.get('show') or {}
				number = season.get('number', item.get('number'))
				if int(number) == int(params.get('season')) and _ids_match(show.get('ids'), params):
					return item.get('rating')
			elif _ids_match((item.get(key) or {}).get('ids'), params):
				return item.get('rating')
		except: pass
	return None


def _clear_trakt_lists_cache(slug=None):
	try: trakt_cache.clear_trakt_list_data('my_lists')
	except: pass
	try: trakt_cache.clear_trakt_list_contents_data('my_lists')
	except: pass
	if slug:
		try: trakt_cache.clear_trakt_list_contents_data(str(slug))
		except: pass


def _normal_alkoflix_favourites_media_type(media_type):
	media_type = str(media_type or '').strip().lower()
	if media_type in ('show', 'shows', 'tv', 'tvshow', 'series'): return 'tvshow'
	if media_type in ('season', 'seasons'): return 'season'
	if media_type in ('episode', 'episodes'): return 'episode'
	return 'movie'

def _alkoflix_favourites_list_name(media_type):
	media_type = _normal_alkoflix_favourites_media_type(media_type)
	return ALKOFIX_TRAKT_FAVOURITES_LISTS[media_type]

def trakt_get_or_create_alkoflix_favourites_list(media_type):
	"""Retourne la liste perso Trakt utilisée comme sauvegarde alkoFlix.

	Les favoris natifs Trakt restent utilisés seulement pour l'import initial.
	La synchronisation alkoFlix se fait dans deux listes personnelles afin
	d'éviter la limite native de 100 favoris.
	"""
	media_type = _normal_alkoflix_favourites_media_type(media_type)
	list_name = _alkoflix_favourites_list_name(media_type)
	try: lists = trakt_get_lists('my_lists') or []
	except: lists = []
	for item in lists:
		try:
			if _normal_list_name(item.get('name')) == _normal_list_name(list_name):
				ids = item.get('ids') or {}
				user_ids = (item.get('user') or {}).get('ids') or {}
				return {'name': item.get('name'), 'slug': ids.get('slug'), 'user': user_ids.get('slug') or 'me'}
		except: pass
	data = {'name': list_name, 'privacy': 'private', 'allow_comments': False}
	result = call_trakt('users/me/lists', data=data)
	_clear_trakt_lists_cache()
	try:
		ids = (result or {}).get('ids') or {}
		user_ids = ((result or {}).get('user') or {}).get('ids') or {}
		if ids.get('slug'):
			return {'name': list_name, 'slug': ids.get('slug'), 'user': user_ids.get('slug') or 'me'}
	except: pass
	# Fallback : relire les listes après création si Trakt ne renvoie pas le slug.
	try: lists = trakt_get_lists('my_lists') or []
	except: lists = []
	for item in lists:
		try:
			if _normal_list_name(item.get('name')) == _normal_list_name(list_name):
				ids = item.get('ids') or {}
				user_ids = (item.get('user') or {}).get('ids') or {}
				return {'name': item.get('name'), 'slug': ids.get('slug'), 'user': user_ids.get('slug') or 'me'}
		except: pass
	return None


def _show_has_season_or_episode(show):
	for season in show.get('seasons') or []:
		if season.get('episodes'): return 'episode'
		return 'season'
	return 'tvshow'

def _split_payload_by_type(data):
	payloads = []
	if data and data.get('movies'): payloads.append(('movie', {'movies': data.get('movies') or []}))
	shows_by_type = {'tvshow': [], 'season': [], 'episode': []}
	for show in (data or {}).get('shows') or []:
		shows_by_type[_show_has_season_or_episode(show)].append(show)
	for media_type in ('tvshow', 'season', 'episode'):
		if shows_by_type[media_type]: payloads.append((media_type, {'shows': shows_by_type[media_type]}))
	return payloads

def _trakt_list_modify(media_type, data, action=True):
	lst = trakt_get_or_create_alkoflix_favourites_list(media_type)
	if not lst or not lst.get('slug'):
		logger('trakt alkoFlix favourites list', 'missing list | media_type=%s' % media_type)
		return False
	path = 'users/%s/lists/%s/items%s' % (lst.get('user') or 'me', lst.get('slug'), '' if action else '/remove')
	result = call_trakt(path, data=data)
	if not isinstance(result, dict):
		logger('trakt alkoFlix favourites list', 'invalid response | action=%s | media_type=%s' % ('add' if action else 'remove', media_type))
		return False
	try:
		bucket = 'added' if action else 'deleted'
		existing = result.get('existing', {}) or {}
		counts = ('movies', 'shows', 'seasons', 'episodes')
		count = sum((result.get(bucket, {}) or {}).get(i, 0) for i in counts) + sum(existing.get(i, 0) for i in counts)
		success = count > 0
	except Exception:
		success = bool(result)
	if success:
		_clear_trakt_lists_cache(lst.get('slug'))
		trakt_sync_activities()
	return success


def trakt_add_to_alkoflix_favourites(data):
	"""Ajoute un film/série dans les listes perso alkoFlix de Trakt."""
	success = False
	for media_type, payload in _split_payload_by_type(data):
		success = _trakt_list_modify(media_type, payload, True) or success
	return success


def trakt_remove_from_alkoflix_favourites(data):
	"""Retire un film/série des listes perso alkoFlix de Trakt."""
	success = False
	for media_type, payload in _split_payload_by_type(data):
		success = _trakt_list_modify(media_type, payload, False) or success
	return success


def trakt_sync_alkoflix_favourites_lists(data):
	"""Synchronise les favoris locaux alkoFlix vers les listes perso Trakt.

	Cette opération n'importe rien : elle crée les listes au besoin et y ajoute
	les IDs locaux. L'utilisateur peut importer les favoris natifs Trakt avec
	l'action dédiée des réglages.
	"""
	return trakt_add_to_alkoflix_favourites(data)


def trakt_fetch_alkoflix_favourites_list(media_type):
	"""Lit les items de la liste perso Trakt alkoFlix pour un type donné."""
	media_type = _normal_alkoflix_favourites_media_type(media_type)
	if media_type == 'movie': key, premiered = 'movie', 'released'
	elif media_type == 'tvshow': key, premiered = 'show', 'first_aired'
	else: key, premiered = media_type, 'first_aired'
	lst = trakt_get_or_create_alkoflix_favourites_list(media_type)
	if not lst or not lst.get('slug'): return []
	try:
		url = {'path': 'users/%s/lists/%s/items', 'path_insert': (lst.get('user') or 'me', lst.get('slug')), 'params': {'extended': 'full', 'limit': 250}, 'with_auth': True}
		data = trakt_fetch_all_pages(url) or []
	except Exception as e:
		logger('trakt alkoFlix favourites list fetch', '%s | %s: %s' % (media_type, type(e).__name__, e))
		data = []
	result = []
	for i in data:
		try:
			if media_type in ('movie', 'tvshow'):
				if i.get('type') != key or not i.get(key): continue
				result.append({'media_ids': i[key].get('ids') or {}, 'title': i[key].get('title') or '', 'collected_at': i.get('listed_at') or '', 'favorited_at': i.get('listed_at') or '', 'premiered': i[key].get(premiered) or ''})
			elif media_type == 'season':
				season = i.get('season') or {}
				show = season.get('show') or i.get('show') or {}
				if not season: continue
				result.append({'media_ids': show.get('ids') or {}, 'title': '%s - Saison %s' % (show.get('title') or '', season.get('number')), 'season': season.get('number'), 'collected_at': i.get('listed_at') or '', 'favorited_at': i.get('listed_at') or ''})
			else:
				episode = i.get('episode') or {}
				show = episode.get('show') or i.get('show') or {}
				if not episode: continue
				result.append({'media_ids': show.get('ids') or {}, 'title': '%s - S%sE%s - %s' % (show.get('title') or '', episode.get('season'), episode.get('number'), episode.get('title') or ''), 'season': episode.get('season'), 'episode': episode.get('number'), 'collected_at': i.get('listed_at') or '', 'favorited_at': i.get('listed_at') or ''})
		except: pass
	return result

def add_to_list(user, slug, data):
	result = call_trakt('users/%s/lists/%s/items' % (user, slug), data=data)
	if result['added']['movies'] + result['added']['shows'] == 0: return kodi_utils.notification(32574)
	kodi_utils.notification(32576)
	trakt_sync_activities()
	return result

def remove_from_list(user, slug, data):
	result = call_trakt('users/%s/lists/%s/items/remove' % (user, slug), data=data)
	if result['deleted']['movies'] + result['deleted']['shows'] == 0: return kodi_utils.notification(32574)
	kodi_utils.notification(32576)
	trakt_sync_activities()
	kodi_utils.container_refresh()
	return result

def add_to_watchlist(data):
	result = call_trakt('sync/watchlist', data=data)
	if result['added']['movies'] + result['added']['shows'] == 0: return kodi_utils.notification(32574)
	kodi_utils.notification(32576)
	trakt_sync_activities()
	return result

def remove_from_watchlist(data):
	result = call_trakt('sync/watchlist/remove', data=data)
	if result['deleted']['movies'] + result['deleted']['shows'] == 0: return kodi_utils.notification(32574)
	kodi_utils.notification(32576)
	trakt_sync_activities()
	kodi_utils.container_refresh()
	return result

def add_to_favorites(data):
	result = call_trakt('sync/favorites', data=data)
	if isinstance(result, dict) and result.get('account_limit'):
		return False
	if isinstance(result, dict) and result.get('rate_limited'):
		return False
	if not isinstance(result, dict):
		logger('trakt favorites sync skipped', 'add | empty_or_invalid_response')
		return False
	try: success = result.get('added', {}).get('movies', 0) + result.get('added', {}).get('shows', 0) > 0
	except Exception as e:
		logger('trakt favorites sync skipped', 'add | %s: %s' % (type(e).__name__, e))
		success = False
	if not success: return False
	_clear_trakt_favourites_account_limit()
	trakt_cache.clear_trakt_collection_watchlist_data('favorites', 'movie')
	trakt_cache.clear_trakt_collection_watchlist_data('favorites', 'tvshow')
	trakt_sync_activities()
	return result

def remove_from_favorites(data):
	result = call_trakt('sync/favorites/remove', data=data)
	if isinstance(result, dict) and result.get('rate_limited'):
		return False
	if not isinstance(result, dict):
		logger('trakt favorites sync skipped', 'remove | empty_or_invalid_response')
		return False
	try: success = result.get('deleted', {}).get('movies', 0) + result.get('deleted', {}).get('shows', 0) > 0
	except Exception as e:
		logger('trakt favorites sync skipped', 'remove | %s: %s' % (type(e).__name__, e))
		success = False
	if not success: return False
	_clear_trakt_favourites_account_limit()
	trakt_cache.clear_trakt_collection_watchlist_data('favorites', 'movie')
	trakt_cache.clear_trakt_collection_watchlist_data('favorites', 'tvshow')
	trakt_sync_activities()
	kodi_utils.container_refresh()
	return result

def add_to_collection(data):
	result = call_trakt('sync/collection', data=data)
	if result['added']['movies'] + result['added']['episodes'] == 0: return kodi_utils.notification(32574)
	kodi_utils.notification(32576)
	trakt_sync_activities()
	return result


def _trakt_valid_id(value):
	try: value = str(value).strip()
	except: return False
	return value not in ('', 'None', 'none', 'null', '0')

def _trakt_cast_id(value, key):
	if key in ('tmdb', 'tvdb'):
		try: return int(value)
		except: return value
	return value

def _trakt_show_id_candidates(imdb_id, tvdb_id, tmdb_id):
	# Conserve l'ordre historique, puis tente les autres identifiants si Trakt ne
	# reconnaît pas une série récente avec le premier ID disponible.
	items = ((imdb_id, 'imdb'), (tvdb_id, 'tvdb'), (tmdb_id, 'tmdb'))
	candidates, seen = [], set()
	for value, key in items:
		if not _trakt_valid_id(value): continue
		value = _trakt_cast_id(value, key)
		marker = '%s:%s' % (key, value)
		if marker in seen: continue
		seen.add(marker)
		candidates.append((value, key))
	return candidates

def _trakt_sync_count(result, action_key, media_key='shows'):
	try: return int(result.get(action_key, {}).get(media_key, 0))
	except: return 0

def _trakt_call_with_show_id_fallback(path, action_key, tmdb_id, tvdb_id, imdb_id, user=None, slug=None, remove=False):
	last_result = None
	for media_id, media_key in _trakt_show_id_candidates(imdb_id, tvdb_id, tmdb_id):
		data = {'shows': [{'ids': {media_key: media_id}}]}
		if user and slug:
			url = 'users/%s/lists/%s/items%s' % (user, slug, '/remove' if remove else '')
		else:
			url = path
		result = call_trakt(url, data=data)
		last_result = result
		if _trakt_sync_count(result, action_key, 'shows') > 0:
			logger('trakt show id fallback', 'success | path=%s | key=%s | id=%s' % (url, media_key, media_id))
			return result
		logger('trakt show id fallback', 'no match | path=%s | key=%s | id=%s' % (url, media_key, media_id))
	return last_result

def remove_from_collection(data):
	result = call_trakt('sync/collection/remove', data=data)
	if result['deleted']['movies'] + result['deleted']['episodes'] == 0: return kodi_utils.notification(32574)
	kodi_utils.notification(32576)
	trakt_sync_activities()
	kodi_utils.container_refresh()
	return result

def hide_unhide_trakt_items(action, media_type, media_id, list_type):
	if not action in ('hide', 'unhide'):
		try:
			hidden_data = set(map(str, trakt_get_hidden_items('dropped')))
			action = 'unhide' if action in hidden_data else 'hide'
		except: return kodi_utils.notification(32574)
	media_type = 'movies' if media_type in ['movie', 'movies'] else 'shows'
	key = 'tmdb' if media_type == 'movies' else 'imdb'
	url = 'users/hidden/%s' % list_type if action == 'hide' else 'users/hidden/%s/remove' % list_type
	data = {media_type: [{'ids': {key: media_id}}]}
	call_trakt(url, data=data)
	trakt_sync_activities()
	kodi_utils.container_refresh()

def trakt_search_lists(search_title, page):
#	lists, pages = call_trakt('search', params={'type': 'list', 'fields': 'name, description', 'query': search_title, 'limit': 50}, pagination=True, page=page)
	lists, pages = call_trakt('search/list', params={'query': search_title, 'limit': 50}, pagination=True, page=page)
	return lists, pages

def get_trakt_list_contents(list_type, list_id, user, slug):
	string = 'trakt_list_contents_%s_%s_%s' % (list_type, user, slug)
#	url = {'path': 'users/%s/lists/%s/items', 'path_insert': (user, slug), 'params': {'extended':'full'}, 'with_auth': True} # , 'method': 'sort_by_headers'}
	url = {'path': 'users/%s/lists/%s/items', 'path_insert': (user, list_id), 'with_auth': True}
	return trakt_cache.cache_trakt_object(get_trakt, string, url)

def trakt_trending_popular_lists(list_type):
	string = 'trakt_%s_user_lists' % list_type
	url = {'path': 'lists/%s', 'path_insert': list_type, 'params': {'limit': 100}}
	return cache_object(get_trakt, string, url, json=False)

def trakt_get_lists(list_type):
	if list_type in ('my_lists', 'liked_lists') and not get_setting('trakt_user', ''): return []
	if list_type == 'my_lists':
		string = 'trakt_my_lists'
		path = 'users/me/lists%s'
	elif list_type == 'liked_lists':
		string = 'trakt_liked_lists'
		path = 'users/likes/lists%s'
	url = {'path': path, 'params': {'limit': 1000}, 'with_auth': True, 'pagination': False}
	return trakt_cache.cache_trakt_object(get_trakt, string, url)

def get_trakt_list_selection(list_choice=None, highlight=None):
	my_lists = [
		{'name': item['name'], 'display': ls(32778) % item['name'].upper(), 'user': item['user']['ids']['slug'], 'slug': item['ids']['slug']}
		for item in trakt_get_lists('my_lists')
	]
	my_lists.sort(key=lambda k: k['name'])
	if list_choice == 'nav_edit':
		liked_lists = [
			{'name': item['list']['name'],
			 'display': ls(32779) % item['list']['name'].upper(),
			 'user': item['list']['user']['ids']['slug'],
			 'slug': item['list']['ids']['slug']}
			for item in trakt_get_lists('liked_lists')
		]
		liked_lists.sort(key=lambda k: (k['display']))
		my_lists.extend(liked_lists)
#	else:
#		my_lists.insert(0, {'name': 'Collection', 'display': '[B]%s [/B]' % ls(32499).upper(), 'user': 'Collection', 'slug': 'Collection'})
#		my_lists.insert(0, {'name': 'Watchlist', 'display': '[B]%s [/B]' % ls(32500).upper(),  'user': 'Watchlist', 'slug': 'Watchlist'})
	if not my_lists: return kodi_utils.notification(32760)
	icon = kodi_utils.media_path('trakt.png')
	list_items = [{'line1': item['display'], 'icon': icon} for item in my_lists]
	kwargs = {'items': json.dumps(list_items), 'heading': 'Select list', 'enumerate': 'false', 'multi_choice': 'false', 'multi_line': 'false'}
	if highlight: kwargs['highlight'] = highlight
	selection = kodi_utils.select_dialog(my_lists, **kwargs)
	if selection is None: return None
	return selection

def make_new_trakt_list(params):
	from urllib.parse import unquote
	mode = params['mode']
	list_title = kodi_utils.dialog.input('alkoFlix')
	if not list_title: return
	list_name = unquote(list_title)
	data = {'name': list_name, 'privacy': 'private', 'allow_comments': False}
	call_trakt('users/me/lists', data=data)
	trakt_sync_activities()
	kodi_utils.notification(32576)
	kodi_utils.container_refresh()

def delete_trakt_list(params):
	user = params['user']
	list_slug = params['list_slug']
	if not kodi_utils.confirm_dialog(): return
	url = 'users/%s/lists/%s' % (user, list_slug)
	call_trakt(url, method='delete')
	trakt_sync_activities()
	kodi_utils.notification(32576)
	kodi_utils.container_refresh()

def trakt_add_to_list(params):
	tmdb_id, tvdb_id, imdb_id, media_type = params['tmdb_id'], params['tvdb_id'], params['imdb_id'], params['media_type']
	if media_type == 'movie':
		key, media_key, media_id = ('movies', 'tmdb', int(tmdb_id))
	else:
		key = 'shows'
		media_ids = [(imdb_id, 'imdb'), (tvdb_id, 'tvdb'), (tmdb_id, 'tmdb')]
		media_id, media_key = next(item for item in media_ids if item[0] != 'None')
		if media_id in (tmdb_id, tvdb_id): media_id = int(media_id)
	selected = get_trakt_list_selection(highlight=params.get('highlight'))
	if selected is None: return
	if media_type != 'movie':
		if selected['user'] == 'Watchlist':
			result = _trakt_call_with_show_id_fallback('sync/watchlist', 'added', tmdb_id, tvdb_id, imdb_id)
			if _trakt_sync_count(result, 'added', 'shows') == 0: return kodi_utils.notification(32574)
			kodi_utils.notification(32576)
			trakt_sync_activities()
			return result
		elif selected['user'] not in ('Collection', 'Favorites'):
			result = _trakt_call_with_show_id_fallback('', 'added', tmdb_id, tvdb_id, imdb_id, selected['user'], selected['slug'])
			if _trakt_sync_count(result, 'added', 'shows') == 0: return kodi_utils.notification(32574)
			kodi_utils.notification(32576)
			trakt_sync_activities()
			return result
	data = {key: [{'ids': {media_key: media_id}}]}
	if selected['user'] == 'Watchlist': add_to_watchlist(data)
	elif selected['user'] == 'Collection': add_to_collection(data)
	elif selected['user'] == 'Favorites': add_to_favorites(data)
	else: add_to_list(selected['user'], selected['slug'], data)

def trakt_remove_from_list(params):
	tmdb_id, tvdb_id, imdb_id, media_type = params['tmdb_id'], params['tvdb_id'], params['imdb_id'], params['media_type']
	if media_type == 'movie':
		key, media_key, media_id = ('movies', 'tmdb', int(tmdb_id))
	else:
		key = 'shows'
		media_ids = [(imdb_id, 'imdb'), (tvdb_id, 'tvdb'), (tmdb_id, 'tmdb')]
		media_id, media_key = next(item for item in media_ids if item[0] != 'None')
		if media_id in (tmdb_id, tvdb_id): media_id = int(media_id)
	selected = get_trakt_list_selection(highlight=params.get('highlight'))
	if selected is None: return
	if media_type != 'movie':
		if selected['user'] == 'Watchlist':
			result = _trakt_call_with_show_id_fallback('sync/watchlist/remove', 'deleted', tmdb_id, tvdb_id, imdb_id, remove=True)
			if _trakt_sync_count(result, 'deleted', 'shows') == 0: return kodi_utils.notification(32574)
			kodi_utils.notification(32576)
			trakt_sync_activities()
			kodi_utils.container_refresh()
			return result
		elif selected['user'] not in ('Collection', 'Favorites'):
			result = _trakt_call_with_show_id_fallback('', 'deleted', tmdb_id, tvdb_id, imdb_id, selected['user'], selected['slug'], remove=True)
			if _trakt_sync_count(result, 'deleted', 'shows') == 0: return kodi_utils.notification(32574)
			kodi_utils.notification(32576)
			trakt_sync_activities()
			kodi_utils.container_refresh()
			return result
	data = {key: [{'ids': {media_key: media_id}}]}
	if selected['user'] == 'Watchlist': remove_from_watchlist(data)
	elif selected['user'] == 'Collection': remove_from_collection(data)
	elif selected['user'] == 'Favorites': remove_from_favorites(data)
	else: remove_from_list(selected['user'], selected['slug'], data)

def trakt_like_a_list(params):
	user = params['user']
	list_slug = params['list_slug']
	try:
		call_trakt('users/%s/lists/%s/like' % (user, list_slug), method='post')
		kodi_utils.notification(32576)
		trakt_sync_activities()
	except: kodi_utils.notification(32574)

def trakt_unlike_a_list(params):
	user = params['user']
	list_slug = params['list_slug']
	try:
		call_trakt('users/%s/lists/%s/like' % (user, list_slug), method='delete')
		kodi_utils.notification(32576)
		trakt_sync_activities()
		kodi_utils.container_refresh()
	except: kodi_utils.notification(32574)

def get_trakt_movie_id(item):
	if item['tmdb']: return item['tmdb']
	tmdb_id = None
	if item['imdb']:
		try:
			meta = movie_external_id('imdb_id', item['imdb'])
			tmdb_id = meta['id']
		except: pass
	return tmdb_id

def get_trakt_tvshow_id(item):
	if item['tmdb']: return item['tmdb']
	tmdb_id = None
	if item['imdb']:
		try:
			meta = tvshow_external_id('imdb_id', item['imdb'])
			tmdb_id = meta['id']
		except: tmdb_id = None
	if not tmdb_id:
		if item['tvdb']:
			try:
				meta = tvshow_external_id('tvdb_id', item['tvdb'])
				tmdb_id = meta['id']
			except: tmdb_id = None
	return tmdb_id

def trakt_indicators_movies():
	def _process(item):
		movie = item['movie']
		title = movie['title']
		tmdb_id = get_trakt_movie_id(movie['ids'])
		if not tmdb_id: return
		insert_append(('movie', tmdb_id, '', '', item['last_watched_at'], title))
	insert_list = []
	insert_append = insert_list.append
	result = [(i,) for i in call_trakt('sync/watched/movies')] # TaskPool requires tuple
#	threads = list(make_thread_list(_process, result, Thread))
	for i in TaskPool().tasks(_process, result, Thread): i.join()
	trakt_cache.TraktCache().set_bulk_movie_watched(insert_list)

def trakt_indicators_tv():
	def _process(item):
		show, seasons = item['show'], item['seasons']
		title = show['title']
		tmdb_id = get_trakt_tvshow_id(show['ids'])
		if not tmdb_id: return
		reset_at = item.get('reset_at')
		if reset_at: reset_at = js2date(reset_at, '%Y-%m-%dT%H:%M:%S.%fZ')
		for s in seasons:
			season_no, episodes = s['number'], s['episodes']
			for e in episodes:
				if reset_at and reset_at > js2date(e['last_watched_at'], '%Y-%m-%dT%H:%M:%S.%fZ'): continue
				insert_append(('episode', tmdb_id, season_no, e['number'], e['last_watched_at'], title))
	insert_list = []
	insert_append = insert_list.append
	result = [(i,) for i in call_trakt('sync/watched/shows')] # TaskPool requires tuple
#	threads = list(make_thread_list(_process, result, Thread))
	for i in TaskPool().tasks(_process, result, Thread): i.join()
	trakt_cache.TraktCache().set_bulk_tvshow_watched(insert_list)

def trakt_playback_progress():
	url = {'path': 'sync/playback%s', 'with_auth': True, 'pagination': False}
	return get_trakt(url)

def trakt_progress_movies(progress_info):
	def _process(item):
		tmdb_id = get_trakt_movie_id(item['movie']['ids'])
		if not tmdb_id: return
		insert_append((
			'movie', str(tmdb_id), '', '', str(round(item['progress'], 1)),
			0, item['paused_at'], item['id'], item['movie']['title']
		))
	insert_list = []
	insert_append = insert_list.append
	progress_items = [i for i in progress_info if i['type'] == 'movie' and i['progress'] > 1]
	if progress_items:
		threads = list(make_thread_list(_process, progress_items, Thread))
		[i.join() for i in threads]
	trakt_cache.TraktCache().set_bulk_movie_progress(insert_list)

def trakt_progress_tv(progress_info):
	def _process_tmdb_ids(item):
		tmdb_id = get_trakt_tvshow_id(item['ids'])
		tmdb_list_append((tmdb_id, item['title']))
	def _process():
		for item in tmdb_list:
			try:
				tmdb_id, title = item[0], item[1]
				if not tmdb_id: continue
				for p_item in progress_items:
					if not p_item['show']['title'] == title: continue
					season, episode = p_item['episode']['season'], p_item['episode']['number']
					if season > 0: yield (
						'episode', str(tmdb_id), season, episode, str(round(p_item['progress'], 1)),
						0, p_item['paused_at'], p_item['id'], p_item['show']['title']
					)
			except: pass
	tmdb_list = []
	tmdb_list_append = tmdb_list.append
	progress_items = [i for i in progress_info if i['type'] == 'episode' and i['progress'] > 1]
	if progress_items:
		all_shows = [i['show'] for i in progress_items]
		all_shows = [i for n, i in enumerate(all_shows) if i not in all_shows[n + 1:]] # remove duplicates
		threads = list(make_thread_list(_process_tmdb_ids, all_shows, Thread))
		[i.join() for i in threads]
	insert_list = list(_process())
	trakt_cache.TraktCache().set_bulk_tvshow_progress(insert_list)

def trakt_official_status(media_type):
	if not kodi_utils.addon_installed('script.trakt'): return True
	trakt_addon = kodi_utils.addon('script.trakt')
	try: authorization = trakt_addon.getSetting('authorization')
	except: authorization = ''
	if authorization == '': return True
	try: exclude_http = trakt_addon.getSetting('ExcludeHTTP')
	except: exclude_http = ''
	if exclude_http in ('true', ''): return True
	media_setting = 'scrobble_movie' if media_type in ('movie', 'movies') else 'scrobble_episode'
	try: scrobble = trakt_addon.getSetting(media_setting)
	except: scrobble = ''
	if scrobble in ('false', ''): return True
	return False

def trakt_get_my_calendar(recently_aired, current_date):
	def _process(dummy):
		data = get_trakt(url)
		data = [
			{'sort_title': '%s s%s e%s' % (i['show']['title'], str(i['episode']['season']).zfill(2), str(i['episode']['number']).zfill(2)),
			'media_ids': i['show']['ids'], 'season': i['episode']['season'], 'episode': i['episode']['number'], 'first_aired': i['first_aired']}
			for i in data
			if i['episode']['season'] > 0 and not 'anime' in i['show']['genres']
		]
		data = [i for n, i in enumerate(data) if i not in data[n + 1:]] # remove duplicates
		return data
	start, finish = trakt_calendar_days(recently_aired, current_date)
	string = 'trakt_get_my_calendar_%s_%s' % (start, finish)
	url = {'path': 'calendars/my/shows/%s/%s', 'path_insert': (start, finish), 'params': {'extended': 'full'}, 'with_auth': True, 'pagination': False}
	return trakt_cache.cache_trakt_object(_process, string, url)

def trakt_my_anime_calendar(current_date):
	def _process(dummy):
		data = get_trakt(url)
		data = [
			{'sort_title': '%s s%s e%s' % (i['show']['title'], str(i['episode']['season']).zfill(2), str(i['episode']['number']).zfill(2)),
			'media_ids': i['show']['ids'], 'season': i['episode']['season'], 'episode': i['episode']['number'], 'first_aired': i['first_aired']}
			for i in data
			if i['episode']['season'] > 0
		]
		data = [i for n, i in enumerate(data) if i not in data[n + 1:]] # remove duplicates
		return data
	start, finish = trakt_calendar_days(False, current_date)
	string = 'trakt_my_anime_calendar_%s_%s' % (start, finish)
	url = {'path': 'calendars/my/shows/%s/%s', 'path_insert': (start, finish), 'params': {'genres': 'anime'}, 'with_auth': True, 'pagination': False}
	return trakt_cache.cache_trakt_object(_process, string, url)

def trakt_anime_calendar(current_date):
	def _process(dummy):
		data = get_trakt(url)
		data = [
			{'sort_title': '%s s%s e%s' % (i['show']['title'], str(i['episode']['season']).zfill(2), str(i['episode']['number']).zfill(2)),
			'media_ids': i['show']['ids'], 'season': i['episode']['season'], 'episode': i['episode']['number'], 'first_aired': i['first_aired']}
			for i in data
			if i['episode']['season'] > 0
		]
		data = [i for n, i in enumerate(data) if i not in data[n + 1:]] # remove duplicates
		return data
	start, finish = trakt_calendar_days(False, current_date)
	string = 'trakt_anime_calendar_%s_%s' % (start, finish)
	url = {'path': 'calendars/all/shows/%s/%s', 'path_insert': (start, finish), 'params': {'genres': 'anime'}, 'with_auth': True, 'pagination': False}
	return trakt_cache.cache_trakt_object(_process, string, url)

def trakt_calendar_days(recently_aired, current_date):
	from datetime import timedelta
	if recently_aired: start, finish = (current_date - timedelta(days=7)).strftime('%Y-%m-%d'), '7'
	else:
		previous_days = int(get_setting('trakt.calendar_previous_days', '3'))
		future_days = int(get_setting('trakt.calendar_future_days', '7'))
		start = (current_date - timedelta(days=previous_days)).strftime('%Y-%m-%d')
		finish = str(previous_days + future_days)
	return start, finish

def trakt_get_activity():
	url = {'path': 'sync/last_activities%s', 'with_auth': True, 'pagination': False}
	return get_trakt(url)

def trakt_sync_activities_thread(*args, **kwargs):
	Thread(target=trakt_sync_activities, args=args, kwargs=kwargs).start()

@trakt_expires
def trakt_sync_activities(force_update=False):
	def _get_timestamp(date_time):
		return int(time.mktime(date_time.timetuple()))
	def _compare(latest, cached, res_format='%Y-%m-%dT%H:%M:%S.%fZ'):
		try: result = _get_timestamp(js2date(latest, res_format)) > _get_timestamp(js2date(cached, res_format))
		except: result = True
		return result
	if not get_setting('trakt_user', ''): return 'no account'
	if force_update:
		check_databases()
		trakt_cache.clear_all_trakt_cache_data(refresh=False)
	trakt_cache.clear_trakt_calendar()
	latest = trakt_get_activity()
	if not latest:
		# Ne pas vider un cache sain sur un timeout temporaire Trakt.
		# Le prochain cycle retentera la synchro sans laisser les listes vides.
		return 'failed'
	cached = trakt_cache.reset_activity(latest)
	if not _compare(latest['all'], cached['all']):
		trakt_cache.clear_trakt_list_contents_data('liked_lists')
		return 'not needed'
	clear_list_contents, lists_actions = False, []
	refresh_movies_progress, refresh_shows_progress = False, False
	cached_movies, latest_movies = cached['movies'], latest['movies']
	cached_shows, latest_shows = cached['shows'], latest['shows']
	cached_episodes, latest_episodes = cached['episodes'], latest['episodes']
	cached_lists, latest_lists = cached['lists'], latest['lists']
	if _compare(latest_movies['collected_at'], cached_movies['collected_at']): trakt_cache.clear_trakt_collection_watchlist_data('collection', 'movie')
	if _compare(latest_episodes['collected_at'], cached_episodes['collected_at']): trakt_cache.clear_trakt_collection_watchlist_data('collection', 'tvshow')
	if _compare(latest_movies['watchlisted_at'], cached_movies['watchlisted_at']): trakt_cache.clear_trakt_collection_watchlist_data('watchlist', 'movie')
	if _compare(latest_shows['watchlisted_at'], cached_shows['watchlisted_at']): trakt_cache.clear_trakt_collection_watchlist_data('watchlist', 'tvshow')
	if _compare(latest_movies['rated_at'], cached_movies['rated_at']) or _compare(latest_shows['rated_at'], cached_shows['rated_at']) or _compare(latest_episodes['rated_at'], cached_episodes['rated_at']) or _compare((latest.get('seasons') or {}).get('rated_at'), (cached.get('seasons') or {}).get('rated_at')):
		try:
			from modules import user_ratings
			user_ratings.clear_user_ratings_cache(0)
		except: pass
	if 'favorited_at' in latest_movies and _compare(latest_movies['favorited_at'], cached_movies.get('favorited_at')): trakt_cache.clear_trakt_collection_watchlist_data('favorites', 'movie')
	if 'favorited_at' in latest_shows and _compare(latest_shows['favorited_at'], cached_shows.get('favorited_at')): trakt_cache.clear_trakt_collection_watchlist_data('favorites', 'tvshow')
	if _compare(latest_shows['dropped_at'], cached_shows['dropped_at']): trakt_cache.clear_trakt_hidden_data('dropped')
	if _compare(latest_movies['recommendations_at'], cached_movies['recommendations_at']): trakt_cache.clear_trakt_recommendations('movies')
	if _compare(latest_shows['recommendations_at'], cached_shows['recommendations_at']): trakt_cache.clear_trakt_recommendations('shows')
	if _compare(latest_movies['watched_at'], cached_movies['watched_at']): trakt_indicators_movies()
	if _compare(latest_episodes['watched_at'], cached_episodes['watched_at']): trakt_indicators_tv()
	if _compare(latest_movies['paused_at'], cached_movies['paused_at']): refresh_movies_progress = True
	if _compare(latest_episodes['paused_at'], cached_episodes['paused_at']): refresh_shows_progress = True
	if _compare(latest_lists['updated_at'], cached_lists['updated_at']):
		clear_list_contents = True
		lists_actions.append('my_lists')
	if _compare(latest_lists['liked_at'], cached_lists['liked_at']):
		clear_list_contents = True
		lists_actions.append('liked_lists')
	if refresh_movies_progress or refresh_shows_progress:
		progress_info = trakt_playback_progress()
		if refresh_movies_progress: trakt_progress_movies(progress_info)
		if refresh_shows_progress: trakt_progress_tv(progress_info)
	if clear_list_contents:
		for item in lists_actions:
			trakt_cache.clear_trakt_list_data(item)
			trakt_cache.clear_trakt_list_contents_data(item)
	else: trakt_cache.clear_trakt_list_contents_data('liked_lists')
	return 'success'

