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

import base64
import gzip
import hashlib
import json
import re
import sqlite3
import sys
import time
import uuid
from datetime import datetime

import xbmcvfs

from modules import kodi_utils
from modules.alkopastes_api import has_user_api_key, _request, _paste_code, _mask_code

PROFILE_DIR = 'special://profile/addon_data/plugin.video.alkoflix/'
BACKUP_SETTING_LOCAL = 'alkopastes.userdata_backup_code'
BACKUP_SETTING_MULTI = 'alkopastes.userdata_multi_backup_code'
BACKUP_MODE_SETTING = 'alkopastes.userdata_backup_mode'
INSTALLATION_ROLE_SETTING = 'alkopastes.userdata_installation_role'
BACKUP_MULTI_DEVICE_SETTING = 'alkopastes.userdata_multi_device'
INSTALLATION_MASTER_SETTING = 'alkopastes.userdata_master_installation'
INSTALLATION_SECONDARY_SETTING = 'alkopastes.userdata_secondary_installation_v2'
USERDATA_AUTO_RESTORE_SETTING = 'alkopastes.userdata_auto_restore'
USERDATA_AUTO_RESTORE_DUE_SETTING = 'alkopastes.userdata_auto_restore_due'
USERDATA_AUTO_RESTORE_LAST_CREATED_SETTING = 'alkopastes.userdata_auto_restore_last_created'
USERDATA_MANUAL_ONLY_DEFAULTS = {
	'alkopastes.userdata_auto_backup': 'false',
	'alkopastes.userdata_auto_backup_interval': '0',
	'alkopastes.userdata_auto_backup_due': '0',
	USERDATA_AUTO_RESTORE_SETTING: 'false',
	USERDATA_AUTO_RESTORE_DUE_SETTING: '0',
	USERDATA_AUTO_RESTORE_LAST_CREATED_SETTING: ''
}
DEVICE_ID_SETTING = 'alkopastes.userdata_device_id'
MASTER_DEVICE_HASH_SETTING = 'alkopastes.userdata_master_device_hash'
BACKUP_MODE_LOCAL = '0'
BACKUP_MODE_MULTI = '1'
INSTALLATION_ROLE_SECONDARY = '0'
INSTALLATION_ROLE_MASTER = '1'
BACKUP_TYPE = 'alkoflix_userdata_backup'
BACKUP_VERSION = 2

# La sauvegarde/restauration alkoFlix est toujours portable.
# On conserve uniquement les données utiles entre appareils et on exclut les caches techniques.
PORTABLE_EXACT = ('settings.xml',)
WATCHED_DATABASE = 'watched.db'
PORTABLE_DATABASES = (
	'favourites.db',      # Favoris locaux alkoFlix
	'navigator.db',       # Menus, dossiers/widgets personnalisés, icônes personnalisées
	WATCHED_DATABASE      # Statuts locaux de visionnement / reprise alkoFlix (mode local seulement)
)
NON_PORTABLE_DATABASES = (
	'maincache.db',
	'metacache.db',
	'debridcache.db',
	'providerscache.db',
	'traktcache4.db',
	'mdblcache.db',
	'views.db',
	'alkocache.db'
)
EXCLUDED_SUFFIX = ('-journal', '-wal', '-shm')

# Réglages propres à une autorisation de compte/appareil.
# Ils sont retirés de la sauvegarde portable et neutralisés lors d'une restauration
# afin de ne jamais cloner de tokens ou autorisations de compte entre deux profils Kodi.
AUTH_SETTING_DEFAULTS = {
	# Trakt / indicateurs de visionnage
	'trakt_user': '',
	'trakt.token': '',
	'trakt.refresh': '',
	'trakt.expires': '',
	'trakt_indicators_active': 'false',
	'watched_indicators': '0',

	# MDBList
	'mdblist_user': '',
	'mdblist.token': '',
	'mdblist.auth_type': '',
	'mdblist.access_token': '',
	'mdblist.refresh_token': '',
	'mdblist.expires': '',
	'mdblist.client_id': 'EUk8hb6sCGab70Z08k9EKMv1kahOh311Xxk4fDrj',
	'mdbl_indicators_active': 'false',

	# TMDb List / session TMDb utilisateur
	'tmdb.username': '',
	'tmdb.token': '',
	'tmdb.account_id': '',
	'tmdb.session_id': '',
	'tmdb.session_account_id': '',

	# alkoPastes : la connexion API et les codes de sauvegarde/favoris sont portables,
	# car ils servent précisément à partager les sauvegardes et la synchronisation entre appareils.
	# On garde seulement les choix de rôle, minuteries et empreintes techniques propres à l'appareil.
	'alkopastes.userdata_backup_mode': BACKUP_MODE_LOCAL,
	'alkopastes.userdata_installation_role': INSTALLATION_ROLE_SECONDARY,
	'alkopastes.userdata_multi_device': 'false',
	'alkopastes.userdata_master_installation': 'false',
	'alkopastes.userdata_secondary_installation': 'false',
	'alkopastes.userdata_secondary_installation_v2': 'false',
	'alkopastes.userdata_auto_backup': 'false',
	'alkopastes.userdata_auto_backup_interval': '0',
	'alkopastes.userdata_auto_backup_due': '0',
	'alkopastes.userdata_auto_restore': 'false',
	'alkopastes.userdata_auto_restore_due': '0',
	'alkopastes.userdata_auto_restore_last_created': '',
	'alkopastes.favourites_secondary_lock': 'false',
	'alkopastes.favourites_auto_restore_due': '0',
	'alkopastes.favourites_auto_restore_last_hash': '',
	'alkopastes.favourites_auto_restore_backoff_count': '0',
	'alkopastes.favourites_sync_pending': 'false',
	'alkopastes.favourites_sync_pending_at': '0',
	'alkopastes.favourites_sync_last_flush': '0',
	'alkopastes.favourites_cleanup_last': '0',
	'alkopastes.userdata_device_id': '',
	'alkopastes.userdata_master_device_hash': '',
	'alkopastes.favourites_device_hash': '',

	# Contrôle parental : le PIN est portable, mais l'état déverrouillé ne l'est pas.
	'parental.settings_unlocked': 'false',
	'parental.pin_backup_code': '',

	# Débrideurs / services de liens
	'ad.account_id': '',
	'ad.token': '',
	'rd.username': '',
	'rd.client_id': '',
	'rd.secret': '',
	'rd.refresh': '',
	'rd.token': '',
	'tb.account_id': '',
	'tb.token': '',
	'easynews_user': '',
	'easynews_password': '',
	'provider.easynews': 'false',

	# Les clés API personnelles facultatives (Fanart.tv, TMDb, RPDb, Wyzie, etc.)
	# sont volontairement portables : elles font partie de la configuration alkoFlix
	# et peuvent être restaurées d'un appareil à l'autre.
}

# Lors d'une restauration, les réglages neutralisés dans la sauvegarde
# reprennent la valeur locale déjà présente sur l'appareil cible.
# Cela restaure les données portables sans cloner les comptes externes,
# ni l'identité technique ou le rôle maître/secondaire de l'appareil.
LOCAL_SETTINGS_TO_PRESERVE = tuple(AUTH_SETTING_DEFAULTS.keys())

# Compatibilité WIP : ces réglages étaient auparavant stockés comme des sélecteurs 0/1.
# Les settings Kodi booléens doivent recevoir false/true, sinon Kodi refuse de relire
# la valeur restaurée depuis une sauvegarde plus ancienne.
LEGACY_BOOL_SETTING_VALUES = {
	'media.click_action': {'0': 'false', '1': 'true'},
	'info_extra.window': {'0': 'false', '1': 'true'}
}


