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

"""Réutilisation prudente des packs saison/intégrale déjà choisis.

Cette couche ne remplace pas la recherche de sources. Elle évite seulement de
réinterroger les trackers privés lorsqu'un pack C411/Torr9/TR4KER déjà sélectionné
contient clairement l'épisode demandé.
"""

import json
import re
import time
import unicodedata

from modules import kodi_utils, settings
from modules.source_objects import _is_blocked_file_source
from modules.source_utils import seas_ep_filter, supported_video_extensions, extras_filter

PROPERTY_NAME = 'alkoflix_private_pack_reuse'
PRIVATE_PACK_PROVIDERS = ('c411', 'torr9', 'tr4ker')
PACK_TYPES = ('season', 'show')
MAX_AGE_SECONDS = 72 * 60 * 60


def _now():
	try: return int(time.time())
	except Exception: return 0


def _safe_int(value, default=0):
	try:
		if value in ('', None): return default
		return int(value)
	except Exception:
		return default


def _clean(value):
	try: return str(value or '').strip().lower()
	except Exception: return ''


def _normalize_title(value):
	try:
		value = unicodedata.normalize('NFKD', str(value or ''))
		value = ''.join(c for c in value if not unicodedata.combining(c))
		value = re.sub(r'[^a-z0-9]+', ' ', value.lower())
		return re.sub(r'\s+', ' ', value).strip()
	except Exception:
		return ''


def _provider_from_item(item):
	try:
		values = (
			item.get('provider'), item.get('provider_name'), item.get('tracker'),
			item.get('tracker_name'), item.get('source'), item.get('scrape_provider')
		)
		for value in values:
			value = _clean(value)
			if value in PRIVATE_PACK_PROVIDERS: return value
	except Exception:
		pass
	return ''


def _is_private_pack_item(item):
	try:
		return bool(
			isinstance(item, dict)
			and item.get('package') in PACK_TYPES
			and _provider_from_item(item) in PRIVATE_PACK_PROVIDERS
			and item.get('hash')
			and item.get('url')
			and str(item.get('url') or '').lower().startswith('magnet')
			and item.get('debrid')
		)
	except Exception:
		return False


def _identity_from_meta(meta):
	meta = meta or {}
	return {
		'tmdb_id': _clean(meta.get('tmdb_id')),
		'imdb_id': _clean(meta.get('imdb_id')),
		'tvdb_id': _clean(meta.get('tvdb_id')),
		'title_key': _normalize_title(meta.get('title') or meta.get('tvshowtitle') or meta.get('original_title')),
		'year': _clean(meta.get('year'))
	}


def _identity_matches(stored, meta):
	try:
		current = _identity_from_meta(meta)
		for key in ('tmdb_id', 'imdb_id', 'tvdb_id'):
			if stored.get(key) and current.get(key) and stored.get(key) == current.get(key): return True
		# Fallback seulement si aucun identifiant fiable n'est disponible des deux côtés.
		if any(stored.get(k) or current.get(k) for k in ('tmdb_id', 'imdb_id', 'tvdb_id')): return False
		return bool(stored.get('title_key') and stored.get('title_key') == current.get('title_key'))
	except Exception:
		return False


def _safe_file(item):
	try:
		filename = item.get('filename') or item.get('path') or item.get('name') or ''
		link = item.get('link') or item.get('url') or item.get('id') or ''
		if not filename or not link: return None
		data = {
			'filename': str(filename),
			'link': str(link),
			'size': item.get('size') or item.get('bytes') or 0,
			'torrent_id': item.get('torrent_id') or item.get('id') or ''
		}
		return None if _is_blocked_file_source(data) else data
	except Exception:
		return None


def _candidate_files(files, season, episode, title=''):
	try:
		extensions = supported_video_extensions()
		extra_tokens = tuple(i for i in extras_filter() if i not in _clean(title))
		candidates = []
		for item in files or []:
			file_item = _safe_file(item)
			if not file_item: continue
			filename = _clean(file_item.get('filename'))
			if not filename or filename.endswith('.m2ts'): continue
			if not filename.endswith(tuple(extensions)): continue
			if any(token in filename for token in extra_tokens): continue
			if not seas_ep_filter(season, episode, filename): continue
			candidates.append(file_item)
		try: candidates.sort(key=lambda k: float(k.get('size') or 0), reverse=True)
		except Exception: pass
		return candidates
	except Exception:
		return []


def _load_cache():
	try:
		value = kodi_utils.get_property(PROPERTY_NAME)
		if not value: return {}
		data = json.loads(value)
		if not isinstance(data, dict): return {}
		created = _safe_int(data.get('created'), 0)
		if created and _now() - created > MAX_AGE_SECONDS:
			clear_cached_pack('expiré')
			return {}
		return data
	except Exception:
		return {}


def clear_cached_pack(reason=''):
	try: kodi_utils.clear_property(PROPERTY_NAME)
	except Exception: pass
	try:
		if reason: kodi_utils.logger('Réutilisation pack', 'Cache pack vidé | raison=%s' % reason)
	except Exception:
		pass


