import json
import re
import traceback
import time
import base64
import xml.etree.ElementTree as ET
from urllib.parse import quote_plus, urlparse, parse_qsl
from datetime import datetime, date
from typing import Union, Optional
from concurrent.futures import ThreadPoolExecutor
import requests
from requests import Response
from bs4 import BeautifulSoup
from tzlocal import get_localzone
import pytz
import xbmc
import xbmcgui
import xbmcplugin
import xbmcvfs
import variables as var
from models import Item
from tor_manager import TorManager, get_tor_manager

# Singleton condiviso — NON crea una nuova istanza di TorManager.
# variables.py l'ha gia' creata; qui la riusiamo.
_tor = get_tor_manager()


class proxydt(datetime):

    @classmethod
    def strptime(cls, date_string, _format):
        return datetime(*(time.strptime(date_string, _format)[:6]))


datetime = proxydt


# ─────────────────────────────────────────────
#  UTILITIES
# ─────────────────────────────────────────────

def log(message: str):
    xbmc.log(str(message), xbmc.LOGINFO)


def container_refresh():
    xbmc.executebuiltin('Container.Refresh')


def get(url: str, referer: str = '', headers=None, timeout: int = 10) -> '_FakeResponse':
    """
    HTTP GET via Tor con fallback automatico a bypass (DoH + proxy integrati).

    Ordine tentativi:
      1. TorManager (Tor bundled su Windows/Linux, proxy utente su Android)
      2. TorManager con http:// se https:// fallisce
      3. bypass.py (DoH + proxy integrati) — per Android senza Tor
    """
    req_headers = dict(var.headers) if headers is None else dict(headers)
    if referer:
        req_headers['Referer'] = req_headers['Origin'] = referer

    # Tentativo 1: via TorManager
    html = _tor.get(url, headers=req_headers, timeout=timeout)
    if html is not None:
        return _FakeResponse(html, url)

    # Tentativo 2: http:// se era https://
    if url.startswith('https://'):
        http_url = 'http://' + url[len('https://'):]
        html = _tor.get(http_url, headers=req_headers, timeout=timeout)
        if html is not None:
            return _FakeResponse(html, http_url)

    # Tentativo 3: bypass automatico (DoH + proxy integrati)
    # Su Android senza Tor i server bloccano la connessione diretta.
    # bypass.py prova DoH per aggirare blocchi DNS, poi proxy integrati.
    try:
        from bypass import get_with_bypass
        log(f'[torproxy] get() TorManager fallito – provo bypass per: {url}')
        html = get_with_bypass(url, headers=req_headers, timeout=timeout)
        if html is not None:
            log(f'[torproxy] bypass riuscito per: {url}')
            return _FakeResponse(html, url)
    except Exception as e:
        log(f'[torproxy] bypass errore: {e}')

    raise ConnectionError(f'[torproxy] get() fallito per: {url}')


class _FakeResponse:
    """Adattatore minimale che rende il testo restituito da TorManager
    compatibile con l'interfaccia requests.Response usata nel codice."""

    def __init__(self, text: str, url: str):
        self.text = text
        self.url = url
        self.status_code = 200

    def json(self):
        return json.loads(self.text)


def get_soup(response: str) -> BeautifulSoup:
    return BeautifulSoup(response, 'html.parser')


def ok_dialog(text: str):
    xbmcgui.Dialog().ok(var.addon_name, text)


def write_file(file_path, string):
    with open(file_path, 'w', encoding='utf-8', errors='ignore') as f:
        f.write(string)


def read_file(file_path):
    with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
        return f.read()


# ─────────────────────────────────────────────
#  LISTITEM / UI
# ─────────────────────────────────────────────

def set_info(liz: xbmcgui.ListItem, infolabels: dict, cast: list = None):
    cast = cast or []
    i = liz.getVideoInfoTag()
    i.setMediaType(infolabels.get('mediatype', 'video'))
    i.setTitle(infolabels.get('title', 'Unknown'))
    i.setPlot(infolabels.get('plot', infolabels.get('title', '')))
    i.setTagLine(infolabels.get('tagline', ''))
    i.setPremiered(infolabels.get('premiered', ''))
    i.setGenres(infolabels.get('genre', []))
    i.setMpaa(infolabels.get('mpaa', ''))
    i.setDirectors(infolabels.get('director', []))
    i.setWriters(infolabels.get('writer', []))
    i.setRating(infolabels.get('rating', 0))
    i.setVotes(infolabels.get('votes', 0))
    i.setStudios(infolabels.get('studio', []))
    i.setCountries(infolabels.get('country', []))
    i.setSet(infolabels.get('set', ''))
    i.setTvShowStatus(infolabels.get('status', ''))
    i.setDuration(infolabels.get('duration', 0))
    i.setTrailer(infolabels.get('trailer', ''))
    cast_list = [
        xbmc.Actor(
            name=actor.get('name', ''),
            role=actor.get('role', ''),
            thumbnail=actor.get('thumbnail', '')
        )
        for actor in cast
    ]
    i.setCast(cast_list)


def create_listitem(item: Union[Item, dict]):
    if isinstance(item, dict):
        item = Item(**item)
    is_folder = item.type == 'dir'
    title = item.title
    thumbnail = item.thumbnail
    fanart = item.fanart
    contextmenu = item.contextmenu or []
    is_media = item.is_media
    is_search = item.is_search
    list_item = xbmcgui.ListItem(label=title)
    list_item.setArt({'thumb': thumbnail, 'icon': thumbnail, 'poster': thumbnail, 'fanart': fanart})
    infolabels = {'mediatype': 'video', 'title': title, 'plot': title}
    set_info(list_item, infolabels)
    if not is_folder and is_media and not is_search:
        list_item.setProperty('IsPlayable', 'true')
    list_item.addContextMenuItems(contextmenu)
    plugin_url = f'{var.plugin_url}?{item.url_encode()}'
    xbmcplugin.addDirectoryItem(var.handle, plugin_url, list_item, is_folder)