def _request_userdata(method, action, payload=None, params=None, timeout=60.0):
	return _request(method, action, payload=payload, params=params, timeout=timeout, log_name='alkoPastes userdata request')


def _setting_true(setting_id):
	try: return str(kodi_utils.get_setting(setting_id, 'false')).strip().lower() == 'true'
	except Exception: return False


def _master_selected():
	return _setting_true(INSTALLATION_MASTER_SETTING)


def _secondary_selected():
	return _setting_true(INSTALLATION_SECONDARY_SETTING)


def _role_selected():
	return _master_selected() or _secondary_selected()


def _backup_mode():
	# Réglage 2026 : le choix visible se fait entre installation maître et secondaire.
	# L'ancien interrupteur interne reste lu seulement pour compatibilité.
	if _role_selected() or _setting_true(BACKUP_MULTI_DEVICE_SETTING): return BACKUP_MODE_MULTI
	return BACKUP_MODE_LOCAL


def _installation_role():
	# Sécurité : si les deux rôles sont cochés par un ancien réglage cassé, on traite
	# l'installation comme secondaire tant que le choix n'est pas réinitialisé.
	if _master_selected() and not _secondary_selected(): return INSTALLATION_ROLE_MASTER
	return INSTALLATION_ROLE_SECONDARY


def _is_multi_device_mode():
	return _backup_mode() == BACKUP_MODE_MULTI


def _is_master_installation():
	return _installation_role() == INSTALLATION_ROLE_MASTER

def _active_backup_setting():
	return BACKUP_SETTING_MULTI if _is_multi_device_mode() else BACKUP_SETTING_LOCAL


def _active_backup_label():
	if _is_multi_device_mode():
		return 'maître' if _is_master_installation() else 'secondaire'
	return 'locale'


def _installation_device_id():
	# Identifiant local aléatoire, propre à ce profil Kodi.
	# Il n'est jamais copié dans une sauvegarde restaurée.
	try:
		device_id = str(kodi_utils.get_setting(DEVICE_ID_SETTING, '') or '').strip()
	except Exception:
		device_id = ''
	if len(device_id) >= 24:
		return device_id
	device_id = uuid.uuid4().hex
	try:
		kodi_utils.set_setting(DEVICE_ID_SETTING, device_id)
		kodi_utils.clear_property('alkoflix_settings')
	except Exception as exc:
		kodi_utils.logger('alkoPastes userdata device id save failed', '%s: %s' % (type(exc).__name__, exc))
	return device_id


def _current_master_device_hash():
	# Le hash public sert uniquement à reconnaître l'installation maître.
	# Le vrai identifiant local n'est pas envoyé à alkoPastes.
	device_id = _installation_device_id()
	digest = hashlib.sha256(('alkoFlix userdata master:%s' % device_id).encode('utf-8')).hexdigest()
	try:
		if str(kodi_utils.get_setting(MASTER_DEVICE_HASH_SETTING, '') or '') != digest:
			kodi_utils.set_setting(MASTER_DEVICE_HASH_SETTING, digest)
	except Exception:
		pass
	return digest


def _remote_master_device_hash(data):
	try:
		return str(data.get('master_device_hash') or '').strip()
	except Exception:
		return ''


def _check_master_device_claim_for_code(code, show_dialog=False, offer_switch=False):
	# Si le paste existe déjà et contient un hash maître différent, cette installation
	# ne peut pas se déclarer maître avec ce code. Cela empêche deux Kodi de pousser
	# la même sauvegarde maître en parallèle.
	code = str(code or '').strip()
	if not code or not _is_multi_device_mode() or not _is_master_installation(): return True
	try:
		data = _parse_backup(_get_backup_content(code))
	except Exception as exc:
		# Ancien code invalide, paste supprimé ou problème temporaire : on laisse la logique
		# d'écriture normale gérer l'erreur/création. Le verrou ne doit bloquer que les
		# conflits clairement identifiés.
		kodi_utils.logger('alkoPastes userdata master claim check skipped', '%s | %s: %s' % (_mask_code(code), type(exc).__name__, exc))
		return True
	remote_hash = _remote_master_device_hash(data)
	local_hash = _current_master_device_hash()
	if not remote_hash or remote_hash == local_hash:
		try: kodi_utils.set_setting(MASTER_DEVICE_HASH_SETTING, local_hash)
		except Exception: pass
		kodi_utils.logger('alkoPastes userdata master claim check', 'OK | code=%s | legacy=%s' % (_mask_code(code), 'yes' if not remote_hash else 'no'))
		return True
	kodi_utils.logger('alkoPastes userdata master claim conflict', 'code=%s | local=%s | remote=%s' % (_mask_code(code), local_hash[:10], remote_hash[:10]))
	if show_dialog:
		text = ('Ce code de sauvegarde est déjà associé à une autre [B]installation maître[/B].[CR][CR]'
			'Pour éviter d’écraser la sauvegarde de référence, cette installation doit être utilisée comme [B]installation secondaire[/B].[CR][CR]'
			'Elle pourra restaurer la sauvegarde maître, mais ne pourra pas l’écraser.')
		if offer_switch:
			if kodi_utils.confirm_dialog(text=text, ok_label='Passer en secondaire', cancel_label='Annuler', top_space=True):
				set_alkoflix_userdata_role('secondary')
		else:
			kodi_utils.ok_dialog(text=text, top_space=True)
	return False


def verify_alkoflix_userdata_master_claim(show_dialog=False, offer_switch=False):
	# Fonction utilisée aussi par la synchronisation des favoris : si l'appareil
	# prétend être maître, il doit être le maître associé au code de sauvegarde.
	normalize_alkoflix_userdata_role(refresh=False)
	if not _is_multi_device_mode() or not _is_master_installation(): return True
	code = str(kodi_utils.get_setting(BACKUP_SETTING_MULTI, '') or '').strip()
	if not code: return True
	return _check_master_device_claim_for_code(code, show_dialog=show_dialog, offer_switch=offer_switch)


def _migrate_legacy_backup_code_to_role():
	# Compatibilité avec l’ancien modèle : le code unique était stocké dans
	# alkopastes.userdata_backup_code. Dès qu’un rôle maître/secondaire est choisi,
	# on reprend ce code comme code de sauvegarde visible afin d’éviter de créer
	# ou de demander un nouveau code inutilement.
	try:
		legacy_code = str(kodi_utils.get_setting(BACKUP_SETTING_LOCAL, '') or '').strip()
		active_code = str(kodi_utils.get_setting(BACKUP_SETTING_MULTI, '') or '').strip()
	except Exception: return False
	if not legacy_code or active_code: return False
	if not _is_multi_device_mode() or not _role_selected(): return False
	try:
		kodi_utils.set_setting(BACKUP_SETTING_MULTI, legacy_code)
		kodi_utils.clear_property('alkoflix_settings')
		kodi_utils.logger('alkoPastes userdata legacy backup code migrated', '%s | role=%s' % (_mask_code(legacy_code), _active_backup_label()))
		try: kodi_utils.refresh_addon_settings_window()
		except Exception: pass
		return True
	except Exception as exc:
		kodi_utils.logger('alkoPastes userdata legacy backup code migration failed', '%s: %s' % (type(exc).__name__, exc))
		return False


def _can_write_active_backup(show_dialog=False):
	# En mode maître/secondaire, seule l'installation maître peut écrire la sauvegarde de référence.
	# Les installations secondaires peuvent restaurer, mais ne doivent pas écraser la référence.
	if _is_multi_device_mode() and not _is_master_installation():
		if show_dialog:
			text = ('Cette installation est réglée comme [B]installation secondaire[/B].[CR][CR]'
				'Elle peut récupérer/restaurer la sauvegarde maître, mais elle ne doit pas l’écraser.[CR][CR]'
				'Pour transmettre des modifications vers les autres appareils, faites-les depuis l’[B]installation maître[/B].')
			kodi_utils.ok_dialog(text=text, top_space=True)
		return False
	return True