def store_pack_from_source(source_item, meta, files, selected_file=None, store_to_cloud=None):
	try:
		source_item = dict(source_item or {})
		if not meta or meta.get('media_type') != 'episode': return False
		if not _is_private_pack_item(source_item): return False
		season, episode = _safe_int(meta.get('custom_season') or meta.get('season')), _safe_int(meta.get('custom_episode') or meta.get('episode'))
		if not season or not episode: return False
		safe_files = [i for i in (_safe_file(item) for item in (files or [])) if i]
		if not _candidate_files(safe_files, season, episode, meta.get('ep_name') or meta.get('title') or ''): return False
		provider = _provider_from_item(source_item)
		identity = _identity_from_meta(meta)
		payload = {
			'created': _now(),
			'provider': provider,
			'debrid': source_item.get('debrid') or '',
			'hash': str(source_item.get('hash') or '').lower(),
			'url': source_item.get('url') or '',
			'name': source_item.get('name') or source_item.get('URLName') or '',
			'package': source_item.get('package') or '',
			'quality': source_item.get('quality') or '',
			'language': source_item.get('language') or '',
			'extraInfo': source_item.get('extraInfo') or source_item.get('info') or '',
			'size': source_item.get('size') or 0,
			'size_label': source_item.get('size_label') or '',
			'tracker': source_item.get('tracker') or source_item.get('provider_name') or provider,
			'source_provider': source_item.get('provider') or provider,
			'store_to_cloud': bool(store_to_cloud),
			'identity': identity,
			'files': safe_files[:500]
		}
		kodi_utils.set_property(PROPERTY_NAME, json.dumps(payload))
		try:
			kodi_utils.logger('Réutilisation pack', 'Pack mémorisé | provider=%s | debrid=%s | hash=%s | fichiers=%s | cloud=%s' % (provider, payload.get('debrid'), payload.get('hash'), len(payload.get('files') or []), payload.get('store_to_cloud')))
		except Exception:
			pass
		return True
	except Exception as exc:
		try: kodi_utils.logger('Réutilisation pack', 'Échec mémorisation pack | erreur=%s' % exc)
		except Exception: pass
		return False


def build_reuse_source(meta):
	try:
		meta = meta or {}
		if meta.get('media_type') != 'episode': return None
		season, episode = _safe_int(meta.get('custom_season') or meta.get('season')), _safe_int(meta.get('custom_episode') or meta.get('episode'))
		if not season or not episode: return None
		cache = _load_cache()
		if not cache: return None
		if not _identity_matches(cache.get('identity') or {}, meta): return None
		if cache.get('package') == 'season':
			# Un pack saison ne doit jamais être réutilisé hors saison.
			cached_files = cache.get('files') or []
		else:
			cached_files = cache.get('files') or []
		candidates = _candidate_files(cached_files, season, episode, meta.get('ep_name') or meta.get('title') or '')
		if not candidates: return None
		file_item = candidates[0]
		provider = cache.get('provider') or cache.get('source_provider') or ''
		debrid = cache.get('debrid') or ''
		item = {
			'external': True, 'provider': provider, 'provider_name': cache.get('tracker') or provider,
			'scrape_provider': 'external', 'source': 'torrent', 'debrid': debrid,
			'cache_provider': ('Cached %s' % debrid) if debrid else 'Cached Pack', 'direct': False, 'debridonly': True,
			'personal_source': False, 'bypass_filter': True, 'pack_reuse': True,
			'provider_cache_hit': True, 'local_cache': True, 'local_cache_label': 'CACHE',
			'package': cache.get('package') or 'season', 'hash': cache.get('hash'), 'url': cache.get('url'),
			'name': cache.get('name') or file_item.get('filename'), 'URLName': file_item.get('filename'),
			'name_info': '%s %s' % (cache.get('name') or '', file_item.get('filename') or ''),
			'quality': cache.get('quality') or 'SD', 'language': cache.get('language') or 'fr',
			'extraInfo': cache.get('extraInfo') or 'Pack déjà choisi', 'info': cache.get('extraInfo') or '',
			'size': cache.get('size') or 0, 'size_label': cache.get('size_label') or '',
			'tracker': cache.get('tracker') or provider,
			'pack_reuse_file_link': file_item.get('link'),
			'pack_reuse_file_name': file_item.get('filename'),
			'pack_reuse_torrent_id': file_item.get('torrent_id') or '',
			'bingeGroup': 'private-pack:%s:%s' % (provider, cache.get('hash') or '')
		}
		return None if _is_blocked_file_source(item) else item
	except Exception as exc:
		try: kodi_utils.logger('Réutilisation pack', 'Échec préparation épisode depuis cache | erreur=%s' % exc)
		except Exception: pass
		return None


def private_provider_names():
	return PRIVATE_PACK_PROVIDERS


def resolve_reuse_source(source, api):
	try:
		file_link = getattr(source, 'pack_reuse_file_link', None)
		if not file_link: return None
		file_name = getattr(source, 'pack_reuse_file_name', '')
		if _is_blocked_file_source({'filename': file_name, 'link': file_link}):
			clear_cached_pack('fichier refusé')
			return None
		url = api.unrestrict_link(file_link)
		if url:
			try: kodi_utils.logger('Réutilisation pack', 'Épisode résolu depuis le pack mémorisé | provider=%s | debrid=%s | fichier=%s' % (getattr(source, 'provider', ''), getattr(source, 'debrid', ''), file_name or 'N/A'))
			except Exception: pass
			return url
		clear_cached_pack('lien de pack non résolu')
	except Exception as exc:
		clear_cached_pack('erreur résolution pack')
		try: kodi_utils.logger('Réutilisation pack', 'Échec résolution depuis pack | erreur=%s' % exc)
		except Exception: pass
	return None