def get_multilink(lists):
    """Show dialog to pick among multiple stream links. Returns chosen URL."""
    if not lists:
        ok_dialog('No links were found.')
        var.system_exit()
    if len(lists) == 1:
        return lists[0][1]   # BUG FIX: was lists[1]
    labels = [l[0] for l in lists]
    links = [l[1] for l in lists]
    ret = xbmcgui.Dialog().select('Choose a Link', labels)
    if ret == -1:
        var.system_exit()
    return links[ret]


# ─────────────────────────────────────────────
#  CHANNELS
# ─────────────────────────────────────────────

def fetch_channels():
    """
    Scarica la lista canali via Tor.
    Prova prima l'endpoint JSON/API; se 404 o vuoto usa la pagina HTML 24-7.
    I link vengono sempre normalizzati a /stream/stream-{id}.php
    """
    item_list = []

    # ── Tentativo 1: endpoint API (potrebbe essere 404) ──────────────────
    try:
        response = get(var.channels_url)
        soup = get_soup(response.text)
        for card in soup.select('.card'):
            title = card.div.text.strip() if card.div else card.get_text(strip=True)
            href = card.get('href', '')
            if not href:
                continue
            full = f"{var.base_url}{href}" if href.startswith('/') else href
            try:
                channel_id = dict(parse_qsl(urlparse(full).query))['id']
                link = f'/stream/stream-{channel_id}.php'
                item_list.append({'title': title, 'link': link})
            except Exception:
                pass
    except Exception as e:
        log(f'fetch_channels API failed: {e}')

    if item_list:
        return item_list

    # ── Tentativo 2: pagina HTML 24-7-channels.php ───────────────────────
    html_url = f'{var.base_url}/24-7-channels.php'
    try:
        response = get(html_url)
        soup = get_soup(response.text)

        # Struttura con .card (layout a griglia)
        for card in soup.select('.card'):
            title = card.get_text(strip=True)
            href = card.get('href', '')
            if not href:
                continue
            full = f"{var.base_url}{href}" if href.startswith('/') else href
            try:
                channel_id = dict(parse_qsl(urlparse(full).query))['id']
                item_list.append({'title': title, 'link': f'/stream/stream-{channel_id}.php'})
            except Exception:
                pass

        # Struttura alternativa: <a href="/watch.php?id=X">Nome</a>
        if not item_list:
            seen = set()
            for a in soup.find_all('a', href=True):
                href = a['href']
                if 'watch.php' not in href and 'id=' not in href:
                    continue
                title = a.get_text(strip=True)
                if not title:
                    continue
                try:
                    full = f"{var.base_url}{href}" if href.startswith('/') else href
                    channel_id = dict(parse_qsl(urlparse(full).query))['id']
                    if channel_id in seen:
                        continue
                    seen.add(channel_id)
                    item_list.append({'title': title, 'link': f'/stream/stream-{channel_id}.php'})
                except Exception:
                    pass

        log(f'fetch_channels HTML: trovati {len(item_list)} canali')
    except Exception as e:
        log(f'fetch_channels HTML failed: {e}')

    # ── Tentativo 3: backup locale ────────────────────────────────────────
    if not item_list:
        log('fetch_channels: uso backup locale')
        try:
            item_list = json.loads(read_file(var.ch_bak_path))
        except Exception:
            item_list = []

    return item_list


def write_channels():
    if not xbmcvfs.exists(var.profile_path):
        xbmcvfs.mkdirs(var.profile_path)
    items = fetch_channels()
    write_file(var.ch_path, json.dumps(items))


def read_channels():
    if not xbmcvfs.exists(var.ch_path):
        write_channels()
    try:
        data = json.loads(read_file(var.ch_path))
    except Exception:
        data = []
    if not data:          # cache vuota o corrotta → ri-scarica
        write_channels()
        try:
            data = json.loads(read_file(var.ch_path))
        except Exception:
            data = []
    return data


# ─────────────────────────────────────────────
#  SCHEDULE
# ─────────────────────────────────────────────

def fetch_schedule():
    schedule_dict = {}
    try:
        response = get(var.schedule_url)
        soup = get_soup(response.text)
        for schedule in soup.select('.schedule__day'):
            if categories := schedule.find_all(class_='schedule__category is-expanded'):
                s_date = schedule.select_one('.schedule__dayTitle').get_text(strip=True)
                s_date = s_date.split(' -', 1)[0].strip()
                dict_date = schedule_dict[s_date] = {}
            else:
                continue
            for cat in categories:
                cat_name = cat.select_one('.card__meta').get_text(strip=True)
                events = dict_date[cat_name] = []
                for event in cat.select('.schedule__event'):
                    e_time = event.select_one('.schedule__time').get_text(strip=True)
                    title = event.select_one('.schedule__eventTitle').get_text(strip=True)
                    channels = []
                    for channel in event.select_one('.schedule__channels').find_all('a'):
                        link = f"{var.base_url}{channel['href']}"
                        channel_id = dict(parse_qsl(urlparse(link).query))['id']
                        channels.append({
                            'channel_name': channel.get_text(strip=True),
                            'channel_id': channel_id
                        })
                    events.append({
                        'time': e_time,
                        'event': title,
                        'channels': channels,
                        'channels2': []
                    })
    except Exception as e:
        log(f'fetch_schedule failed: {e}')
    return json.dumps(schedule_dict, indent=2)


def write_schedule():
    if not xbmcvfs.exists(var.profile_path):
        xbmcvfs.mkdirs(var.profile_path)
    write_file(var.schedule_path, fetch_schedule())