def _ensure_alkopastes_connected():
	if has_user_api_key(): return True
	text = ('alkoPastes n’est pas connecté.[CR][CR]'
		'Connectez votre clé API dans [B]Service(s)[/B] pour utiliser la sauvegarde/restauration alkoFlix.')
	if kodi_utils.confirm_dialog(text=text, ok_label='Service(s)', cancel_label='Annuler', top_space=True):
		kodi_utils.execute_builtin('RunPlugin(plugin://plugin.video.alkoflix/?mode=myservices)')
	return False


def normalize_alkoflix_userdata_role(refresh=False):
	"""Synchronise les anciens réglages cachés avec le choix visible maître/secondaire."""
	master = _master_selected()
	secondary = _secondary_selected()
	changed = False
	values = {}
	# Protection de récupération : si un ancien WIP a laissé les deux choix cochés,
	# on garde maître et on libère secondaire pour éviter un écran bloqué.
	if master and secondary:
		secondary = False
		values[INSTALLATION_SECONDARY_SETTING] = 'false'
		values['alkopastes.userdata_secondary_installation'] = 'false'
	if master:
		values['alkopastes.favourites_secondary_lock'] = 'false'
		values.update({
			BACKUP_MULTI_DEVICE_SETTING: 'true',
			BACKUP_MODE_SETTING: BACKUP_MODE_MULTI,
			INSTALLATION_ROLE_SETTING: INSTALLATION_ROLE_MASTER,
			USERDATA_AUTO_RESTORE_SETTING: 'false',
			USERDATA_AUTO_RESTORE_DUE_SETTING: '0'
		})
	elif secondary:
		values['alkopastes.favourites_secondary_lock'] = 'true'
		values.update({
			BACKUP_MULTI_DEVICE_SETTING: 'true',
			BACKUP_MODE_SETTING: BACKUP_MODE_MULTI,
			INSTALLATION_ROLE_SETTING: INSTALLATION_ROLE_SECONDARY,
			'alkopastes.userdata_auto_backup': 'false',
			'alkopastes.userdata_auto_backup_due': '0',
			'alkopastes.userdata_auto_backup_interval': '0'
		})
	else:
		values['alkopastes.favourites_secondary_lock'] = 'false'
		values.update({
			BACKUP_MULTI_DEVICE_SETTING: 'false',
			BACKUP_MODE_SETTING: BACKUP_MODE_LOCAL,
			INSTALLATION_ROLE_SETTING: INSTALLATION_ROLE_SECONDARY,
			'alkopastes.userdata_auto_backup': 'false',
			'alkopastes.userdata_auto_backup_due': '0',
			USERDATA_AUTO_RESTORE_SETTING: 'false',
			USERDATA_AUTO_RESTORE_DUE_SETTING: '0'
		})
	values.update(USERDATA_MANUAL_ONLY_DEFAULTS)
	for setting_id, value in values.items():
		try:
			if str(kodi_utils.get_setting(setting_id, '')) != str(value):
				kodi_utils.set_setting(setting_id, value)
				changed = True
		except Exception as exc:
			kodi_utils.logger('alkoPastes userdata normalize setting failed', '%s | %s' % (setting_id, exc))
	if _migrate_legacy_backup_code_to_role(): changed = True
	if changed:
		try: kodi_utils.clear_property('alkoflix_settings')
		except Exception: pass
		if refresh:
			try: kodi_utils.refresh_addon_settings_window()
			except Exception: pass
	return changed


def _clear_alkoflix_userdata_backup_codes():
	# Lors d’un changement de rôle maître/secondaire, l’ancien code ne doit jamais
	# survivre. On vide aussi l’ancien champ local pour empêcher une migration
	# automatique de le remettre dans le champ multi-appareils.
	changed = False
	for setting_id in (BACKUP_SETTING_MULTI, BACKUP_SETTING_LOCAL):
		try:
			if str(kodi_utils.get_setting(setting_id, '') or '').strip():
				kodi_utils.set_setting(setting_id, '')
				changed = True
		except Exception as exc:
			kodi_utils.logger('alkoPastes userdata clear backup code failed', '%s | %s' % (setting_id, exc))
	for setting_id in (USERDATA_AUTO_RESTORE_DUE_SETTING, 'alkopastes.userdata_auto_backup_due'):
		try: kodi_utils.set_setting(setting_id, '0')
		except Exception as exc: kodi_utils.logger('alkoPastes userdata reset backup due failed', '%s | %s' % (setting_id, exc))
	return changed


def _prompt_new_alkoflix_userdata_code(role):
	role_label = 'maître' if role == 'master' else 'secondaire'
	text = ('Cette installation est maintenant en mode [B]%s[/B].[CR][CR]'
		'L’ancien code de sauvegarde a été supprimé afin d’éviter de mélanger deux rôles ou deux sauvegardes.[CR][CR]'
		'[B]Inscrire votre nouveau code de sauvegarde[/B]') % role_label
	if kodi_utils.confirm_dialog(text=text, ok_label='Entrer le code', cancel_label='Annuler', top_space=True):
		return edit_alkoflix_userdata_code()
	try: kodi_utils.notification('Mode %s activé. Aucun code inscrit.' % role_label, 2500)
	except Exception: pass


def set_alkoflix_userdata_role(role):
	role = str(role or '').strip().lower()
	values = {
		BACKUP_MULTI_DEVICE_SETTING: 'true',
		BACKUP_MODE_SETTING: BACKUP_MODE_MULTI
	}
	if role == 'master':
		values.update({
			INSTALLATION_MASTER_SETTING: 'true',
			INSTALLATION_SECONDARY_SETTING: 'false',
			'alkopastes.userdata_secondary_installation': 'false',
			INSTALLATION_ROLE_SETTING: INSTALLATION_ROLE_MASTER,
			'alkopastes.favourites_secondary_lock': 'false',
			USERDATA_AUTO_RESTORE_SETTING: 'false',
			USERDATA_AUTO_RESTORE_DUE_SETTING: '0'
		})
	elif role == 'secondary':
		values.update({
			INSTALLATION_MASTER_SETTING: 'false',
			INSTALLATION_SECONDARY_SETTING: 'true',
			'alkopastes.userdata_secondary_installation': 'true',
			INSTALLATION_ROLE_SETTING: INSTALLATION_ROLE_SECONDARY,
			'alkopastes.favourites_secondary_lock': 'true',
			'alkopastes.userdata_auto_backup': 'false',
			'alkopastes.userdata_auto_backup_due': '0',
			'alkopastes.userdata_auto_backup_interval': '0'
		})
	else:
		return kodi_utils.ok_dialog(text='Choix d’installation invalide.', top_space=True)
	_clear_alkoflix_userdata_backup_codes()
	values.update(USERDATA_MANUAL_ONLY_DEFAULTS)
	for setting_id, value in values.items():
		try: kodi_utils.set_setting(setting_id, value)
		except Exception as exc: kodi_utils.logger('alkoPastes userdata set role failed', '%s | %s' % (setting_id, exc))
	try: kodi_utils.clear_property('alkoflix_settings')
	except Exception: pass
	try: kodi_utils.refresh_addon_settings_window()
	except Exception: pass
	return _prompt_new_alkoflix_userdata_code(role)


