# -*- coding: utf-8 -*-
# plugin.video.alkoflix
# resources/lib/magneto/ygg.py

import re
import xml.etree.ElementTree as ET
from concurrent.futures import ThreadPoolExecutor as TPE
from urllib.parse import quote

import requests

from alko import source_utils
from modules.source_objects import _is_blocked_file_source


class source:
	timeout = 6
	priority = 4
	pack_capable = True
	hasMovies = True
	hasEpisodes = True

	def __init__(self):
		self.language = ['fr', 'en']
		# Relays U2P/YGG exposés en Torznab via /torznab.
		# On garde uniquement les endpoints HTTP/Torznab connus pour Kodi.
		# Les adresses wss:// et .onion ne sont pas utilisées ici.
		# u2prelais.eliottb.dev retiré : certificat SSL expiré.
		self.base_links = (
			'https://relay.ygg.gratis/torznab',
			'https://u2p.anhkagi.net/torznab'
		)
		self.base_link = self.base_links[0]
		self.dead_base_links = set()
		self._session = requests.Session()
		self._headers = {'User-Agent': 'alkoFlix/1.5.10 Kodi'}
		self.min_seeders = 0

	def _safe_int(self, value, default=0):
		try:
			if value in ('', None): return default
			return int(str(value).strip())
		except:
			return default

	def _size_gb(self, value):
		try:
			value = float(value or 0)
			if value <= 0: return 0
			return round(value / 1073741824.0, 2)
		except:
			return 0

	def _hash_from_text(self, value):
		try:
			if not value: return None
			match = re.search(r'btih:([A-Fa-f0-9]{40})', str(value), re.I)
			if match: return match.group(1).lower()
			match = re.search(r'\b([A-Fa-f0-9]{40})\b', str(value), re.I)
			if match: return match.group(1).lower()
		except:
			pass
		return None

	def _make_magnet(self, info_hash, name):
		try:
			if not info_hash: return ''
			return 'magnet:?xt=urn:btih:%s&dn=%s' % (info_hash, quote(name or 'YGG'))
		except:
			return ''

	def _query_titles(self, title, aliases=None, limit=2):
		values, seen = [], set()
		try:
			for item in (aliases or []):
				value = item.get('title') if isinstance(item, dict) else item
				value = str(value or '').strip()
				key = value.casefold()
				if value and key not in seen:
					values.append(value)
					seen.add(key)
		except:
			pass
		try:
			value = str(title or '').strip()
			key = value.casefold()
			if value and key not in seen:
				values.insert(0, value)
				seen.add(key)
		except:
			pass
		return values[:limit]

	def _ordered_base_links(self):
		try:
			available = tuple(i for i in self.base_links if i not in self.dead_base_links)
			if not available:
				self.dead_base_links.clear()
				available = self.base_links
			preferred = self.base_link if self.base_link in available else available[0]
			return (preferred,) + tuple(i for i in available if i != preferred)
		except:
			return self.base_links

	def _mark_base_link_dead(self, base_link):
		try:
			if base_link in self.base_links:
				self.dead_base_links.add(base_link)
				if self.base_link == base_link:
					self.base_link = next((i for i in self.base_links if i not in self.dead_base_links), self.base_links[0])
		except:
			pass

	def _request(self, params):
		try:
			params = dict(params or {})
			request_timeout = max(3, min(int(self.timeout or 6), 6))
			base_links = self._ordered_base_links()
			for index, base_link in enumerate(base_links):
				try:
					response = self._session.get(base_link, params=params, timeout=request_timeout, headers=self._headers)
					if response.status_code != 200:
						self._mark_base_link_dead(base_link)
						continue
					results = self._parse_xml(response.text, log_error=False)
					if results is None:
						self._mark_base_link_dead(base_link)
						continue
					if results:
						self.base_link = base_link
						return results
				except (requests.exceptions.SSLError, requests.exceptions.Timeout, requests.exceptions.ConnectionError):
					self._mark_base_link_dead(base_link)
					if index == len(base_links) - 1:
						source_utils.scraper_error('YGG')
					continue
				except requests.exceptions.RequestException:
					self._mark_base_link_dead(base_link)
					if index == len(base_links) - 1:
						source_utils.scraper_error('YGG')
					continue
				except:
					if index == len(base_links) - 1:
						source_utils.scraper_error('YGG')
					continue
		except:
			source_utils.scraper_error('YGG')
		return []

	def _parse_xml(self, xml_text, log_error=True):
		try:
			root = ET.fromstring(xml_text)
		except:
			if log_error:
				source_utils.scraper_error('YGG')
				return []
			return None

		namespaces = (
			{'torznab': 'http://torznab.com/schemas/2015/feed'},
			{'torznab': 'http://www.newznab.com/DTD/2010/feeds/attributes/'}
		)
		items = root.findall('.//item')
		results = []

		for item in items:
			try:
				title = (item.findtext('title') or '').strip()
				if not title: continue

				size = self._safe_int(item.findtext('size'), 0)
				page_link = (item.findtext('link') or '').strip()
				enclosure = item.find('enclosure')
				enclosure_url = (enclosure.get('url') or '').strip() if enclosure is not None else ''

				info_hash, magnet_url = None, ''
				seeders, leechers = 0, 0

				attrs = []
				for ns in namespaces:
					attrs.extend(item.findall('torznab:attr', ns))

				for attr in attrs:
					name = (attr.get('name') or '').strip().lower()
					value = (attr.get('value') or '').strip()
					if name == 'infohash':
						info_hash = self._hash_from_text(value) or value.lower()
					elif name == 'seeders':
						seeders = self._safe_int(value, 0)
					elif name in ('peers', 'leechers'):
						leechers = self._safe_int(value, 0)
					elif name == 'magneturl':
						magnet_url = value

				candidate_link = magnet_url or enclosure_url or page_link
				if not info_hash:
					info_hash = self._hash_from_text(candidate_link)
				if not info_hash:
					continue

				magnet = magnet_url if magnet_url.startswith('magnet:') else self._make_magnet(info_hash, title)
				if not magnet: continue

				results.append({
					'name': title,
					'url': magnet,
					'hash': info_hash,
					'size_bytes': size,
					'size': self._size_gb(size),
					'seeders': seeders,
					'leechers': leechers
				})
			except:
				source_utils.scraper_error('YGG')

		return results

	def _merge_results(self, results):
		merged, seen = [], set()
		for item in results or []:
			try:
				key = item.get('hash') or item.get('url') or '%s|%s' % (item.get('name'), item.get('size_bytes'))
				if not key or key in seen: continue
				seen.add(key)
				merged.append(item)
			except:
				pass
		return merged

	def _search_one(self, query, search_type):
		query = str(query or '').strip()
		if not query: return []
		items = self._request({'t': search_type, 'q': query})
		# Certains relays Torznab U2P peuvent être plus bavards avec t=search
		# qu'avec movie/tvsearch. On garde ce repli sans modifier l'UX.
		if not items and search_type != 'search':
			items = self._request({'t': 'search', 'q': query})
		return items

	def _search_many(self, queries, search_type):
		results = []
		queries = [str(query or '').strip() for query in (queries or []) if str(query or '').strip()]
		if not queries: return []
		if len(queries) == 1:
			return self._merge_results(self._search_one(queries[0], search_type))
		try:
			workers = max(1, min(3, len(queries)))
			with TPE(max_workers=workers) as executor:
				for items in executor.map(lambda q: self._search_one(q, search_type), queries):
					results.extend(items or [])
		except:
			for query in queries:
				results.extend(self._search_one(query, search_type) or [])
		return self._merge_results(results)

	def _language_rank_from_text(self, text):
		try:
			text = str(text or '')
			if re.search(r'(?<![a-z0-9])(?:true[ ._-]?french|french|vf[ ._-]?fr|vf[ ._-]?fq|vf[ ._-]?i|vfi|vff|vfq|vf2|vof|vf|fr|fra|fre|frn|multi)(?![a-z0-9])', text, re.I): return 0
			if re.search(r'(?<![a-z0-9])vo[ ._-]?stfr(?![a-z0-9])|(?<![a-z0-9])vostfr(?![a-z0-9])|(?<![a-z0-9])subfrench(?![a-z0-9])', text, re.I): return 1
		except:
			pass
		return 2

	def _language_from_text(self, text):
		return 'fr' if self._language_rank_from_text(text) in (0, 1) else 'en'

	def _fr_rank(self, item):
		try:
			return self._language_rank_from_text('%s %s %s' % (item.get('name', ''), item.get('name_info', ''), item.get('info', '')))
		except:
			return 3

	def _result_item(self, raw, data, package=None, episode_start=0, episode_end=0, last_season=None):
		try:
			title = data['tvshowtitle'] if 'tvshowtitle' in data else data['title']
			year = str(data.get('year') or '')
			season = data.get('season') if 'tvshowtitle' in data else None
			episode_title = data.get('title') if 'tvshowtitle' in data else None
			hdlr = 'S%02dE%02d' % (int(data['season']), int(data['episode'])) if 'tvshowtitle' in data and not package else year
			name = source_utils.clean_name(raw.get('name') or '')
			if not name: return None
			if _is_blocked_file_source({'name': name, 'url': raw.get('url'), 'raw': raw}): return None
			pack = package if package in ('season', 'show') else None
			name_info = source_utils.info_from_name(name, title, year, hdlr, episode_title, season=str(season or ''), pack=pack)
			check_foreign_audio = source_utils.check_foreign_audio()
			if source_utils.remove_lang(name_info, check_foreign_audio): return None
			language = self._language_from_text('%s %s' % (name_info, name))
			quality, info = source_utils.get_release_quality(name_info, raw.get('url'))
			if raw.get('size'):
				info.insert(0, '%.2f GB' % raw.get('size'))
			info = ' | '.join(info)
			item = {
				'provider': 'ygg', 'provider_name': 'YGG',
				'source': 'torrent', 'language': language, 'direct': False, 'debridonly': True,
				'hash': raw.get('hash'), 'url': raw.get('url'), 'name': name, 'name_info': name_info,
				'quality': quality, 'info': info, 'size': raw.get('size', 0), 'seeders': raw.get('seeders', 0),
				'tracker': 'YGG', 'true_size': bool(raw.get('size'))
			}
			if package:
				item.update({'package': package, 'true_size': True})
			if package == 'show':
				item.update({'last_season': last_season or data.get('total_seasons') or 0})
			if episode_start:
				item.update({'episode_start': episode_start, 'episode_end': episode_end})
			return item if not _is_blocked_file_source(item) else None
		except:
			source_utils.scraper_error('YGG')
			return None

	def _movie_queries(self, title, aliases, year):
		queries = []
		for query_title in self._query_titles(title, aliases):
			query = '%s %s' % (query_title, year) if year and year != '0' else query_title
			if query not in queries: queries.append(query)
		return queries

	def _episode_queries(self, title, aliases, season, episode):
		queries = []
		for query_title in self._query_titles(title, aliases):
			query = '%s S%02dE%02d' % (query_title, int(season), int(episode))
			if query not in queries: queries.append(query)
		return queries

	def _season_pack_queries(self, title, aliases, season):
		queries = []
		for query_title in self._query_titles(title, aliases):
			for suffix in ('S%02d' % int(season), 'Saison %s' % int(season)):
				query = '%s %s' % (query_title, suffix)
				if query not in queries: queries.append(query)
		return queries[:4]

	def _show_pack_queries(self, title, aliases):
		queries = []
		for query_title in self._query_titles(title, aliases):
			for suffix in ('Complete', 'Intégrale', 'Integrale', 'Toutes saisons'):
				query = '%s %s' % (query_title, suffix)
				if query not in queries: queries.append(query)
		return queries[:4]

	def sources(self, data, hostDict):
		sources = []
		if not data: return sources
		try:
			title = data['tvshowtitle'] if 'tvshowtitle' in data else data['title']
			aliases = data.get('aliases', [])
			year = str(data.get('year') or '')
			if 'timeout' in data: self.timeout = max(4, min(6, int(data['timeout'])))
			if 'tvshowtitle' in data:
				season, episode = data['season'], data['episode']
				hdlr = 'S%02dE%02d' % (int(season), int(episode))
				raw_results = self._search_many(self._episode_queries(title, aliases, season, episode), 'tvsearch')
			else:
				season = episode = None
				hdlr = year
				raw_results = self._search_many(self._movie_queries(title, aliases, year), 'movie')
		except:
			source_utils.scraper_error('YGG')
			return sources

		for raw in raw_results:
			try:
				name = source_utils.clean_name(raw.get('name') or '')
				if not name: continue
				if not source_utils.check_title(title, aliases, name, hdlr, year): continue
				item = self._result_item(raw, data)
				if item: sources.append(item)
			except:
				source_utils.scraper_error('YGG')
		try:
			sources.sort(key=lambda i: (self._fr_rank(i), -int(i.get('seeders', 0) or 0), -float(i.get('size', 0) or 0)))
		except:
			pass
		return sources

	def sources_packs(self, data, hostDict, search_series=False, total_seasons=None):
		sources = []
		if not data or 'tvshowtitle' not in data: return sources
		try:
			title = data['tvshowtitle']
			aliases = data.get('aliases', [])
			year = str(data.get('year') or '')
			season = data['season']
			if 'timeout' in data: self.timeout = max(4, min(6, int(data['timeout'])))
			queries = self._show_pack_queries(title, aliases) if search_series else self._season_pack_queries(title, aliases, season)
			raw_results = self._search_many(queries, 'tvsearch')
		except:
			source_utils.scraper_error('YGG')
			return sources

		for raw in raw_results:
			try:
				name = source_utils.clean_name(raw.get('name') or '')
				if not name: continue
				if search_series:
					valid, last_season = source_utils.filter_show_pack(title, aliases, data.get('imdb'), year, season, name, total_seasons or data.get('total_seasons'))
					if not valid: continue
					item = self._result_item(raw, data, package='show', last_season=last_season)
				else:
					valid, episode_start, episode_end = source_utils.filter_season_pack(title, aliases, year, season, name)
					if not valid: continue
					item = self._result_item(raw, data, package='season', episode_start=episode_start, episode_end=episode_end)
				if item: sources.append(item)
			except:
				source_utils.scraper_error('YGG')
		try:
			sources.sort(key=lambda i: (self._fr_rank(i), -int(i.get('seeders', 0) or 0), -float(i.get('size', 0) or 0)))
		except:
			pass
		return sources