def read_schedule() -> dict:
    if not xbmcvfs.exists(var.schedule_path):
        write_schedule()
    try:
        return json.loads(read_file(var.schedule_path))
    except Exception:
        return {}


def write_cat_schedule(string):
    write_file(var.cat_schedule_path, string)


def read_cat_schedule() -> list:
    return json.loads(read_file(var.cat_schedule_path))


def get_schedule_and_channels():
    with ThreadPoolExecutor() as executor:
        executor.submit(write_schedule)
        executor.submit(write_channels)


def convert_utc_time_to_local(utc_time_str):
    today = date.today()
    datetime_str = f"{today} {utc_time_str}"
    utc_datetime = datetime.strptime(datetime_str, "%Y-%m-%d %H:%M")
    utc_datetime = utc_datetime.replace(tzinfo=pytz.utc)
    local_tz = get_localzone()
    local_time = utc_datetime.astimezone(local_tz)
    return local_time.strftime("%I:%M %p").lstrip('0')


# ─────────────────────────────────────────────
#  FAVOURITES
# ─────────────────────────────────────────────

def read_favourites() -> list:
    if not xbmcvfs.exists(var.fav_path):
        write_file(var.fav_path, json.dumps([]))
    favourites = json.loads(read_file(var.fav_path))
    if not favourites and xbmcvfs.exists(var.fav_old_path):
        old_favs = json.loads(read_file(var.fav_old_path))
        if old_favs:
            write_file(var.fav_path, json.dumps(old_favs))
            return old_favs
    return favourites


def write_favourite(title, link):
    if not xbmcvfs.exists(var.profile_path):
        xbmcvfs.mkdirs(var.profile_path)
    items = read_favourites()
    if link not in str(items):
        items.append([title, link])
    write_file(var.fav_path, json.dumps(items))


# ─────────────────────────────────────────────
#  SEARCH
# ─────────────────────────────────────────────

def get_match_links(match):
    item = {}
    schedule = read_cat_schedule()
    for event in schedule:
        if event['event'] == match:
            item = event
            break
    links = []
    for channel in item.get('channels', []):
        if isinstance(channel, dict):
            links.append([
                channel.get('channel_name'),
                f"/stream/stream-{channel.get('channel_id')}.php"
            ])
    for channel in item.get('channels2', []):
        if isinstance(channel, dict):
            links.append([
                channel.get('channel_name'),
                f"/stream/bet.php?id=bet{channel.get('channel_id')}"
            ])
    return links


def get_search_results():
    stopwords = {
        "a", "an", "and", "are", "as", "at", "be", "but", "by",
        "for", "if", "in", "into", "is", "it", "no", "not", "of",
        "on", "or", "such", "that", "the", "their", "then", "there",
        "these", "they", "this", "to", "was", "will", "with"
    }
    query = xbmcgui.Dialog().input('Enter search query:')
    if not query:
        var.system_exit()
    keywords = [k.lower() for k in query.split() if k.lower() not in stopwords]
    schedule = read_schedule()
    results = {'events': [], 'channels': []}
    events = results['events']
    for key in schedule:
        for cat in schedule[key]:
            for event in schedule[key][cat]:
                if any(kw in event['event'].lower() for kw in keywords) and event not in events:
                    events.append(event)
    ch_list = read_channels()
    channels = results['channels']
    for channel in ch_list:
        if any(kw in channel['title'].lower() for kw in keywords) and channel not in channels:
            channels.append(channel)
    return results


# ─────────────────────────────────────────────
#  STREAM RESOLUTION  (delegata a daddyhd.py)
# ─────────────────────────────────────────────

def resolve_link(url: str):
    """Stub di retrocompatibilità: delega a resources.resolvers.daddyhd."""
    from resources.resolvers.daddyhd import resolve_link as _rl
    return _rl(url)



# ══════════════════════════════════════════════════════════════════════
# SEZIONE .ONION — menu remoto, sottomenu, regex, ricerca
# ══════════════════════════════════════════════════════════════════════

# Proxy SOCKS5 per connessioni .onion (socks5h = DNS dentro Tor)
_ONION_PROXY = {
    'http' : 'socks5h://127.0.0.1:9050',
    'https': 'socks5h://127.0.0.1:9050',
}

# URL XML remoto principale (menu Tor Network — modificabile dal server)
_ONION_HOME_XML = 'https://thegroove360.org/xml/hometor.xml'

# URL base per i sottomenu XML relativi
_ONION_XML_BASE = 'https://thegroove360.org/xml'

# ── Fallback hardcoded usato SOLO se hometor.xml non è raggiungibile ──
# Formato esteso: ogni voce può avere tipo browse/search/submenu/regex/play/custom
_ONION_SITES_FALLBACK = [
    {
        'title'       : '[COLOR orange][B]The Pirate Bay[/B][/COLOR]',
        'url'         : 'https://tpb.party/',
        'thumb'       : 'https://thegroove360.org/scripters/Torrent/img/allstars.png',
        'plot'        : 'Il più grande indice di torrent al mondo.',
        'type'        : 'browse',
        'search_param': 'q',
        'search_path' : '/search',
    },
    {
        'title'       : '[COLOR deepskyblue][B]Torch Search[/B][/COLOR]',
        'url'         : 'http://torchdeedp3i2jigzjdmfpn5ttjhthh5wbmda2rr3jvqjg5p77c54dqd.onion',
        'thumb'       : 'https://thegroove360.org/scripters/Torrent/img/allstars.png',
        'plot'        : 'Motore di ricerca per la rete Tor.',
        'type'        : 'search',
        'search_param': 'search',
        'search_path' : '/search',
    },
    {
        'title': '[COLOR deepskyblue]🧅  Inserisci URL .onion personalizzato[/COLOR]',
        'url'  : 'CUSTOM',
        'type' : 'custom',
        'thumb': '',
        'plot' : '',
    },
]