def edit_alkoflix_userdata_code():
	setting_id = BACKUP_SETTING_MULTI
	current = str(kodi_utils.get_setting(setting_id, '') or '').strip()
	code = kodi_utils.dialog.input('Code de sauvegarde alkoPastes :', defaultt=current)
	if code is None: return
	code = str(code or '').strip()
	if code and _is_multi_device_mode() and _is_master_installation() and has_user_api_key():
		if not _check_master_device_claim_for_code(code, show_dialog=True, offer_switch=True):
			if _is_multi_device_mode() and not _is_master_installation():
				kodi_utils.set_setting(setting_id, code)
				kodi_utils.set_setting(USERDATA_AUTO_RESTORE_DUE_SETTING, '0')
				kodi_utils.clear_property('alkoflix_settings')
				try: kodi_utils.refresh_addon_settings_window()
				except Exception: pass
				return kodi_utils.ok_dialog(text='Code de sauvegarde enregistré pour l’installation secondaire.', top_space=True)
			return
	kodi_utils.set_setting(setting_id, code)
	kodi_utils.set_setting(USERDATA_AUTO_RESTORE_DUE_SETTING, '0')
	kodi_utils.set_setting('alkopastes.userdata_auto_backup_due', '0')
	kodi_utils.clear_property('alkoflix_settings')
	try: kodi_utils.refresh_addon_settings_window()
	except Exception: pass
	if code:
		return kodi_utils.ok_dialog(text='Code de sauvegarde enregistré.', top_space=True)
	return kodi_utils.ok_dialog(text='Code de sauvegarde vidé.', top_space=True)


def reset_alkoflix_userdata_settings():
	# Conservé pour compatibilité avec un ancien raccourci WIP, mais plus affiché dans les réglages.
	values = {
		BACKUP_MULTI_DEVICE_SETTING: 'false',
		INSTALLATION_MASTER_SETTING: 'false',
		INSTALLATION_SECONDARY_SETTING: 'false',
		'alkopastes.userdata_secondary_installation': 'false',
		BACKUP_MODE_SETTING: BACKUP_MODE_LOCAL,
		INSTALLATION_ROLE_SETTING: INSTALLATION_ROLE_SECONDARY,
		'alkopastes.userdata_auto_backup': 'false',
		'alkopastes.userdata_auto_backup_due': '0',
		'alkopastes.userdata_auto_backup_interval': '0',
		USERDATA_AUTO_RESTORE_SETTING: 'false',
		USERDATA_AUTO_RESTORE_DUE_SETTING: '0'
	}
	values.update(USERDATA_MANUAL_ONLY_DEFAULTS)
	for setting_id, value in values.items():
		try: kodi_utils.set_setting(setting_id, value)
		except Exception as exc: kodi_utils.logger('alkoPastes userdata reset setting failed', '%s | %s' % (setting_id, exc))
	try: kodi_utils.clear_property('alkoflix_settings')
	except Exception: pass
	try: kodi_utils.refresh_addon_settings_window()
	except Exception: pass
	return kodi_utils.ok_dialog(text='Le choix de sauvegarde a été réinitialisé.', top_space=True)

def _is_backup_file(filename):
	name = str(filename or '')
	lower = name.lower()
	if not name or '/' in name or '\\' in name: return False
	if any(lower.endswith(suffix) for suffix in EXCLUDED_SUFFIX): return False
	return lower in PORTABLE_EXACT or lower in PORTABLE_DATABASES


def _is_watched_database(filename):
	return str(filename or '').lower() == WATCHED_DATABASE


def _current_watched_provider():
	try: return int(str(kodi_utils.get_setting('watched_indicators', '0') or '0').strip())
	except Exception: return 0


def _watched_database_portable_for_current_context():
	# watched.db est une base locale de secours, pas un mécanisme multi-appareil.
	# On la sauvegarde/restaure seulement quand alkoFlix utilise les statuts locaux.
	if _is_multi_device_mode(): return False
	return _current_watched_provider() == 0


def _should_collect_backup_file(filename):
	if not _is_backup_file(filename): return False
	if _is_watched_database(filename) and not _watched_database_portable_for_current_context():
		kodi_utils.logger('alkoPastes userdata backup collect', 'skip watched.db | provider=%s | scope=%s' % (_current_watched_provider(), _active_backup_label()))
		return False
	return True


def _should_restore_backup_file(filename, data=None):
	if not _is_backup_file(filename): return False
	if not _is_watched_database(filename): return True
	backup_scope = str((data or {}).get('backup_scope') or '').strip().lower()
	if backup_scope == 'master' or not _watched_database_portable_for_current_context():
		kodi_utils.logger('alkoPastes userdata restore write', 'SKIP | watched.db non importée | provider=%s | local_scope=%s | backup_scope=%s' % (_current_watched_provider(), _active_backup_label(), backup_scope or 'unknown'))
		return False
	return True


def _read_binary(path):
	reader = xbmcvfs.File(path, 'rb')
	try:
		data = reader.readBytes()
	finally:
		reader.close()
	if isinstance(data, bytes): return data
	try: return bytes(data)
	except Exception: return str(data).encode('utf-8')


def _settings_line_id(line):
	match = re.search(r'id=[\"\']([^\"\']+)[\"\']', line)
	return match.group(1) if match else ''


def _settings_line_value(line):
	match = re.search(r'value=([\"\'])(.*?)\1', line)
	if match: return match.group(2)
	match = re.search(r'>(.*?)</setting>', line)
	return match.group(1) if match else None


def _escape_xml_value(value):
	value = str(value or '')
	return value.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('\"', '&quot;').replace("'", '&apos;')


def _replace_settings_value(line, value):
	value = _escape_xml_value(value)
	if ' value=' in line:
		return re.sub(r'value=([\"\'])(.*?)\1', lambda match: 'value=%s%s%s' % (match.group(1), value, match.group(1)), line, count=1)
	if '</setting>' in line:
		return re.sub(r'>(.*?)</setting>', '>%s</setting>' % value, line, count=1)
	return line




def _settings_entry_line(setting_id, value):
	# Kodi accepte les réglages utilisateur au format value= dans settings.xml.
	# Sert uniquement à réinjecter une valeur locale sensible absente de la sauvegarde restaurée.
	return '\t<setting id="%s" value="%s" />\n' % (_escape_xml_value(setting_id), _escape_xml_value(value))


def _should_append_preserved_setting(setting_id, value):
	# Si la sauvegarde source ne contient pas une ligne sensible, on réinjecte la valeur locale
	# seulement lorsqu'elle a réellement une valeur utile à préserver.
	default_value = AUTH_SETTING_DEFAULTS.get(setting_id, '')
	value = str(value or '')
	return value != '' and value != str(default_value or '')

def _local_settings_values(setting_ids):
	values = {}
	for setting_id in setting_ids:
		try:
			values[setting_id] = kodi_utils.get_setting(setting_id, '')
		except Exception:
			values[setting_id] = ''
	return values


