#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
bypass.py – Modulo avanzato per il bypass di blocchi DNS e di rete.
Ottimizzato per siti di streaming e liste IPTV.

Miglioramenti apportati:
1. Risolto problema SNI (TLS) per connessioni dirette via IP.
2. Gestione thread-safe e accumulo timeout nel refresh dei proxy.
3. Correzione endpoint Quad9 e parsing JSON flessibile.
4. Logica di timeout aggressiva per non bloccare la UI (Kodi).
"""

import socket
import logging
import requests
import urllib3
import threading
import time
import ssl
from typing import Optional, Dict, List
from urllib.parse import urlparse, urlunparse
from requests.adapters import HTTPAdapter

# Sopprime warning SSL (necessario per connessioni dirette via IP e proxy trasparenti)
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

log = logging.getLogger("bypass")

# ======================================================================
# FIX TLS SNI: Consente di inviare l'hostname corretto durante l'handshake SSL
# anche quando ci connettiamo direttamente a un indirizzo IP.
# ======================================================================
class SNIAdapter(HTTPAdapter):
    """
    Adapter personalizzato che forza l'hostname corretto nell'handshake TLS
    anche se l'URL punta a un indirizzo IP.
    """
    def __init__(self, server_hostname, **kwargs):
        self.server_hostname = server_hostname
        super(SNIAdapter, self).__init__(**kwargs)

    def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs):
        # Crea un contesto SSL che supporta SNI
        context = ssl.create_default_context()
        context.check_hostname = False
        context.verify_mode = ssl.CERT_NONE
        self.poolmanager = urllib3.PoolManager(
            num_pools=connections,
            maxsize=maxsize,
            block=block,
            ssl_context=context,
            server_hostname=self.server_hostname,
            **pool_kwargs
        )

# ======================================================================
# CONFIGURAZIONE
# ======================================================================

_DOH_RESOLVERS = [
    "https://cloudflare-dns.com/dns-query",
    "https://dns.google/resolve",
    "https://dns.quad9.net/dns-query", # Porta 443 standard
]

_PROXY_SOURCES = [
    {
        "url": "https://api.proxyscrape.com/v3/free-proxy-list/get?request=displayproxies&protocol=socks5&timeout=3000&country=all&ssl=all&anonymity=elite",
        "type": "socks5",
    },
    {
        "url": "https://api.proxyscrape.com/v3/free-proxy-list/get?request=displayproxies&protocol=http&timeout=3000&country=all&ssl=all&anonymity=elite",
        "type": "http",
    },
    {
        "url": "https://proxylist.geonode.com/api/proxy-list?limit=50&page=1&sort_by=lastChecked&sort_type=desc&protocols=socks5",
        "type": "socks5",
        "json": True,
    },
]

_STATIC_PROXIES: List[Dict] = [
    {"url": "http://103.152.112.162:80",  "type": "http"},
    {"url": "http://185.162.231.106:80",  "type": "http"},
]

_dynamic_proxies: List[Dict] = []
_proxy_lock  = threading.Lock()
_proxy_index = 0
_dns_cache: Dict[str, str] = {}

# ======================================================================
# 1. RESOLVER DoH
# ======================================================================

def resolve_doh(hostname: str) -> Optional[str]:
    if hostname in _dns_cache:
        return _dns_cache[hostname]

    for resolver in _DOH_RESOLVERS:
        try:
            # Supporta sia il formato JSON di Google/Cloudflare che i parametri standard
            resp = requests.get(
                resolver,
                params={"name": hostname, "type": "A"},
                headers={"Accept": "application/dns-json"},
                timeout=4,
            )
            data = resp.json()
            # Alcuni resolver usano 'Answer', altri una struttura diversa
            answers = data.get("Answer", [])
            for answer in answers:
                if answer.get("type") == 1: # Tipo A (IPv4)
                    ip = answer["data"]
                    _dns_cache[hostname] = ip
                    log.info(f"DoH: {hostname} risolto in {ip}")
                    return ip
        except Exception as e:
            log.debug(f"DoH resolver {resolver} fallito: {e}")
    return None

# ======================================================================
# 2. GESTIONE PROXY DINAMICI
# ======================================================================

def _fetch_raw_proxies() -> List[Dict]:
    found: List[Dict] = []
    for src in _PROXY_SOURCES:
        try:
            resp = requests.get(src["url"], timeout=10)
            if resp.status_code != 200: continue

            prefix = "socks5h://" if src["type"] == "socks5" else "http://"
            
            if src.get("json"):
                data = resp.json()
                items = data.get("data", []) if isinstance(data, dict) else []
                for entry in items[:25]:
                    ip, port = entry.get("ip"), entry.get("port")
                    if ip and port:
                        found.append({"url": f"{prefix}{ip}:{port}", "type": src["type"]})
            else:
                for line in resp.text.splitlines()[:25]:
                    line = line.strip()
                    if ":" in line:
                        found.append({"url": f"{prefix}{line}", "type": src["type"]})
        except Exception as e:
            log.debug(f"Fetch proxy fallito per {src['url']}: {e}")
    return found

def _check_proxy_worker(proxy_cfg: Dict, valid_list: List[Dict]):
    try:
        proxies = {"http": proxy_cfg["url"], "https": proxy_cfg["url"]}
        # Test su Cloudflare per verificare latenza e connettività
        r = requests.get("https://1.1.1.1", proxies=proxies, timeout=3, verify=False)
        if r.status_code == 200:
            with _proxy_lock:
                valid_list.append(proxy_cfg)
    except:
        pass

def refresh_proxies():
    global _dynamic_proxies
    log.info("Inizio aggiornamento proxy in background...")
    raw = _fetch_raw_proxies()
    valid: List[Dict] = []
    threads = []

    for p in raw:
        t = threading.Thread(target=_check_proxy_worker, args=(p, valid))
        t.daemon = True
        t.start()
        threads.append(t)

    # Attesa intelligente: max 10 secondi totali per tutti i thread
    deadline = time.time() + 10
    for t in threads:
        timeout_left = deadline - time.time()
        if timeout_left <= 0: break
        t.join(timeout=timeout_left)

    with _proxy_lock:
        _dynamic_proxies = valid
    log.info(f"Refresh completato: {len(_dynamic_proxies)} proxy validati.")

def _next_proxy() -> Optional[Dict]:
    global _proxy_index
    with _proxy_lock:
        pool = _dynamic_proxies if _dynamic_proxies else _STATIC_PROXIES
        if not pool: return None
        proxy = pool[_proxy_index % len(pool)]
        _proxy_index += 1
        return proxy

# ======================================================================
# 3. FUNZIONE PRINCIPALE (BYPASS A CASCATA)
# ======================================================================

def get_with_bypass(url: str, headers: Optional[Dict] = None, timeout: int = 15) -> Optional[str]:
    """
    GET con bypass automatico. 
    Ordine: DoH con SNI Fix -> Proxy Dinamici -> Proxy Statici -> Diretta.
    """
    req_headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
        "Accept": "*/*",
    }
    if headers: req_headers.update(headers)

    parsed = urlparse(url)
    hostname = parsed.hostname
    if not hostname: return None

    # --- TENTATIVO 1: DoH + SNI Fix (Molto efficace per AGCOM/blocchi DNS) ---
    ip = resolve_doh(hostname)
    if ip:
        try:
            # Costruiamo l'URL usando l'IP invece dell'hostname
            netloc = f"{ip}:{parsed.port}" if parsed.port else ip
            ip_url = urlunparse(parsed._replace(netloc=netloc))
            
            session = requests.Session()
            # Fondamentale: montiamo l'adapter SNI per l'IP risolto
            session.mount(f"https://{ip}", SNIAdapter(server_hostname=hostname))
            
            log.info(f"Tentativo DoH+SNI: {hostname} -> {ip}")
            resp = session.get(ip_url, headers=req_headers, timeout=7, verify=False)
            if resp.status_code < 400:
                return resp.text
        except Exception as e:
            log.debug(f"Bypass DoH fallito: {e}")

    # --- TENTATIVO 2: Proxy (Dinamici/Statici) ---
    # Proviamo massimo 3 proxy per non eccedere il timeout totale
    for _ in range(3):
        proxy_conf = _next_proxy()
        if not proxy_conf: break
        try:
            proxies = {"http": proxy_conf["url"], "https": proxy_conf["url"]}
            log.info(f"Tentativo Proxy: {proxy_conf['url']}")
            resp = requests.get(url, headers=req_headers, proxies=proxies, timeout=5)
            if resp.status_code < 400:
                return resp.text
        except Exception as e:
            # Se il proxy dinamico fallisce, lo rimuoviamo per i futuri tentativi
            with _proxy_lock:
                if proxy_conf in _dynamic_proxies:
                    _dynamic_proxies.remove(proxy_conf)

    # --- TENTATIVO 3: Diretta (Ultima spiaggia) ---
    try:
        log.info(f"Tentativo diretto: {url}")
        resp = requests.get(url, headers=req_headers, timeout=5)
        if resp.status_code < 400:
            return resp.text
    except:
        pass

    return None

# ======================================================================
# 4. DIAGNOSI
# ======================================================================

def diagnose(url: str) -> Dict:
    parsed = urlparse(url)
    hostname = parsed.hostname
    result = {"hostname": hostname, "verdict": "unknown"}
    
    # Check DNS sistema
    try:
        sys_ip = socket.gethostbyname(hostname)
        result["system_dns"] = sys_ip
    except:
        result["system_dns"] = "BLOCKED/FAILED"

    # Check DoH
    doh_ip = resolve_doh(hostname)
    result["doh_dns"] = doh_ip if doh_ip else "FAILED"

    # Verdetto rapido
    if result["system_dns"] == "BLOCKED/FAILED" and doh_ip:
        result["verdict"] = "DNS_BLOCK_DETECTED"
    else:
        result["verdict"] = "POSSIBLE_IP_OR_GEO_BLOCK"
        
    return result

# Avvio automatico refresh proxy all'importazione
threading.Thread(target=refresh_proxies, daemon=True).start()