# ── FORMATO hometor.xml (e ogni file sottomenu sul server) ─────────────
#
#  <menu>
#    <item>
#      <title>[COLOR orange][B]Nome[/B][/COLOR]</title>
#      <url>http://sito.onion</url>
#      <thumb>https://...immagine.png</thumb>
#      <plot>Descrizione</plot>
#      <!-- type: browse | search | submenu | regex | play | custom -->
#      <type>browse</type>
#      <!-- per type=submenu: nome file relativo o URL completo -->
#      <submenu>sottocategoria.xml</submenu>
#      <!-- per type=search: parametro GET e path -->
#      <search_param>q</search_param>
#      <search_path>/search</search_path>
#      <!-- per type=regex: pattern Python e gruppo di cattura -->
#      <regex>href="(/film/[^"]+)"[^>]*>([^<]+)</regex>
#      <regex_group>1</regex_group>
#    </item>
#    <!-- Voce speciale per URL manuale -->
#    <item>
#      <title>URL personalizzato</title>
#      <url>CUSTOM</url>
#      <type>custom</type>
#    </item>
#  </menu>
# ───────────────────────────────────────────────────────────────────────


def _tag(text: str, tag: str) -> str:
    """Estrae il contenuto del primo tag XML. Robusto a CDATA e spazi."""
    m = re.search(
        r'<' + re.escape(tag) + r'>\s*(?:<!\[CDATA\[)?(.*?)(?:\]\]>)?\s*</' + re.escape(tag) + r'>',
        text, re.DOTALL
    )
    return m.group(1).strip() if m else ''


def _parse_xml_menu(xml_text) -> list:
    """
    Parsa un file XML menu nel nuovo formato <menu><item> oppure
    nel vecchio formato <onion_sites><site>.
    Restituisce lista di dict con: title, url, thumb, plot, type,
    submenu, regex, regex_group, search_param, search_path.
    """
    """
    Parsa un file XML menu in qualsiasi formato TheGroove/custom.
    Accetta:
      - Formato TheGroove: <item><n>/<name>...<link>...<thumbnail>...
      - Formato custom:    <item><title>...<url>...<thumb>...
      - Item con $doregex: vengono eseguiti tramite onion_parser
    Usa regex invece di ET.fromstring perché i file TheGroove:
      - Spesso mancano di root element
      - Contengono [COLOR...] e altri tag non-XML
      - Possono avere & non escapati
    """
    if isinstance(xml_text, bytes):
        xml_text = xml_text.decode('utf-8', errors='replace')

    blocks = re.findall(
        r'<(?:item|plugin|dir)>(.*?)</(?:item|plugin|dir)>',
        xml_text, re.DOTALL
    )
    items = []
    for block in blocks:
        # Estrae pattern regex PRIMA di rimuovere i blocchi <regex>.
        # Usa _tag() sull'originale poi distingue pattern semplice da
        # blocco TheGroove multi-tag (che contiene <n>, <expres>, ecc.).
        _raw_rx = _tag(block, 'regex')
        if _raw_rx and re.search(r'<(?:n|expres|listrepeat|page)\s*>', _raw_rx):
            # Blocco TheGroove: gestito da onion_parser, non un pattern regex
            raw_regex = ''
        else:
            raw_regex = _raw_rx
        raw_regex_group = _tag(block, 'regex_group') or '1'

        # Rimuove i blocchi <regex> TheGroove (multi-tag) per non
        # confondere i tag di livello superiore (<title>, <url>, ecc.)
        clean = re.sub(r'<regex>.*?</regex>', '', block, flags=re.DOTALL)

        title  = (_tag(clean, 'name') or _tag(clean, 'n') or _tag(clean, 'title')).strip()
        url    = (_tag(clean, 'link') or _tag(clean, 'url')).strip()
        thumb  = (_tag(clean, 'thumbnail') or _tag(clean, 'thumb')).strip()
        fanart = _tag(clean, 'fanart').strip()
        plot   = _tag(clean, 'plot').strip()
        itype  = (_tag(clean, 'type') or '').strip().lower()

        if not title:
            continue

        # Item con $doregex → tipo speciale, gestito da onion_doregex nel router
        if url.startswith('$doregex['):
            rname = re.search(r'\$doregex\[([^\]]+)\]', url)
            items.append({
                'title'       : title,
                'url'         : url,
                'thumb'       : thumb,
                'fanart'      : fanart,
                'plot'        : plot,
                'type'        : 'doregex',
                'regex_name'  : rname.group(1) if rname else '',
                '_raw_block'  : block,        # conserva il blocco completo per eseguire la regex
            })
            continue

        # Voci decorative (ignora / senza link)
        if not url or url.lower() == 'ignora':
            if title.strip():
                items.append({
                    'title': title, 'url': 'ignora', 'thumb': thumb,
                    'fanart': fanart, 'plot': plot, 'type': 'separator',
                    'search_param': 'q', 'search_path': '/search',
                    'submenu': '', 'regex': '', 'regex_group': '1',
                })
            continue

        if not itype:
            if url.startswith('plugin://'):
                itype = 'plugin'
            elif url.startswith('$doregex['):
                itype = 'doregex'
            elif url == 'CUSTOM':
                itype = 'custom'
            else:
                itype = 'browse'

        items.append({
            'title'       : title,
            'url'         : url,
            'thumb'       : thumb,
            'fanart'      : fanart,
            'plot'        : plot,
            'type'        : itype,
            'submenu'     : _tag(clean, 'submenu').strip(),
            'regex'       : raw_regex,        # estratto PRIMA dello strip <regex>
            'regex_group' : raw_regex_group,  # idem
            'search_param': _tag(clean, 'search_param').strip() or 'q',
            'search_path' : _tag(clean, 'search_path').strip() or '/search',
        })
    return items