def _sanitize_settings_xml(data, preserve_values=None):
	preserve_values = preserve_values or {}
	try:
		text = data.decode('utf-8-sig')
	except Exception:
		try: text = data.decode('utf-8')
		except Exception: return data, 0

	changed = 0
	seen_settings = set()
	new_lines = []
	for line in text.splitlines(True):
		setting_id = _settings_line_id(line)
		if setting_id:
			seen_settings.add(setting_id)
		if setting_id in AUTH_SETTING_DEFAULTS:
			value = preserve_values.get(setting_id, AUTH_SETTING_DEFAULTS[setting_id])
			updated = _replace_settings_value(line, value)
			if updated != line: changed += 1
			line = updated
		else:
			mapping = LEGACY_BOOL_SETTING_VALUES.get(setting_id)
			if mapping:
				current_value = _settings_line_value(line)
				new_value = mapping.get(str(current_value or '').strip())
				if new_value is not None:
					updated = _replace_settings_value(line, new_value)
					if updated != line: changed += 1
					line = updated
		new_lines.append(line)

	missing_lines = []
	for setting_id, value in preserve_values.items():
		if setting_id in seen_settings: continue
		if setting_id not in AUTH_SETTING_DEFAULTS: continue
		if not _should_append_preserved_setting(setting_id, value): continue
		missing_lines.append(_settings_entry_line(setting_id, value))

	if missing_lines:
		changed += len(missing_lines)
		inserted = False
		for index in range(len(new_lines) - 1, -1, -1):
			if '</settings>' in new_lines[index]:
				new_lines[index:index] = missing_lines
				inserted = True
				break
		if not inserted:
			if new_lines and not new_lines[-1].endswith('\n'):
				new_lines[-1] += '\n'
			new_lines.extend(missing_lines)

	return ''.join(new_lines).encode('utf-8'), changed


def _write_binary(path, data):
	writer = xbmcvfs.File(path, 'wb')
	try:
		writer.write(data)
	finally:
		writer.close()
	return kodi_utils.path_exists(path)


def _file_size(path):
	try:
		stat = xbmcvfs.Stat(path)
		return int(stat.st_size())
	except Exception:
		try:
			translated = kodi_utils.translate_path(path)
			import os
			return os.path.getsize(translated)
		except Exception:
			return -1


def _validate_sqlite_file(path):
	# Validation légère mais très utile avant de remplacer une base existante.
	# Si la sauvegarde contient une base corrompue, on garde l'ancienne base locale.
	translated = kodi_utils.translate_path(path)
	con = None
	try:
		con = sqlite3.connect(translated, timeout=30)
		result = con.execute('PRAGMA integrity_check').fetchone()
		return bool(result and str(result[0]).lower() == 'ok')
	finally:
		try:
			if con: con.close()
		except Exception:
			pass


def _safe_rename(source, destination):
	try:
		if xbmcvfs.rename(source, destination): return True
	except Exception:
		pass
	try:
		if xbmcvfs.copy(source, destination):
			kodi_utils.delete_file(source)
			return kodi_utils.path_exists(destination) and not kodi_utils.path_exists(source)
	except Exception:
		pass
	return False


def _replace_file_atomically(filename, data):
	filename = str(filename or '')
	target = PROFILE_DIR + filename
	timestamp = str(int(time.time() * 1000))
	tmp = PROFILE_DIR + '.__restore_tmp_%s_%s' % (timestamp, filename)
	old = PROFILE_DIR + '.__restore_old_%s_%s' % (timestamp, filename)
	is_database = filename.lower().endswith('.db')

	if not _write_binary(tmp, data):
		kodi_utils.logger('alkoPastes userdata restore temp write failed', filename)
		return False
	if _file_size(tmp) != len(data):
		kodi_utils.logger('alkoPastes userdata restore temp size mismatch', '%s | expected=%s | actual=%s' % (filename, len(data), _file_size(tmp)))
		try: kodi_utils.delete_file(tmp)
		except Exception: pass
		return False
	if is_database and not _validate_sqlite_file(tmp):
		kodi_utils.logger('alkoPastes userdata restore sqlite validation failed', filename)
		try: kodi_utils.delete_file(tmp)
		except Exception: pass
		return False

	for suffix in ('-journal', '-wal', '-shm'):
		try:
			sidecar = target + suffix
			if kodi_utils.path_exists(sidecar): kodi_utils.delete_file(sidecar)
		except Exception as exc:
			kodi_utils.logger('alkoPastes userdata restore sidecar cleanup failed', '%s%s | %s' % (filename, suffix, exc))

	old_exists = False
	if kodi_utils.path_exists(target):
		old_exists = _safe_rename(target, old)
		if not old_exists:
			try:
				kodi_utils.delete_file(tmp)
			except Exception:
				pass
			kodi_utils.logger('alkoPastes userdata restore target locked', filename)
			return False

	if _safe_rename(tmp, target) and kodi_utils.path_exists(target):
		try:
			if old_exists and kodi_utils.path_exists(old): kodi_utils.delete_file(old)
		except Exception:
			pass
		return True

	# Sécurité : si le remplacement échoue, on remet l'ancien fichier.
	try:
		if old_exists and kodi_utils.path_exists(old): _safe_rename(old, target)
	except Exception:
		pass
	try:
		if kodi_utils.path_exists(tmp): kodi_utils.delete_file(tmp)
	except Exception:
		pass
	kodi_utils.logger('alkoPastes userdata restore atomic replace failed', filename)
	return False


def _database_basename(path):
	value = str(path or '')
	value = value.replace('\\', '/')
	return value.rsplit('/', 1)[-1].lower()


def _close_open_database_handles():
	# Ne force aucun import : on ferme uniquement les connexions déjà chargées dans le processus Kodi.
	# Cela évite d'écraser une base SQLite ouverte, surtout sur Windows/Android.
	target_names = set([i.lower() for i in PORTABLE_DATABASES + NON_PORTABLE_DATABASES])
	closed = []
	seen = set()
	for module in list(sys.modules.values()):
		try: values = list(vars(module).values())
		except Exception: continue
		for obj in values:
			try: marker = id(obj)
			except Exception: continue
			if marker in seen: continue
			seen.add(marker)
			db_file = getattr(obj, 'db_file', '')
			if not db_file or _database_basename(db_file) not in target_names: continue
			if not hasattr(obj, 'dbcon'): continue
			try:
				if getattr(obj, 'dbcur', None): obj.dbcur.close()
			except Exception:
				pass
			try:
				if getattr(obj, 'dbcon', None): obj.dbcon.close()
			except Exception:
				pass
			closed.append(obj)
	kodi_utils.logger('alkoPastes userdata restore db handles', 'closed=%s' % len(closed))
	return closed


def _reopen_database_handles(objects):
	reopened = 0
	for obj in objects or []:
		try:
			db_file = getattr(obj, 'db_file', '')
			if not db_file or db_file == ':memory:': continue
			obj.dbcon = kodi_utils.database_connect(db_file, isolation_level=None)
			obj.dbcur = obj.dbcon.cursor()
			if hasattr(obj, '_set_PRAGMAS'): obj._set_PRAGMAS()
			reopened += 1
		except Exception as exc:
			kodi_utils.logger('alkoPastes userdata restore db handle reopen failed', '%s: %s' % (type(exc).__name__, exc))
	kodi_utils.logger('alkoPastes userdata restore db handles', 'reopened=%s' % reopened)
	return reopened


def _collect_files():
	try:
		_, files = kodi_utils.list_dirs(PROFILE_DIR)
	except Exception:
		files = []
	items, sanitized_settings = [], 0
	for filename in sorted(files):
		if not _should_collect_backup_file(filename): continue
		path = PROFILE_DIR + filename
		try:
			data = _read_binary(path)
			if filename.lower() == 'settings.xml':
				data, sanitized_settings = _sanitize_settings_xml(data)
		except Exception as exc:
			kodi_utils.logger('alkoPastes userdata backup read skipped', '%s | %s' % (filename, exc))
			continue
		items.append({
			'name': filename,
			'size': len(data),
			'encoding': 'base64+gzip',
			'data': base64.b64encode(gzip.compress(data)).decode('ascii')
		})
	kodi_utils.logger('alkoPastes userdata backup collect', 'portable_files=%s | sanitized_settings=%s | names=%s' % (len(items), sanitized_settings, ','.join([item.get('name', '') for item in items]) or 'none'))
	return items, sanitized_settings


