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

import json
import re
from urllib.parse import quote, urlparse

from alko import client
from alko import source_utils


class source:
	timeout = 10
	priority = 5
	pack_capable = False
	hasMovies = True
	hasEpisodes = True

	def __init__(self):
		self.language = ['en', 'fr']
		# Repli volontaire vers la logique legacy v1.2.4 / TorrentsDB.
		# Le manifest nyaa-scraper retourne des 400/timeout sur plusieurs IDs Stremio/Kodi.
		self.base_link = 'https://torrentsdb.com'
		self.movieSearch_link = '/stream/movie/%s.json'
		self.tvSearch_link = '/stream/series/%s:%s:%s.json'
		self.min_seeders = 0

	def _valid_id(self, value):
		try:
			value = str(value or '').strip()
			if value.lower() in ('', 'none', 'null', 'false', '0'): return ''
			return value
		except:
			return ''

	def _legacy_id_candidates(self, data, is_episode=False):
		candidates = []
		append = candidates.append
		imdb = self._valid_id(data.get('imdb'))
		catalogue_id = self._valid_id(data.get('catalogue_id'))
		tvdb = self._valid_id(data.get('tvdb') or data.get('tvdb_id'))
		tmdb = self._valid_id(data.get('tmdb') or data.get('tmdb_id'))

		# TorrentsDB répond d'abord aux IDs IMDb, comme la source historique.
		if imdb: append(imdb)
		if catalogue_id: append(catalogue_id)
		if is_episode and tvdb:
			append(tvdb if tvdb.startswith('tvdb:') else 'tvdb:%s' % tvdb)
		if tmdb:
			if tmdb.startswith(('tmdb:', 'tmdbmovie:')): append(tmdb)
			else:
				if is_episode: append('tmdb:%s' % tmdb)
				else: append('tmdbmovie:%s' % tmdb)

		result = []
		for item in candidates:
			if item and item not in result: result.append(item)
		return result

	def _stream_urls(self, data):
		urls = []
		append = urls.append
		try:
			is_episode = 'tvshowtitle' in data
			ids = self._legacy_id_candidates(data, is_episode=is_episode)
			if not ids: return []
			if is_episode:
				season, episode = data['season'], data['episode']
				for video_id in ids:
					video_id = quote(str(video_id), safe='')
					append('%s%s' % (self.base_link, self.tvSearch_link % (video_id, season, episode)))
			else:
				for video_id in ids:
					video_id = quote(str(video_id), safe='')
					append('%s%s' % (self.base_link, self.movieSearch_link % video_id))
		except:
			return []

		result = []
		for url in urls:
			if url and url not in result: result.append(url)
		return result

	def _get_streams(self, url):
		try:
			results = client.request(url, timeout=self.timeout)
			if not results: return []
			data = json.loads(results)
			streams = data.get('streams', []) if isinstance(data, dict) else []
			return streams if isinstance(streams, list) else []
		except:
			source_utils.scraper_error('NYAA')
			return []

	def _stream_text(self, stream, meta=None, behavior=None):
		meta = meta or stream.get('meta') or {}
		behavior = behavior or stream.get('behaviorHints') or {}
		parts = []
		for value in (
			behavior.get('filename'), stream.get('filename'), stream.get('folderName'),
			stream.get('name'), stream.get('title'), stream.get('description'),
			meta.get('title'), meta.get('name'), meta.get('originalTitle')
		):
			try:
				if value: parts.append(str(value))
			except:
				pass
		return '\n'.join(parts)

	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 {}
		for value in (
			behavior.get('filename'), stream.get('filename'), stream.get('folderName'),
			meta.get('originalTitle'), meta.get('title'), meta.get('name'),
			stream.get('title'), stream.get('name'), stream.get('description')
		):
			try:
				if not value: continue
				value = str(value).replace('\u2508\u27a4', '\n').strip()
				match = re.search(r'([A-Za-z0-9\[\(].*?\.(?:mkv|mp4|avi|m2ts|m4v|ts))', value, re.I)
				if match: return match.group(1).strip()
				line = value.split('\n')[0].strip()
				if line: return line
			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
				match = re.search(r'([A-Fa-f0-9]{40})', str(value))
				if match: return match.group(1).lower()
			except:
				pass
		for text in (direct_url, stream.get('url'), stream.get('externalUrl'), self._stream_text(stream, meta, behavior)):
			try:
				if not text: continue
				match = re.search(r'btih:([A-Fa-f0-9]{40})', str(text), re.I)
				if match: return match.group(1).lower()
				match = re.search(r'\b([A-Fa-f0-9]{40})\b', str(text))
				if match: return match.group(1).lower()
			except:
				pass
		return None

	def _size_from_stream(self, stream, meta=None, behavior=None):
		meta = meta or stream.get('meta') or {}
		behavior = behavior or stream.get('behaviorHints') or {}
		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 '%.2f GB' % (value / 1073741824)
			except:
				pass
		text = self._stream_text(stream, meta, behavior)
		try:
			match = re.search(r'(?<![A-Za-z0-9])([0-9]+(?:[\.,][0-9]+)?)\s*(GB|GiB|MB|MiB)\b', 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 _seeders_from_stream(self, stream, meta=None, behavior=None):
		meta = meta or stream.get('meta') or {}
		behavior = behavior or stream.get('behaviorHints') or {}
		for value in (
			stream.get('seeders'), stream.get('seeds'), stream.get('seed'),
			meta.get('seeders'), meta.get('seeds'), behavior.get('seeders'), behavior.get('seeds')
		):
			try:
				if value in ('', None): continue
				return int(value)
			except:
				pass
		text = self._stream_text(stream, meta, behavior)
		for pattern in (r'👤\s*(\d+)', r'\bseed(?:er)?s?\s*[:=]?\s*(\d+)\b', r'\bS\s*[:=]\s*(\d+)\b'):
			try:
				match = re.search(pattern, text, re.I)
				if match: return int(match.group(1))
			except:
				pass
		return 0

	def _is_vostfr(self, text):
		try:
			return bool(re.search(r'(?<![a-z0-9])vo\s*[.-]?\s*stfr(?![a-z0-9])|(?<![a-z0-9])vostfr(?![a-z0-9])', text, re.I))
		except:
			return False

	def _is_multi(self, text):
		try:
			return bool(re.search(r'(?<![a-z0-9])multi(?![a-z0-9])|(?<![a-z0-9])multi[ ._-]', text, re.I))
		except:
			return False

	def _is_explicit_fr_audio(self, text):
		try:
			# Même famille de tags FR que les autres sources, avec quelques variantes
			# typiques des releases anime.
			return bool(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)(?![a-z0-9])', text, re.I))
		except:
			return False

	def _is_english(self, text):
		try:
			return bool(re.search(r'(?<![a-z0-9])(?:english|eng|en|vosta|eng[ ._-]?sub|en[ ._-]?sub)(?![a-z0-9])', text, re.I))
		except:
			return False

	def _language_rank_from_text(self, text):
		try:
			# Pour Nyaa/anime, MULTI peut contenir une piste FR et doit rester avec
			# les résultats prioritaires. VOSTFR reste juste après.
			if self._is_explicit_fr_audio(text) or self._is_multi(text): return 0
			if self._is_vostfr(text): return 1
		except:
			pass
		return 2

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

	def _language_tags_from_text(self, text):
		tags = []
		try:
			# Tags FR repris dans l'esprit de source_utils.FRENCH_CHECK, mais avec
			# des regex à frontières propres pour éviter les faux positifs.
			checks = (
				('TRUEFRENCH', r'(?<![a-z0-9])true[ ._-]?french(?![a-z0-9])'),
				('VF-FR', r'(?<![a-z0-9])vf[ ._-]?fr(?![a-z0-9])'),
				('VF-FQ', r'(?<![a-z0-9])vf[ ._-]?fq(?![a-z0-9])'),
				('VFI', r'(?<![a-z0-9])vfi(?![a-z0-9])|(?<![a-z0-9])vf[ ._-]?i(?![a-z0-9])'),
				('VFF', r'(?<![a-z0-9])vff(?![a-z0-9])'),
				('VFQ', r'(?<![a-z0-9])vfq(?![a-z0-9])'),
				('VF2', r'(?<![a-z0-9])vf2(?![a-z0-9])'),
				('VOF', r'(?<![a-z0-9])vof(?![a-z0-9])'),
				('VF', r'(?<![a-z0-9])vf(?![a-z0-9])'),
				('FRENCH', r'(?<![a-z0-9])french(?![a-z0-9])'),
				('FR', r'(?<![a-z0-9])(?:fr|fra|fre|frn)(?![a-z0-9])'),
				('VOSTFR', r'(?<![a-z0-9])vo[ ._-]?stfr(?![a-z0-9])|(?<![a-z0-9])vostfr(?![a-z0-9])'),
				('VOSTA', r'(?<![a-z0-9])vosta(?![a-z0-9])'),
				('ENG-SUB', r'(?<![a-z0-9])eng[ ._-]?sub(?:s|titles?)?(?![a-z0-9])|(?<![a-z0-9])en[ ._-]?sub(?:s|titles?)?(?![a-z0-9])'),
				('ENG', r'(?<![a-z0-9])(?:english|eng|en)(?![a-z0-9])')
			)
			for label, pattern in checks:
				if label in tags: continue
				if label == 'FR' and any(tag in tags for tag in ('TRUEFRENCH', 'VF-FR', 'VF-FQ', 'VFI', 'VFF', 'VFQ', 'VF2', 'VOF', 'VF', 'FRENCH', 'VOSTFR')): continue
				if label == 'ENG' and any(tag in tags for tag in ('ENG-SUB', 'VOSTA')): continue
				if re.search(pattern, text, re.I): tags.append(label)
			if self._is_multi(text) and 'MULTI' not in tags: tags.append('MULTI')
		except:
			pass
		return tags

	def _add_fr_info(self, info, name_info, stream_text):
		try:
			text = '%s %s' % (name_info, stream_text)
			existing = [str(i).upper() for i in info]
			for tag in reversed(self._language_tags_from_text(text)):
				if tag not in existing:
					info.insert(0, tag)
					existing.append(tag)
		except:
			pass
		return info

	def _direct_playable_url(self, direct_url, info_hash=None):
		try:
			if not direct_url: return False
			direct_url = str(direct_url)
			if direct_url.lower().startswith('magnet:'): return False
			parsed = urlparse(direct_url)
			host = (parsed.netloc or '').lower()
			if info_hash and (host.endswith('nyaa.si') or host.endswith('animetosho.org') or host.endswith('torrentsdb.com')):
				return False
			return bool(parsed.scheme in ('http', 'https'))
		except:
			return False

	def sources(self, data, hostDict):
		sources = []
		if not data: 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('/', ' ')
			aliases = data.get('aliases', [])
			episode_title = data['title'] if 'tvshowtitle' in data else None
			total_seasons = data.get('total_seasons') if 'tvshowtitle' in data else None
			year = str(data['year'])
			imdb = self._valid_id(data.get('imdb'))
			if 'tvshowtitle' in data:
				season = data['season']
				episode = data['episode']
				hdlr = 'S%02dE%02d' % (int(season), int(episode))
			else:
				season = episode = None
				hdlr = year
			if 'timeout' in data: self.timeout = max(10, int(data['timeout']))
			urls = self._stream_urls(data)
			if not urls: return sources
		except:
			source_utils.scraper_error('NYAA')
			return sources

		seen = set()
		for url in urls:
			for stream in self._get_streams(url):
				try:
					if not isinstance(stream, dict): continue
					meta = stream.get('meta') or {}
					behavior = stream.get('behaviorHints') or {}
					direct_url = stream.get('url') or stream.get('externalUrl') or ''
					info_hash = self._hash_from_stream(stream, meta, behavior, direct_url)
					release_title = self._release_title_from_stream(stream, meta, behavior)
					name = source_utils.clean_name(release_title)
					if not name: continue

					package, episode_start = None, 0
					if not source_utils.check_title(title, aliases, name, hdlr, year):
						if total_seasons is None: continue
						valid, last_season = source_utils.filter_show_pack(title, aliases, imdb, year, season, name, total_seasons)
						if not valid:
							valid, episode_start, episode_end = source_utils.filter_season_pack(title, aliases, year, season, name)
							if not valid: continue
							package = 'season'
						else:
							package = 'show'

					stream_text = self._stream_text(stream, meta, behavior)
					name_info = source_utils.info_from_name(name, title, year, hdlr, episode_title)
					language_text = '%s %s' % (name_info, stream_text)
					language = 'fr' if self._language_rank_from_text(language_text) in (0, 1) else 'en'

					use_direct = self._direct_playable_url(direct_url, info_hash)
					if use_direct:
						final_url = str(direct_url)
						unique_key = final_url
					else:
						if not info_hash: continue
						final_url = 'magnet:?xt=urn:btih:%s&dn=%s' % (info_hash, quote(name))
						unique_key = info_hash
					if unique_key in seen: continue
					seen.add(unique_key)

					seeders = self._seeders_from_stream(stream, meta, behavior)
					if self.min_seeders > seeders: continue

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

					item = {
						'provider': 'nyaa', 'provider_name': 'Nyaa',
						'source': 'torrent' if not use_direct else 'direct',
						'seeders': seeders, 'hash': info_hash, 'name': name, 'name_info': name_info,
						'quality': quality, 'language': language, 'url': final_url, 'info': info,
						'direct': use_direct, 'debridonly': not use_direct, 'size': dsize,
						'true_size': bool(dsize), 'bypass_filter': True
					}
					if package: item.update({'package': package, 'true_size': True})
					if package == 'show': item.update({'last_season': last_season})
					if episode_start: item.update({'episode_start': episode_start, 'episode_end': episode_end})
					sources_append(item)
				except:
					source_utils.scraper_error('NYAA')
		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
