import re
from json import loads as jsloads
from urllib.parse import quote, urlparse

from alko import client
from alko import source_utils
from alko.control import setting as getSetting


class source:
	timeout = 7
	priority = 1
	pack_capable = False # packs parsed in sources function
	hasMovies = True
	hasEpisodes = True
	def _base_from_manifest(self, manifest_url):
		if not manifest_url: return None
		try:
			if '://' not in manifest_url:
				manifest_url = 'https://%s' % manifest_url
			parsed = urlparse(manifest_url)
			if not parsed.scheme or not parsed.netloc: return None
			path = parsed.path or ''
			for suffix in ('/manifest.json', '/manifest'):
				if path.endswith(suffix):
					path = path[:-len(suffix)]
					break
			base_url = '%s://%s%s' % (parsed.scheme, parsed.netloc, path)
			return base_url.rstrip('/')
		except:
			return None
	def _normalize_base_link_for_catalogue_match(self, value):
		try:
			value = str(value or '').strip()
			if not value: return ''
			base = self._base_from_manifest(value) or value.rstrip('/')
			return base.lower().rstrip('/')
		except:
			return ''

	def _catalogue_id_belongs_to_this_manifest(self, catalogue_id='', catalogue_base_url='', catalogue_video_id=False):
		# Un ID interne de catalogue ne doit être envoyé à un scraper custom que si
		# le scraper custom pointe vers le même manifest que le catalogue d'origine.
		# Sinon un ID French Stream (fs:...) envoyé à Baguettio, par exemple, ne peut
		# pas retourner de liens. Dans ce cas, on revient aux IDs standards IMDb/TMDb.
		catalogue_id = str(catalogue_id or '').strip()
		if not catalogue_id: return False
		catalogue_base = self._normalize_base_link_for_catalogue_match(catalogue_base_url)
		provider_base = self._normalize_base_link_for_catalogue_match(self.base_link)
		if catalogue_base and provider_base:
			return catalogue_base == provider_base
		# Garde-fou spécifique : fs: est propre à French Stream. On ne l'envoie pas
		# à un autre manifest custom si la base d'origine n'est pas connue.
		if catalogue_id.lower().startswith('fs:'):
			return provider_base and ('french-stream' in provider_base or 'frenchstream' in provider_base)
		# Sans base connue, on conserve l'ancien comportement pour les autres IDs.
		return True

	def _standard_request_id(self, imdb='', tmdb=''):
		imdb = str(imdb or '').strip()
		tmdb = str(tmdb or '').strip()
		if imdb: return imdb
		if tmdb:
			return tmdb if tmdb.lower().startswith('tmdb:') else 'tmdb:%s' % tmdb
		return ''

	def _release_title_from_stream(self, stream, meta=None, behavior=None):
		meta = meta or stream.get('meta') or {}
		behavior = behavior or stream.get('behaviorHints') or {}
		release_title = meta.get('originalTitle') or meta.get('title') or meta.get('name') or ''
		if not release_title:
			release_title = behavior.get('filename') or ''
		if not release_title:
			for key in ('title', 'description'):
				text = stream.get(key) or ''
				if not text: continue
				text = text.replace('\u2508\u27a4', '\n')
				match = re.search(r'([A-Za-z0-9].*?\.(?:mkv|mp4|avi|m2ts|m4v|ts))', text, re.IGNORECASE)
				if match:
					return match.group(1).strip()
				line = text.split('\n')[0].strip()
				if line:
					release_title = line
					break
		if not release_title:
			release_title = stream.get('name') or ''
		return release_title
	def _size_from_stream(self, stream, meta=None, behavior=None):
		meta = meta or stream.get('meta') or {}
		behavior = behavior or stream.get('behaviorHints') or {}
		# Les addons Stremio ne placent pas tous la taille au même endroit.
		# Baguettio, selon le mode utilisé, peut fournir la taille directement dans "size"
		# ou seulement dans les textes affichés (description, filename, folderName, title).
		for value in (
			meta.get('size'), behavior.get('videoSize'), behavior.get('size'),
			stream.get('size'), stream.get('videoSize'), stream.get('sizeBytes'),
			stream.get('filesize'), stream.get('fileSize')
		):
			try:
				if value in ('', None): continue
				if isinstance(value, str):
					value = value.strip()
					if re.search(r'\b(?:gb|gib|mb|mib)\b', value, re.I):
						return value
				value = float(value)
				if value > 0:
					return f"{value / 1073741824:.2f} GB"
			except:
				pass
		for text in (
			behavior.get('filename'), stream.get('folderName'), stream.get('filename'),
			stream.get('title'), stream.get('name'), stream.get('description')
		):
			try:
				if not text: continue
				match = re.search(r'(?<![A-Za-z0-9])([0-9]+(?:[\.,][0-9]+)?)\s*(GB|GiB|MB|MiB)\b', str(text), re.I)
				if match:
					return '%s %s' % (match.group(1).replace(',', '.'), match.group(2).upper().replace('GIB', 'GB').replace('MIB', 'MB'))
			except:
				pass
		return ''

	def _hash_from_stream(self, stream, meta=None, behavior=None, direct_url=None):
		meta = meta or stream.get('meta') or {}
		behavior = behavior or stream.get('behaviorHints') or {}
		for value in (
			stream.get('infoHash'), stream.get('infohash'), stream.get('info_hash'), stream.get('hash'),
			stream.get('torrentHash'), stream.get('torrent_hash'), stream.get('btih'), stream.get('magnetHash'),
			meta.get('infoHash'), meta.get('infohash'), meta.get('info_hash'), meta.get('hash'),
			meta.get('torrentHash'), meta.get('torrent_hash'), meta.get('btih'), meta.get('magnetHash'),
			behavior.get('infoHash'), behavior.get('infohash'), behavior.get('info_hash'), behavior.get('hash'),
			behavior.get('torrentHash'), behavior.get('torrent_hash'), behavior.get('btih'), behavior.get('magnetHash')
		):
			try:
				if value in ('', None): continue
				value = str(value).strip()
				match = re.search(r'([A-Fa-f0-9]{40})', value)
				if match: return match.group(1).lower()
			except:
				pass
		for text in (
			direct_url, stream.get('url'), stream.get('externalUrl'), stream.get('behaviorHints'),
			behavior.get('filename'), stream.get('folderName'), stream.get('filename'),
			stream.get('title'), stream.get('name'), stream.get('description')
		):
			try:
				if not text: continue
				text = str(text)
				match = re.search(r'btih:([A-Fa-f0-9]{40})', text, re.I)
				if match: return match.group(1).lower()
				match = re.search(r'/alldebrid/([A-Fa-f0-9]{40})(?:/|$)', text, re.I)
				if match: return match.group(1).lower()
				match = re.search(r'\b([A-Fa-f0-9]{40})\b', text)
				if match: return match.group(1).lower()
			except:
				pass
		return None

	def _indexer_from_description(self, description):
		if not description: return None
		text = str(description).replace('\u2508\u27a4', '\n')
		match = re.search(r'[\U0001F50D\U0001F50E]\s*([^\n\r]+)', text)
		if not match: return None
		value = match.group(1).strip()
		return value or None

	def _known_tracker_label(self, text):
		try:
			if not text: return None
			value = str(text).lower()
			for old, repl in (('é', 'e'), ('è', 'e'), ('ê', 'e'), ('ë', 'e'), ('à', 'a'), ('â', 'a'), ('î', 'i'), ('ï', 'i'), ('ô', 'o'), ('ù', 'u'), ('û', 'u'), ('ç', 'c')):
				value = value.replace(old, repl)
			compact = re.sub(r'[^a-z0-9]+', ' ', value).strip()
			if re.search(r'\bfree\s*telecharg', compact): return 'Free-Telecharger'
			if re.search(r'\bwawa\s*city\b|\bwawacity\b', compact): return 'Wawacity'
			if re.search(r'\bdarki\s*api\b|\bdarkiapi\b', compact): return 'Hydracker'
			if re.search(r'\bwa\s*source\b|\bwasource\b', compact): return 'WASource'
			if re.search(r'\bmovix\b', compact): return 'Movix'
			if re.search(r'\bc\s*411\b|\bc411\b', compact): return 'C411'
			if re.search(r'\btorr(?:ent)?\s*9\b|\btorr9\b|\btorrent9\b', compact): return 'Torr9'
			if re.search(r'\bygg(?:torrent)?\b', compact): return 'YGG'
			if re.search(r'\bthe\s*old\s*school\b|\bold\s*school\b|\btos\b', compact): return 'The Old School'
			if re.search(r'\bla\s*cale\b|\blacale\b', compact): return 'La Cale'
			if re.search(r'\bgeneration\s*free\b|\bgenerationfree\b|\bgen\s*free\b|\bg\s*free\b', compact): return 'Generation-Free'
			if re.search(r'\btr4ker(?:\s*net)?\b', compact): return 'TR4KER'
			if re.search(r'\bg3\s*mini\s*(?:track3r|tracker)?\b|\bg3mini\s*(?:track3r|tracker)?\b|\bgemini\b', compact): return 'G3MINI TRACK3R'
			if re.search(r'\bnostradamus\b', compact): return 'Nostradamus'
		except:
			pass
		return None

	def _clean_tracker_label(self, value):
		try:
			if value in ('', None): return None
			value = str(value).replace('\u2508\u27a4', ' ')
			for symbol in ('🔍', '🔎', '☁', '📦', '🌎', '📁', '🎥', '🎞', '🏷'):
				value = value.replace(symbol, ' ')
			value = re.sub(r'\s+', ' ', value).strip(' -|,;')
			known = self._known_tracker_label(value)
			if known: return known
			# Ne jamais utiliser le nom du manifest custom comme tracker/site.
			manifest = str(getattr(self, 'display_name', '') or '').strip().lower()
			if manifest and value.strip().lower() == manifest:
				return None
			return value or None
		except:
			return None

	def _tracker_from_stream(self, stream, meta=None, behavior=None):
		meta = meta or stream.get('meta') or {}
		behavior = behavior or stream.get('behaviorHints') or {}
		# Même logique que AIOStreams : certains manifests placent le vrai
		# tracker/site dans la description visible, d'autres dans meta/indexer.
		for text in (
			stream.get('description'), stream.get('title'), stream.get('name'),
			behavior.get('filename'), stream.get('folderName'), stream.get('filename'),
			meta.get('indexer'), meta.get('tracker'), meta.get('site'), meta.get('source')
		):
			known = self._known_tracker_label(text)
			if known: return known
		for text in (stream.get('description'), stream.get('title'), stream.get('name'), behavior.get('filename')):
			tracker = self._clean_tracker_label(self._indexer_from_description(text or ''))
			if tracker: return tracker
		for value in (
			meta.get('indexer'), meta.get('tracker'), meta.get('site'), meta.get('source'),
			stream.get('indexer'), stream.get('tracker'), stream.get('site'), stream.get('source'),
			behavior.get('indexer'), behavior.get('tracker'), behavior.get('site'), behavior.get('source'),
			stream.get('addon'), stream.get('name')
		):
			tracker = self._clean_tracker_label(value)
			if tracker: return tracker
		return ''

	def _debrid_from_direct_url(self, direct_url):
		try:
			url = str(direct_url or '').lower()
			if 'alldebrid' in url: return 'alldebrid'
			if 'real-debrid' in url or 'realdebrid' in url: return 'real-debrid'
			if 'premiumize' in url: return 'premiumize.me'
			if 'torbox' in url: return 'torbox'
		except:
			pass
		return 'alldebrid'

	def __init__(self):
		self.language = ['en', 'fr']
		self.display_name = getSetting('custom_2.name', '').strip() or 'Custom Stremio Addon 2'
		manifest_url = getSetting('custom_2.manifest_url', '').strip()
		self.base_link = self._base_from_manifest(manifest_url) if manifest_url else None
		self.movieSearch_link = '/stream/movie/%s.json'
		self.tvSearch_link = '/stream/series/%s:%s:%s.json'
		self.min_seeders = 0

	def sources(self, data, hostDict):
		sources = []
		if not data or not self.base_link: return sources
		sources_append = sources.append
		try:
			title = data['tvshowtitle'] if 'tvshowtitle' in data else data['title']
			title = title.replace('&', 'and').replace('Special Victims Unit', 'SVU').replace('/', ' ').replace(':', ' ')
			title = re.sub(r'\s+', ' ', title).strip()
			aliases = data['aliases']
			episode_title = data['title'] if 'tvshowtitle' in data else None
			total_seasons = data['total_seasons'] if 'tvshowtitle' in data else None
			year = data['year']
			imdb = data.get('imdb')
			catalogue_id = data.get('catalogue_id')
			catalogue_base_url = data.get('catalogue_base_url') or ''
			catalogue_video_id = data.get('catalogue_video_id') is True
			if catalogue_id is not None:
				catalogue_id = str(catalogue_id).strip()
				if catalogue_id.lower() in ('', 'none', 'null', 'false', '0'):
					catalogue_id = ''
			else:
				catalogue_id = ''
			if imdb is not None:
				imdb = str(imdb).strip()
				if imdb.lower() in ('', 'none', 'null', 'false', '0'):
					imdb = ''
			else:
				imdb = ''
			tmdb = data.get('tmdb')
			if tmdb is not None:
				tmdb = str(tmdb).strip()
				if tmdb.lower() in ('', 'none', 'null', 'false', '0'):
					tmdb = ''
			else:
				tmdb = ''
			use_catalogue_id = self._catalogue_id_belongs_to_this_manifest(catalogue_id, catalogue_base_url, catalogue_video_id)
			if 'tvshowtitle' in data:
				season = data['season']
				episode = data['episode']
				hdlr = 'S%02dE%02d' % (int(season), int(episode))
				# Les catalogues Stremio peuvent utiliser leur propre identifiant interne
				# pour les épisodes. Si cet identifiant est présent, il doit être
				# transmis au manifest avant l'IMDb, comme le fait Stremio.
				# L'identifiant doit être encodé entièrement, car certains IDs internes
				# contiennent des ':'; sinon le manifest reçoit un ID tronqué.
				request_id = catalogue_id if use_catalogue_id else self._standard_request_id(imdb, tmdb)
				if not request_id: return sources
				if catalogue_video_id and use_catalogue_id:
					url = '%s/stream/series/%s.json' % (self.base_link, quote(str(request_id), safe=''))
				else:
					url = '%s%s' % (self.base_link, self.tvSearch_link % (quote(str(request_id), safe=''), season, episode))
			else:
				hdlr = year
				request_id = catalogue_id if use_catalogue_id else self._standard_request_id(imdb, tmdb)
				if not request_id: return sources
				url = '%s%s' % (self.base_link, self.movieSearch_link % quote(str(request_id), safe=''))
			# log_utils.log('url = %s' % url)
			if 'timeout' in data: self.timeout = int(data['timeout'])
			results = client.request(url, timeout=self.timeout)
			files = jsloads(results).get('streams') or [] if results else []
			# Les résultats CUSTOM_STREMIO2 doivent respecter la configuration du manifest.
			# Aucun filtre de langue alkoFlix n'est appliqué ici.
		except:
			source_utils.scraper_error('CUSTOM_STREMIO2')
			return sources

		for file in files:
			try:
				package, episode_start = None, 0
				meta = file.get('meta') or {}
				behavior = file.get('behaviorHints') or {}
				direct_url = file.get('url')
				info_hash = self._hash_from_stream(file, meta, behavior, direct_url)
				raw_info_hash = info_hash
				source_type = (meta.get('type') or file.get('type') or '').lower()
				if source_type == 'nzb': source_type = 'usenet'
				if source_type == 'http': source_type = 'direct'
				is_magnet_url = bool(direct_url) and str(direct_url).lower().startswith('magnet:')
				if is_magnet_url:
					source_type = 'torrent'
				if not source_type:
					source_type = 'torrent' if info_hash else 'direct'
				# Plusieurs manifests Stremio fournissent une URL technique en plus du hash.
				# Si un hash existe, le résultat reste un magnet/torrent pour le sélecteur
				# et le débrideur. Les vrais liens directs restent directs.
				use_direct = bool(direct_url) and not is_magnet_url and not info_hash
				if not info_hash and not use_direct and direct_url:
					info_hash = self._hash_from_stream(file, meta, behavior, direct_url)
				info_hash = None if use_direct else info_hash

				release_title = self._release_title_from_stream(file, meta, behavior)
				name = source_utils.clean_name(release_title)
				if not name: continue

				if total_seasons is not None:
					valid, last_season = source_utils.filter_show_pack(title, aliases, imdb, year, season, name, total_seasons)
					if valid:
						package = 'show'
					else:
						valid, episode_start, episode_end = source_utils.filter_season_pack(title, aliases, year, season, name)
						if valid: package = 'season'
				name_info = source_utils.info_from_name(name, title, year, hdlr, episode_title)
				name_info = re.sub(r'\b4khdhub(?:\.com)?\b', '', name_info, flags=re.I)
				name_info = re.sub(r'\.{2,}', '.', name_info)
				language = source_utils.release_language(name_info)

				if use_direct:
					if not direct_url: continue
					url = direct_url
				else:
					if not info_hash: continue
					url = 'magnet:?xt=urn:btih:%s&dn=%s' % (info_hash, name)

				seeders = 0

				quality, info = source_utils.get_release_quality(name_info, url)
				dsize = 0
				try:
					size = self._size_from_stream(file, meta, behavior)
					if size:
						dsize, isize = source_utils._size(size)
						if isize: info.insert(0, isize)
				except:
					dsize = 0
				info = ' | '.join(info)

				if use_direct:
					item = {
						'source': self.display_name or source_type or 'direct', 'language': language, 'direct': True, 'debridonly': False,
						'provider': 'custom_stremio2', 'hash': raw_info_hash, 'url': url, 'name': name, 'name_info': name_info,
						'quality': quality, 'info': info, 'size': dsize, 'seeders': seeders,
						'custom_stremio2_direct': True, 'debrid': self._debrid_from_direct_url(url), 'provider_name': self.display_name, 'source_site': self.display_name,
						'bypass_filter': True, 'true_size': True
					}
				else:
					item = {
						'source': 'torrent', 'language': language, 'direct': False, 'debridonly': True,
						'provider': 'custom_stremio2', 'hash': info_hash, 'url': url, 'name': name, 'name_info': name_info,
						'quality': quality, 'info': info, 'size': dsize, 'seeders': seeders, 'provider_name': self.display_name, 'source_site': self.display_name,
						'bypass_filter': True, 'true_size': True
					}
				tracker = self._tracker_from_stream(file, meta, behavior)
				if source_type == 'usenet' and not tracker: tracker = 'usenet'
				if tracker and str(tracker).strip().lower() not in ('n/a', 'na'):
					item['tracker'] = tracker
					item['source_site'] = tracker
				if package: item['package'] = package
				if package == 'show': item.update({'last_season': last_season})
				if episode_start: item.update({'episode_start': episode_start, 'episode_end': episode_end}) # for partial season packs
				sources_append(item)
			except:
				source_utils.scraper_error('CUSTOM_STREMIO2')
		return sources