def _build_backup_payload():
	files, sanitized_settings = _collect_files()
	return {
		'type': BACKUP_TYPE,
		'version': BACKUP_VERSION,
		'portable': True,
		'addon': 'plugin.video.alkoflix',
		'addon_version': kodi_utils.get_addoninfo('version'),
		'created': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
		'backup_scope': 'master' if _is_multi_device_mode() else 'local',
		'installation_role': 'master' if _is_master_installation() else 'secondary',
		'master_device_hash': _current_master_device_hash() if _is_multi_device_mode() and _is_master_installation() else '',
		'master_device_hash_version': 1 if _is_multi_device_mode() and _is_master_installation() else 0,
		'sanitized_settings': sanitized_settings,
		'files': files
	}


def _save_backup_paste(content):
	_migrate_legacy_backup_code_to_role()
	setting_id = _active_backup_setting()
	code = str(kodi_utils.get_setting(setting_id, '')).strip()
	if code and _is_multi_device_mode() and _is_master_installation():
		if not _check_master_device_claim_for_code(code, show_dialog=False, offer_switch=False):
			raise Exception('Ce code de sauvegarde est déjà associé à une autre installation maître. Réglez cette installation comme secondaire.')
	title_prefix = 'Sauvegarde maître alkoFlix' if _is_multi_device_mode() else 'Sauvegarde locale alkoFlix'
	title = '%s - %s' % (title_prefix, datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
	payload = {'title': title, 'content': content, 'syntax': 'json', 'visibility': 'private', 'expiry': 'NULL'}
	kodi_utils.logger('alkoPastes userdata backup save', 'START | cached=%s | content_len=%s | title=%s' % (_mask_code(code), len(str(content or '')), title))
	created_new = False
	try:
		if code:
			result = _request_userdata('POST', 'update', payload=payload, params={'id': code}, timeout=60.0)
		else:
			result = _request_userdata('POST', 'paste', payload=payload, timeout=60.0)
			created_new = True
	except Exception as exc:
		# Si un ancien code est invalide, on crée une nouvelle sauvegarde propre.
		if not code: raise
		kodi_utils.logger('alkoPastes userdata backup update failed', '%s | recreate | %s' % (_mask_code(code), exc))
		result = _request_userdata('POST', 'paste', payload=payload, timeout=60.0)
		created_new = True
	new_code = _paste_code(result, code)
	kodi_utils.logger('alkoPastes userdata backup save', 'RESULT | old=%s | new=%s | created_new=%s' % (_mask_code(code), _mask_code(new_code), created_new))
	if new_code:
		kodi_utils.set_setting(setting_id, new_code)
		kodi_utils.clear_property('alkoflix_settings')
		kodi_utils.logger('alkoPastes userdata backup setting', '%s | %s' % ('updated' if new_code != code else 'unchanged', _mask_code(new_code)))
		try: kodi_utils.refresh_addon_settings_window()
		except Exception: pass
	return new_code


def backup_alkoflix_userdata():
	normalize_alkoflix_userdata_role(refresh=True)
	if not _ensure_alkopastes_connected(): return
	if not _can_write_active_backup(show_dialog=True): return
	if _is_multi_device_mode() and _is_master_installation() and not verify_alkoflix_userdata_master_claim(show_dialog=True, offer_switch=True): return
	mode_label = _active_backup_label()
	text = ('Cette sauvegarde [B]%s[/B] enverra vers votre compte alkoPastes un paste privé contenant uniquement les données alkoFlix utiles :[CR][CR]'
		'- réglages alkoFlix nettoyés[CR]'
		'- favoris[CR]'
		'- menus/dossiers/widgets personnalisés[CR]'
		'- statuts locaux de visionnement, seulement si le fournisseur actif est local[CR]'
		'[CR]'
		'Les caches, tokens, autorisations de comptes externes, caches de sources et informations propres à l’appareil ne sont pas sauvegardés.[CR][CR]'
		'Les statuts locaux ne sont pas utilisés comme synchronisation multi-appareil : Trakt ou MDBList restent la référence quand ils sont choisis.[CR][CR]'
		'Continuer ?') % mode_label
	if not kodi_utils.confirm_dialog(text=text, ok_label='Sauvegarder', cancel_label='Annuler', top_space=True): return
	kodi_utils.show_busy_dialog()
	try:
		payload = _build_backup_payload()
		if not payload.get('files'):
			kodi_utils.hide_busy_dialog()
			return kodi_utils.ok_dialog(text='Aucun fichier alkoFlix à sauvegarder.', top_space=True)
		content = json.dumps(payload, ensure_ascii=False, separators=(',', ':'))
		kodi_utils.logger('alkoPastes userdata backup payload', 'files=%s | content_len=%s' % (len(payload.get('files') or []), len(content)))
		code = _save_backup_paste(content)
		kodi_utils.hide_busy_dialog()
		title = 'Sauvegarde maître alkoFlix terminée.' if _is_multi_device_mode() else 'Sauvegarde locale alkoFlix terminée.'
		message = ('%s[CR][CR]'
			'Code : [B]%s[/B][CR]'
			'Fichiers sauvegardés : [B]%s[/B][CR]'
			'Réglages sensibles nettoyés : [B]%s[/B]') % (title, code or 'inconnu', len(payload.get('files') or []), payload.get('sanitized_settings', 0))
		return kodi_utils.ok_dialog(text=message, top_space=True)
	except Exception as exc:
		kodi_utils.hide_busy_dialog()
		kodi_utils.logger('alkoPastes userdata backup error', '%s: %s' % (type(exc).__name__, exc))
		return kodi_utils.ok_dialog(text='Erreur pendant la sauvegarde alkoPastes.[CR][CR]%s' % exc, top_space=True)


def _auto_backup_interval_seconds():
	value = kodi_utils.get_setting('alkopastes.userdata_auto_backup_interval', '0') or '0'
	value = str(value).strip()
	label_map = {
		'0': 0,
		'chaque jour': 0,
		'tous les 3 jours': 1,
		'1': 1,
		'chaque semaine': 2,
		'2': 2,
	}
	index = label_map.get(value.lower(), 0)
	return {0: 86400, 1: 259200, 2: 604800}.get(index, 86400)


def automatic_alkoflix_userdata_backup(force=False):
	normalize_alkoflix_userdata_role(refresh=False)
	if _is_multi_device_mode() and not _is_master_installation():
		return automatic_alkoflix_userdata_restore(force=force)
	if not force and kodi_utils.get_setting('alkopastes.userdata_auto_backup', 'false') != 'true': return 'disabled'
	current_time = int(time.time())
	interval = _auto_backup_interval_seconds()
	try: due_time = int(kodi_utils.get_setting('alkopastes.userdata_auto_backup_due', '0') or 0)
	except Exception: due_time = 0
	if not force and current_time < due_time:
		return 'not needed'
	if not _can_write_active_backup(show_dialog=False):
		# Protection anti-écrasement : une installation secondaire ne pousse jamais la sauvegarde partagée.
		kodi_utils.set_setting('alkopastes.userdata_auto_backup_due', str(current_time + 3600))
		kodi_utils.clear_property('alkoflix_settings')
		kodi_utils.logger('alkoPastes automatic userdata backup skipped', 'secondary installation in multi-device mode')
		return 'secondary installation'
	if _is_multi_device_mode() and _is_master_installation() and not verify_alkoflix_userdata_master_claim(show_dialog=False, offer_switch=False):
		kodi_utils.set_setting('alkopastes.userdata_auto_backup', 'false')
		kodi_utils.set_setting('alkopastes.userdata_auto_backup_due', str(current_time + 3600))
		kodi_utils.clear_property('alkoflix_settings')
		try: kodi_utils.notification('Sauvegarde maître bloquée : autre maître détecté', 5000)
		except Exception: pass
		kodi_utils.logger('alkoPastes automatic userdata backup skipped', 'master claim conflict')
		return 'master conflict'
	if not has_user_api_key():
		# Évite de relancer la vérification à chaque boucle si alkoPastes n'est pas connecté.
		kodi_utils.set_setting('alkopastes.userdata_auto_backup_due', str(current_time + 3600))
		kodi_utils.clear_property('alkoflix_settings')
		kodi_utils.logger('alkoPastes automatic userdata backup skipped', 'alkoPastes not connected')
		return 'no account'
	try:
		payload = _build_backup_payload()
		if not payload.get('files'):
			kodi_utils.set_setting('alkopastes.userdata_auto_backup_due', str(current_time + 3600))
			kodi_utils.clear_property('alkoflix_settings')
			kodi_utils.logger('alkoPastes automatic userdata backup skipped', 'no userdata files')
			return 'empty'
		content = json.dumps(payload, ensure_ascii=False, separators=(',', ':'))
		kodi_utils.logger('alkoPastes userdata backup payload', 'files=%s | content_len=%s' % (len(payload.get('files') or []), len(content)))
		code = _save_backup_paste(content)
		kodi_utils.set_setting('alkopastes.userdata_auto_backup_due', str(current_time + interval))
		kodi_utils.clear_property('alkoflix_settings')
		kodi_utils.logger('alkoPastes automatic userdata backup success', 'code=%s files=%s next=%s' % (code or 'unknown', len(payload.get('files') or []), current_time + interval))
		return 'success'
	except Exception as exc:
		# En cas d'erreur réseau/API, on réessaie plus tard sans harceler le service.
		kodi_utils.set_setting('alkopastes.userdata_auto_backup_due', str(current_time + 3600))
		kodi_utils.clear_property('alkoflix_settings')
		kodi_utils.logger('alkoPastes automatic userdata backup failed', '%s: %s' % (type(exc).__name__, exc))
		return 'failed'



def _auto_restore_interval_seconds():
	return 86400


def automatic_alkoflix_userdata_restore(force=False):
	normalize_alkoflix_userdata_role(refresh=False)
	if not _is_multi_device_mode() or _is_master_installation(): return 'not secondary'
	if not force and kodi_utils.get_setting(USERDATA_AUTO_RESTORE_SETTING, 'false') != 'true': return 'disabled'
	current_time = int(time.time())
	try: due_time = int(kodi_utils.get_setting(USERDATA_AUTO_RESTORE_DUE_SETTING, '0') or 0)
	except Exception: due_time = 0
	if not force and current_time < due_time: return 'not needed'
	if not has_user_api_key():
		kodi_utils.set_setting(USERDATA_AUTO_RESTORE_DUE_SETTING, str(current_time + 3600))
		kodi_utils.clear_property('alkoflix_settings')
		kodi_utils.logger('alkoPastes automatic userdata restore skipped', 'alkoPastes not connected')
		return 'no account'
	code = str(kodi_utils.get_setting(BACKUP_SETTING_MULTI, '') or '').strip()
	if not code:
		kodi_utils.set_setting(USERDATA_AUTO_RESTORE_DUE_SETTING, str(current_time + 3600))
		kodi_utils.clear_property('alkoflix_settings')
		kodi_utils.logger('alkoPastes automatic userdata restore skipped', 'no backup code')
		return 'no code'
	try:
		content = _get_backup_content(code)
		data = _parse_backup(content)
		if not _backup_version_matches_current(data):
			_warn_backup_version_mismatch(data, automatic=True)
			kodi_utils.set_setting(USERDATA_AUTO_RESTORE_DUE_SETTING, str(current_time + _auto_restore_interval_seconds()))
			kodi_utils.clear_property('alkoflix_settings')
			return 'version mismatch'
		created = str(data.get('created') or '')
		last_created = str(kodi_utils.get_setting(USERDATA_AUTO_RESTORE_LAST_CREATED_SETTING, '') or '')
		if not force and created and created == last_created:
			kodi_utils.set_setting(USERDATA_AUTO_RESTORE_DUE_SETTING, str(current_time + _auto_restore_interval_seconds()))
			kodi_utils.clear_property('alkoflix_settings')
			kodi_utils.logger('alkoPastes automatic userdata restore skipped', 'backup unchanged | created=%s' % created)
			return 'unchanged'
		count, sanitized, cleanup_count, skipped = _restore_files(data)
		if created: kodi_utils.set_setting(USERDATA_AUTO_RESTORE_LAST_CREATED_SETTING, created)
		kodi_utils.set_setting(USERDATA_AUTO_RESTORE_DUE_SETTING, str(current_time + _auto_restore_interval_seconds()))
		kodi_utils.clear_property('alkoflix_settings')
		kodi_utils.logger('alkoPastes automatic userdata restore success', 'code=%s restored=%s sanitized=%s cleanup=%s skipped=%s next=%s' % (_mask_code(code), count, sanitized, cleanup_count, skipped, current_time + _auto_restore_interval_seconds()))
		try: kodi_utils.notification('Restauration alkoFlix appliquée', 3000)
		except Exception: pass
		return 'success'
	except Exception as exc:
		kodi_utils.set_setting(USERDATA_AUTO_RESTORE_DUE_SETTING, str(current_time + 3600))
		kodi_utils.clear_property('alkoflix_settings')
		kodi_utils.logger('alkoPastes automatic userdata restore failed', '%s: %s' % (type(exc).__name__, exc))
		return 'failed'

def _get_backup_content(code):
	kodi_utils.logger('alkoPastes userdata restore get', 'START | code=%s' % _mask_code(code))
	result = _request_userdata('GET', 'get', params={'id': code}, timeout=60.0)
	paste = result.get('paste') or {}
	content = paste.get('content') or ''
	if isinstance(content, (dict, list)):
		content = json.dumps(content, ensure_ascii=False)
	else:
		content = str(content or '')
	kodi_utils.logger('alkoPastes userdata restore get', 'OK | code=%s | content_len=%s | title=%s' % (_mask_code(code), len(content), str(paste.get('title') or '')[:80]))
	return content


def _parse_backup(content):
	data = json.loads(content)
	if data.get('type') != BACKUP_TYPE:
		raise Exception('Le paste indiqué n’est pas une sauvegarde userdata alkoFlix valide.')
	kodi_utils.logger('alkoPastes userdata restore parse', 'OK | version=%s | files=%s | created=%s' % (data.get('version'), len(data.get('files') or []), data.get('created') or ''))
	return data


def _current_addon_version():
	try: return str(kodi_utils.get_addoninfo('version') or '').strip()
	except Exception: return ''


def _backup_addon_version(data):
	try: return str((data or {}).get('addon_version') or '').strip()
	except Exception: return ''


def _backup_version_matches_current(data):
	# Les sauvegardes récentes indiquent la version alkoFlix du maître.
	# Si elle diffère de celle de l'appareil secondaire, on bloque la restauration
	# pour éviter d'injecter une structure de réglages ou de bases incompatible.
	backup_version = _backup_addon_version(data)
	current_version = _current_addon_version()
	if not backup_version or not current_version: return True
	return backup_version == current_version


def _warn_backup_version_mismatch(data, automatic=False):
	backup_version = _backup_addon_version(data) or 'inconnue'
	current_version = _current_addon_version() or 'inconnue'
	kodi_utils.logger('alkoPastes userdata restore version mismatch', 'backup=%s | current=%s | automatic=%s' % (backup_version, current_version, automatic))
	if automatic:
		try: kodi_utils.notification('Restauration alkoFlix bloquée : mise à jour requise', 5000)
		except Exception: pass
		return False
	text = ('Version alkoFlix différente détectée.[CR][CR]'
		'La sauvegarde maître a été créée avec une autre version d’alkoFlix.[CR][CR]'
		'Version de la sauvegarde : [B]%s[/B][CR]'
		'Version installée ici : [B]%s[/B][CR][CR]'
		'Mettez à jour l’extension sur cet appareil avant de restaurer cette sauvegarde, afin d’éviter des problèmes d’incompatibilité entre les réglages, les bases locales ou la structure des menus.') % (backup_version, current_version)
	return kodi_utils.ok_dialog(text=text, top_space=True)


def _delete_profile_file(filename):
	deleted = 0
	for suffix in ('', '-journal', '-wal', '-shm'):
		try:
			path = PROFILE_DIR + filename + suffix
			if kodi_utils.path_exists(path):
				kodi_utils.delete_file(path)
				deleted += 1
		except Exception as exc:
			kodi_utils.logger('alkoPastes userdata portable cleanup failed', '%s%s | %s' % (filename, suffix, exc))
	return deleted


def _cleanup_non_portable_databases():
	deleted = 0
	for filename in NON_PORTABLE_DATABASES:
		deleted += _delete_profile_file(filename)
	kodi_utils.logger('alkoPastes userdata portable cleanup', 'non_portable_deleted=%s | files=%s' % (deleted, ','.join(NON_PORTABLE_DATABASES)))
	return deleted


def _restore_files(data):
	files = data.get('files') or []
	if not kodi_utils.path_exists(PROFILE_DIR): kodi_utils.make_directorys(PROFILE_DIR)
	closed_handles = _close_open_database_handles()
	cleanup_count = _cleanup_non_portable_databases()
	preserve_values = _local_settings_values(LOCAL_SETTINGS_TO_PRESERVE)
	count, sanitized, skipped = 0, 0, 0
	try:
		for item in files:
			filename = item.get('name')
			if not _should_restore_backup_file(filename, data=data):
				skipped += 1
				continue
			raw = base64.b64decode(item.get('data') or '')
			if item.get('encoding') == 'base64+gzip': raw = gzip.decompress(raw)
			if str(filename).lower() == 'settings.xml':
				raw, sanitized = _sanitize_settings_xml(raw, preserve_values=preserve_values)
			if _replace_file_atomically(filename, raw):
				count += 1
				kodi_utils.logger('alkoPastes userdata restore write', 'OK | %s | bytes=%s' % (filename, len(raw)))
			else:
				kodi_utils.logger('alkoPastes userdata restore write', 'FAIL | %s' % filename)
	finally:
		_reopen_database_handles(closed_handles)
	try:
		from modules.cache import check_databases
		check_databases()
	except Exception as exc:
		kodi_utils.logger('alkoPastes userdata restore check databases failed', '%s: %s' % (type(exc).__name__, exc))
	try:
		if str(kodi_utils.get_setting('trakt_user', '') or '').strip():
			from caches.trakt_cache import clear_all_trakt_cache_data
			clear_all_trakt_cache_data(reason='userdata_restore')
	except Exception as exc:
		kodi_utils.logger('alkoPastes userdata restore trakt rebuild failed', '%s: %s' % (type(exc).__name__, exc))
	kodi_utils.logger('alkoPastes userdata restore write', 'DONE | restored=%s | received=%s | skipped_non_portable=%s | sanitized_settings=%s | cleanup=%s' % (count, len(files), skipped, sanitized, cleanup_count))
	return count, sanitized, cleanup_count, skipped


def restore_alkoflix_userdata():
	normalize_alkoflix_userdata_role(refresh=True)
	if not _ensure_alkopastes_connected(): return
	setting_id = _active_backup_setting()
	code = str(kodi_utils.get_setting(setting_id, '')).strip()
	if not code:
		code = kodi_utils.dialog.input('Code de sauvegarde alkoPastes :')
		if not code: return
		kodi_utils.set_setting(setting_id, code.strip())
		kodi_utils.clear_property('alkoflix_settings')
	if _is_multi_device_mode() and _is_master_installation() and not _check_master_device_claim_for_code(code, show_dialog=True, offer_switch=True):
		if _is_multi_device_mode() and not _is_master_installation():
			try: kodi_utils.set_setting(BACKUP_SETTING_MULTI, str(code or '').strip())
			except Exception: pass
		else:
			return
	kodi_utils.show_busy_dialog()
	try:
		data = _parse_backup(_get_backup_content(code))
	except Exception as exc:
		kodi_utils.hide_busy_dialog()
		kodi_utils.logger('alkoPastes userdata restore error', '%s: %s' % (type(exc).__name__, exc))
		return kodi_utils.ok_dialog(text='Erreur pendant la lecture de la sauvegarde alkoPastes.[CR][CR]%s' % exc, top_space=True)
	finally:
		kodi_utils.hide_busy_dialog()
	if not _backup_version_matches_current(data):
		return _warn_backup_version_mismatch(data, automatic=False)

	role_note = ''
	if _is_multi_device_mode() and not _is_master_installation():
		role_note = 'Cette installation est secondaire : elle récupère la sauvegarde, mais ne l’écrit pas automatiquement.[CR][CR]'
	elif _is_multi_device_mode():
		role_note = 'Cette installation est maître : elle sert de référence pour les autres appareils.[CR][CR]'
	text = ('Cette restauration alkoFlix remplacera les données alkoFlix par celles de la sauvegarde.[CR][CR]'
		'%s'
		'Code : [B]%s[/B][CR]'
		'Version : [B]%s[/B][CR][CR]'
		'Les caches, tokens, autorisations de comptes externes et données propres à l’appareil seront ignorés ou nettoyés.[CR][CR]'
		'Les statuts locaux de visionnement ne seront restaurés que si cet appareil utilise le fournisseur local alkoFlix. En mode Trakt/MDBList ou multi-appareil, ils sont ignorés afin d’éviter les conflits entre appareils.[CR][CR]'
		'Un redémarrage de Kodi est recommandé après la restauration.[CR][CR]'
		'Continuer ?') % (role_note, _mask_code(code), _backup_addon_version(data) or _current_addon_version() or 'inconnue')
	if not kodi_utils.confirm_dialog(text=text, ok_label='Restaurer', cancel_label='Annuler', top_space=True): return

	kodi_utils.show_busy_dialog()
	try:
		count, sanitized, cleanup_count, skipped = _restore_files(data)
		created = str(data.get('created') or '')
		if created: kodi_utils.set_setting(USERDATA_AUTO_RESTORE_LAST_CREATED_SETTING, created)
		kodi_utils.clear_property('alkoflix_settings')
		kodi_utils.hide_busy_dialog()
		message = ('Restauration alkoFlix terminée.[CR][CR]'
			'Fichiers restaurés : [B]%s[/B][CR]'
			'Réglages sensibles nettoyés : [B]%s[/B][CR]'
			'Données non portables ignorées : [B]%s[/B][CR]'
			'Caches locaux nettoyés : [B]%s[/B][CR][CR]'
			'Voulez-vous redémarrer Kodi maintenant ?') % (count, sanitized, skipped, cleanup_count)
		if kodi_utils.confirm_dialog(text=message, ok_label='Redémarrer', cancel_label='Plus tard', top_space=True):
			kodi_utils.execute_builtin('RestartApp')
		return
	except Exception as exc:
		kodi_utils.hide_busy_dialog()
		kodi_utils.logger('alkoPastes userdata restore error', '%s: %s' % (type(exc).__name__, exc))
		return kodi_utils.ok_dialog(text='Erreur pendant la restauration alkoPastes.[CR][CR]%s' % exc, top_space=True)