def load_onion_menu(xml_url: str = '') -> list:
    """
    Scarica e parsa il file XML menu (formato TheGroove o custom).
    GET fresca ad ogni chiamata — mai singleton, mai cache.
    Fallback: TorManager → hardcoded.
    """
    target = xml_url or _ONION_HOME_XML

    for attempt, label in [('direct', 'requests'), ('tor', 'TorManager')]:
        try:
            if attempt == 'direct':
                import requests as _req
                resp = _req.get(target, timeout=12,
                                headers={'User-Agent': var.user_agent})
                resp.raise_for_status()
                text = resp.text
            else:
                text = _tor.get(target, timeout=12)
                if not text:
                    raise ValueError('Risposta vuota')

            items = _parse_xml_menu(text)
            if items:
                log(f'[onion] Menu caricato ({label}) da {target}: {len(items)} voci')
                return items
            log(f'[onion] XML vuoto ({label}): {target}')
        except Exception as e:
            log(f'[onion] load_onion_menu {label} fallito ({target}): {e}')

    log('[onion] Tutti i tentativi falliti — uso fallback hardcoded')
    return list(_ONION_SITES_FALLBACK)


def load_onion_submenu(filename: str) -> list:
    """
    Carica un sottomenu XML dal server base.
    filename può essere un nome file relativo ('torrent.xml')
    oppure un URL completo.
    """
    if filename.startswith('http'):
        url = filename
    else:
        url = f'{_ONION_XML_BASE}/{filename.lstrip("/")}'
    return load_onion_menu(url)


def onion_regex_browse(page_url: str, pattern: str,
                        group: int = 1, base_url: str = '') -> list:
    """
    Fetch della pagina e applicazione di una regex per estrarre URL/titoli.
    La regex viene applicata a tutto il sorgente HTML.
    Ogni match produce un item; il gruppo indicato da `group` è l'URL estratto.
    Se esiste un named group 'title' o il gruppo group+1, viene usato come titolo.
    Restituisce lista di dict {title, url, type}.
    """
    from urllib.parse import urljoin as _uj

    _is_onion = '.onion' in page_url
    try:
        html = get_onion(page_url, timeout=25) if _is_onion else get(page_url).text
    except Exception as e:
        log(f'[onion_regex] fetch fallito: {e}')
        return []

    if not html:
        return []

    results = []
    seen    = set()
    rx      = re.compile(pattern, re.IGNORECASE | re.DOTALL)

    for m in rx.finditer(html):
        try:
            extracted_url = m.group(group).strip()
        except (IndexError, Exception):
            continue

        if not extracted_url or extracted_url in seen:
            continue
        seen.add(extracted_url)

        if not extracted_url.startswith(('http', 'magnet')):
            ref = base_url or page_url
            extracted_url = _uj(ref, extracted_url)

        try:
            title = m.group('title').strip()
        except (IndexError, Exception):
            try:
                title = m.group(group + 1).strip()
            except (IndexError, Exception):
                title = extracted_url.split('/')[-1][:60] or extracted_url[:60]

        low = extracted_url.lower().split('?')[0]
        if any(low.endswith(e) for e in ('.m3u8', '.mpd', '.m3u')):
            ltype = 'stream'
        elif any(low.endswith(e) for e in ('.mp4', '.mkv', '.avi', '.webm', '.mov', '.ts')):
            ltype = 'video'
        elif any(low.endswith(e) for e in ('.mp3', '.ogg', '.flac', '.aac', '.opus', '.m4a')):
            ltype = 'audio'
        elif low.endswith('.torrent'):
            ltype = 'torrent'
        elif extracted_url.startswith('magnet:'):
            ltype = 'magnet'
        elif '.onion' in extracted_url:
            ltype = 'onion'
        else:
            ltype = 'http'

        results.append({'title': title, 'url': extracted_url, 'type': ltype})

    log(f'[onion_regex] "{pattern[:40]}"… → {len(results)} match su {page_url[:60]}')
    return results


# ── Compat. legacy: load_onion_sites era usato da codice vecchio ───────
# Adesso chiama load_onion_menu() che carica da hometor.xml ogni volta.
# ONION_SITES rimane come attributo di modulo per il codice che lo legge
# direttamente, ma viene aggiornato ad ogni chiamata di onion_menu().
def load_onion_sites() -> list:
    return load_onion_menu()

ONION_SITES: list = []   # sarà popolato dalla prima chiamata a onion_menu()


def get_onion_menu_items() -> list:
    """Compatibilità legacy — restituisce tuple (title, url, thumb)."""
    return [(s['title'], s['url'], s.get('thumb', '')) for s in ONION_SITES]



def get_onion(url: str, referer: str = '', timeout: int = 30) -> Optional[str]:
    """
    Fetch di qualsiasi URL (anche .onion) tramite il tunnel SOCKS5 di Tor.
    socks5h = DNS risolto DENTRO Tor, obbligatorio per .onion.

    Tentativi in ordine:
      1. SOCKS5 diretto su 127.0.0.1:9050
      2. TorManager (proxy interno se disponibile)
    """
    from tor_manager import _port_open, TOR_HOST, TOR_SOCKS_PORT
    import urllib3
    urllib3.disable_warnings()

    req_headers = dict(var.headers)
    if referer:
        req_headers['Referer'] = referer

    # ── Tentativo 1: SOCKS5 diretto ───────────────────────────────────
    if _port_open(TOR_HOST, TOR_SOCKS_PORT, timeout=2):
        try:
            import requests as _req
            resp = _req.get(
                url,
                proxies=_ONION_PROXY,
                headers=req_headers,
                timeout=timeout,
                verify=False,
            )
            if resp.status_code < 400:
                log(f'[torproxy] get_onion OK (SOCKS5): {url[:60]}')
                return resp.text
            log(f'[torproxy] get_onion HTTP {resp.status_code}: {url[:60]}')
        except Exception as e:
            log(f'[torproxy] get_onion SOCKS5 fallito: {e}')
    else:
        log('[torproxy] get_onion: porta 9050 non aperta')

    # ── Tentativo 2: TorManager ────────────────────────────────────────
    html = _tor.get(url, headers=req_headers, timeout=timeout)
    if html:
        log(f'[torproxy] get_onion OK (TorManager): {url[:60]}')
        return html

    log(f'[torproxy] get_onion: tutti i tentativi falliti per {url[:60]}')
    return None


def parse_onion_links(html: str, base_url: str) -> list:
    """
    Estrae dalla pagina HTML tutti i link e i media utili:
      - magnet  : link magnet (priorità massima)
      - torrent : file .torrent
      - stream  : m3u8 / mpd / HLS
      - video   : mp4, mkv, avi, webm, mov, ogv, ts, flv
      - audio   : mp3, ogg, flac, wav, aac, opus, m4a
      - image   : jpg, jpeg, png, gif, webp, bmp, svg
      - onion   : link .onion interni/esterni
      - http    : link clearnet generici

    Restituisce lista di dict: {title, url, type, thumb}
    """
    from urllib.parse import urljoin

    _VIDEO_EXT  = ('.mp4', '.mkv', '.avi', '.webm', '.mov', '.ogv', '.ts', '.flv', '.m4v')
    _AUDIO_EXT  = ('.mp3', '.ogg', '.flac', '.wav', '.aac', '.opus', '.m4a', '.wma')
    _IMAGE_EXT  = ('.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg', '.tiff', '.tif')
    _STREAM_EXT = ('.m3u8', '.mpd', '.m3u')

    soup = get_soup(html)
    results = []
    seen = set()

    def _add(title, lurl, ltype, thumb=''):
        key = lurl.strip()
        if not key or key in seen:
            return
        seen.add(key)
        clean = (title or lurl[:60]).strip()
        results.append({'title': clean, 'url': key, 'type': ltype, 'thumb': thumb})

    def _ext(href):
        """Estrae l'estensione dal path ignorando query string."""
        path = urlparse(href).path.lower()
        for ext in _STREAM_EXT:
            if path.endswith(ext) or f'{ext}?' in path:
                return 'stream'
        for ext in _VIDEO_EXT:
            if path.endswith(ext):
                return 'video'
        for ext in _AUDIO_EXT:
            if path.endswith(ext):
                return 'audio'
        for ext in _IMAGE_EXT:
            if path.endswith(ext):
                return 'image'
        return None

    # ── Tag <video> e <source> embeddati ──────────────────────────────
    for tag in soup.find_all(['video', 'source']):
        src = tag.get('src') or tag.get('data-src') or ''
        if src and not src.startswith('data:'):
            full = src if src.startswith('http') else urljoin(base_url, src)
            label = tag.get('title') or tag.get('alt') or full.split('/')[-1][:50]
            _add(f'[COLOR cyan]▶  {label}[/COLOR]', full, 'video')

    # ── Tag <audio> ────────────────────────────────────────────────────
    for tag in soup.find_all('audio'):
        src = tag.get('src') or tag.get('data-src') or ''
        if src and not src.startswith('data:'):
            full = src if src.startswith('http') else urljoin(base_url, src)
            label = tag.get('title') or full.split('/')[-1][:50]
            _add(f'[COLOR aquamarine]🎵  {label}[/COLOR]', full, 'audio')

    # ── Tag <img> (immagini dirette nella pagina) ──────────────────────
    for tag in soup.find_all('img'):
        src = tag.get('src') or tag.get('data-src') or tag.get('data-lazy-src') or ''
        if not src or src.startswith('data:'):
            continue
        full = src if src.startswith('http') else urljoin(base_url, src)
        lext = _ext(full)
        if lext == 'image':
            label = tag.get('alt') or tag.get('title') or full.split('/')[-1][:50]
            _add(f'[COLOR yellow]🖼  {label}[/COLOR]', full, 'image', thumb=full)

    # ── Tag <a href> ───────────────────────────────────────────────────
    for tag in soup.find_all('a', href=True):
        href  = tag['href'].strip()
        label = tag.get_text(separator=' ', strip=True) or href[:50]

        if not href or href.startswith('javascript') or href == '#':
            continue

        # Magnet
        if href.startswith('magnet:'):
            dn = re.search(r'dn=([^&]+)', href)
            friendly = dn.group(1).replace('+', ' ') if dn else label
            _add(f'[COLOR gold]🧲  {friendly}[/COLOR]', href, 'magnet')
            continue

        # File .torrent
        if href.lower().endswith('.torrent'):
            full = href if href.startswith('http') else urljoin(base_url, href)
            _add(f'[COLOR limegreen]📥  {label}[/COLOR]', full, 'torrent')
            continue

        full = href if href.startswith(('http', 'magnet')) else urljoin(base_url, href)
        lext = _ext(full)

        # Stream (m3u8 / mpd)
        if lext == 'stream':
            _add(f'[COLOR orange]📡  {label}[/COLOR]', full, 'stream')
            continue

        # Video diretto
        if lext == 'video':
            _add(f'[COLOR cyan]▶  {label}[/COLOR]', full, 'video')
            continue

        # Audio diretto
        if lext == 'audio':
            _add(f'[COLOR aquamarine]🎵  {label}[/COLOR]', full, 'audio')
            continue

        # Immagine diretta
        if lext == 'image':
            _add(f'[COLOR yellow]🖼  {label}[/COLOR]', full, 'image', thumb=full)
            continue

        # Link .onion
        if '.onion' in href or (href.startswith('/') and '.onion' in base_url):
            _add(f'[COLOR deepskyblue]🧅  {label}[/COLOR]', full, 'onion')
            continue

        # Link HTTP clearnet
        if href.startswith('http'):
            _add(label, full, 'http')

    return results


def create_onion_listitem(entry: dict, mode: str = 'onion_browse') -> None:
    """
    Aggiunge un ListItem Kodi per un link proveniente da una pagina .onion.
    Tipi gestiti: magnet, torrent, stream, video, audio, image, onion, http.
    """
    title = entry.get('title', '')
    url   = entry.get('url', '')
    thumb = entry.get('thumb', '') or var.addon_icon
    ltype = entry.get('type', 'http')

    if ltype == 'magnet':
        # Magnet → apre direttamente con il client BitTorrent di Kodi
        liz = var.list_item(title)
        liz.setArt({'thumb': thumb, 'icon': thumb})
        liz.setProperty('IsPlayable', 'true')
        xbmcplugin.addDirectoryItem(var.handle, url, liz, False)

    elif ltype == 'torrent':
        # .torrent → scarica via Tor e salva in Downloads
        item = Item(
            title=title, type='file', mode='onion_torrent',
            link=url, thumbnail=thumb, is_media=False,
        )
        create_listitem(item)

    elif ltype in ('stream', 'video', 'audio'):
        # Media diretto → riproduzione tramite onion_play
        item = Item(
            title=title, type='file', mode='onion_play',
            link=url, thumbnail=thumb, is_media=True,
        )
        create_listitem(item)

    elif ltype == 'image':
        # Immagine → visualizzatore immagini Kodi tramite onion_image
        item = Item(
            title=title, type='file', mode='onion_image',
            link=url, thumbnail=thumb, is_media=False,
        )
        create_listitem(item)

    else:
        # .onion o http → navigazione come cartella
        item = Item(
            title=title, type='dir', mode=mode,
            link=url, thumbnail=thumb, is_media=False,
        )
        create_listitem(item)


# ─────────────────────────────────────────────
#  ONION SEARCH SYSTEM
# ─────────────────────────────────────────────

def onion_search(base_url: str, query: str,
                  search_param: str = 'q',
                  search_path: str = '/search') -> list:
    """
    Ricerca generica su sito .onion o clearnet.
    Usa prima search_param/search_path dall'XML remoto, poi prova i
    pattern comuni come fallback.
    Usa get_onion() per URL .onion, get() per clearnet.
    Restituisce lista di dict: {title, url, type}.
    """
    from urllib.parse import urljoin

    _is_onion = '.onion' in base_url
    _fetch    = get_onion if _is_onion else (lambda u, **kw: get(u).text)

    _VIDEO_EXT  = ('.mp4', '.mkv', '.avi', '.webm', '.mov', '.ogv', '.ts', '.flv', '.m4v')
    _AUDIO_EXT  = ('.mp3', '.ogg', '.flac', '.wav', '.aac', '.opus', '.m4a', '.wma')
    _IMAGE_EXT  = ('.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg')
    _STREAM_EXT = ('.m3u8', '.mpd', '.m3u')

    def _detect_type(href: str) -> str:
        path = urlparse(href).path.lower()
        for e in _STREAM_EXT:
            if path.endswith(e): return 'stream'
        for e in _VIDEO_EXT:
            if path.endswith(e): return 'video'
        for e in _AUDIO_EXT:
            if path.endswith(e): return 'audio'
        for e in _IMAGE_EXT:
            if path.endswith(e): return 'image'
        if '.onion' in href: return 'onion'
        return 'http'

    base = base_url.rstrip('/')
    q    = quote_plus(query)

    # Prima usa i parametri dall'XML, poi i pattern comuni (deduplicati)
    seen_u: list = []
    def _add_candidate(u: str):
        if u not in seen_u:
            seen_u.append(u)

    _add_candidate(f'{base}{search_path}?{search_param}={q}')
    _add_candidate(f'{base}/search?q={q}')
    _add_candidate(f'{base}/search?query={q}')
    _add_candidate(f'{base}/search?s={q}')
    _add_candidate(f'{base}/?s={q}')
    _add_candidate(f'{base}/?q={q}')
    _add_candidate(f'{base}/find?q={q}')
    _add_candidate(f'{base}/results?q={q}')
    _add_candidate(f'{base}/search/{q}')

    html = None
    for search_url in seen_u:
        try:
            log(f'[onion_search] provo: {search_url}')
            html = _fetch(search_url, timeout=25)
            if html and len(html) > 500:
                log(f'[onion_search] risposta valida da: {search_url}')
                break
            html = None
        except Exception as e:
            log(f'[onion_search] errore su {search_url}: {e}')

    if not html:
        log('[onion_search] tutti i pattern falliti')
        return []

    soup = get_soup(html)
    results = []
    seen = set()

    for a in soup.find_all('a', href=True):
        title = a.get_text(strip=True)
        href  = a['href'].strip()

        if not title or not href or href.startswith('javascript') or href == '#':
            continue
        if len(title) < 3:
            continue

        full = href if href.startswith('http') else urljoin(base_url, href)
        if full in seen:
            continue
        seen.add(full)

        ltype = _detect_type(full)
        results.append({'title': title, 'url': full, 'type': ltype})

    log(f'[onion_search] trovati {len(results)} risultati per "{query}"')
    return results


def onion_search_dialog(site_url: str) -> None:
    """
    Mostra la tastiera Kodi per inserire la query, esegue la ricerca
    e aggiunge i risultati come ListItem con il tipo corretto.
    """
    keyboard = xbmc.Keyboard('', 'Cerca contenuti nel sito')
    keyboard.doModal()

    if not keyboard.isConfirmed():
        return

    query = keyboard.getText().strip()
    if not query:
        return

    var.notify_dialog('Tor Network', f'Ricerca: {query}…', icon=var.addon_icon)
    results = onion_search(site_url, query)

    if not results:
        ok_dialog('Nessun risultato trovato')
        return

    for r in results:
        ltype = r.get('type', 'http')
        url   = r['url']
        title = r['title']

        if ltype == 'magnet':
            liz = var.list_item(title)
            liz.setArt({'thumb': var.addon_icon, 'icon': var.addon_icon})
            liz.setProperty('IsPlayable', 'true')
            xbmcplugin.addDirectoryItem(var.handle, url, liz, False)

        elif ltype == 'torrent':
            create_listitem(Item(
                title=f'[COLOR limegreen]📥  {title}[/COLOR]',
                type='file', mode='onion_torrent',
                link=url, thumbnail=var.addon_icon, is_media=False,
            ))

        elif ltype in ('stream', 'video', 'audio'):
            create_listitem(Item(
                title=title, type='file', mode='onion_play',
                link=url, thumbnail=var.addon_icon, is_media=True,
            ))

        elif ltype == 'image':
            create_listitem(Item(
                title=f'[COLOR yellow]🖼  {title}[/COLOR]',
                type='file', mode='onion_image',
                link=url, thumbnail=url, is_media=False,
            ))

        else:
            # onion o http → cartella navigabile
            create_listitem(Item(
                title=title, type='dir', mode='onion_browse',
                link=url, thumbnail=var.addon_icon, is_media=False,
            ))


def resolve_onion_video(url: str) -> Optional[str]:
    """
    Risolve uno stream / media da una pagina .onion o clearnet.
    Usa get_onion() per URL .onion, get() per clearnet.

    Cerca in ordine:
      1. Tag <video src> / <source src>
      2. Link m3u8 / mpd in <a>
      3. attributo data-src, data-video-src
      4. JSON inline con campi src/url/file/stream/hls
      5. iframe src (rimanda a browse)
      6. Magnet link
    """
    _is_onion = '.onion' in url
    try:
        if _is_onion:
            html = get_onion(url, timeout=30)
        else:
            html = get(url).text
    except Exception as e:
        log(f'[resolve_onion_video] fetch fallito: {e}')
        return None

    if not html:
        return None

    soup = get_soup(html)

    # 1. <video src> o <source src>
    for tag in soup.find_all(['video', 'source']):
        src = tag.get('src') or tag.get('data-src') or ''
        if src and not src.startswith('data:'):
            log(f'[resolve_onion_video] trovato tag video/source: {src[:80]}')
            return src if src.startswith('http') else f'{urlparse(url).scheme}://{urlparse(url).netloc}{src}'

    # 2. Link <a> con estensione media
    _MEDIA_EXT = ('.m3u8', '.mpd', '.mp4', '.mkv', '.avi', '.webm', '.mov',
                  '.mp3', '.ogg', '.flac', '.aac', '.opus', '.m4a')
    for a in soup.find_all('a', href=True):
        href = a['href']
        if any(href.lower().endswith(e) or f'{e}?' in href.lower() for e in _MEDIA_EXT):
            full = href if href.startswith('http') else f'{urlparse(url).scheme}://{urlparse(url).netloc}{href}'
            log(f'[resolve_onion_video] trovato link media: {full[:80]}')
            return full

    # 3. attributi data-* con URL media
    for tag in soup.find_all(True):
        for attr in ('data-src', 'data-video-src', 'data-file', 'data-stream', 'data-hls'):
            val = tag.get(attr, '')
            if val and any(val.lower().endswith(e) or f'{e}?' in val.lower()
                           for e in ('.m3u8', '.mp4', '.webm', '.mkv')):
                log(f'[resolve_onion_video] trovato {attr}: {val[:80]}')
                return val if val.startswith('http') else f'{urlparse(url).scheme}://{urlparse(url).netloc}{val}'

    # 4. JSON inline (cerca campi comuni usati dai player)
    for script in soup.find_all('script'):
        text = script.get_text()
        for pattern in (
            r'"(?:src|url|file|stream|hls|source)"\s*:\s*"([^"]+\.(?:m3u8|mp4|webm|mkv|mpd)[^"]*)"',
            r"'(?:src|url|file|stream|hls|source)'\s*:\s*'([^']+\.(?:m3u8|mp4|webm|mkv|mpd)[^']*)'",
            r'(?:src|url|file|stream|hls)\s*=\s*["\']([^"\']+\.(?:m3u8|mp4|webm|mkv|mpd)[^"\']*)["\']',
        ):
            m = re.search(pattern, text, re.IGNORECASE)
            if m:
                found = m.group(1)
                log(f'[resolve_onion_video] trovato JSON inline: {found[:80]}')
                return found if found.startswith('http') else f'{urlparse(url).scheme}://{urlparse(url).netloc}{found}'

    # 5. Magnet link
    for a in soup.find_all('a', href=True):
        if a['href'].startswith('magnet:'):
            log(f'[resolve_onion_video] trovato magnet')
            return a['href']

    # 6. iframe → restituisce l'URL dell'iframe per navigazione
    iframe = soup.find('iframe')
    if iframe:
        src = iframe.get('src', '')
        if src:
            full = src if src.startswith('http') else f'{urlparse(url).scheme}://{urlparse(url).netloc}{src}'
            log(f'[resolve_onion_video] trovato iframe: {full[:80]}')
            return full

    log(f'[resolve_onion_video] nessuno stream trovato in: {url[:80]}')
    return None