#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys
import time
import ast
import argparse
import requests
from collections import defaultdict
from requests.exceptions import RequestException

# ========== НАСТРОЙКИ ==========
CHECK_URLS = [
    ("https://1.1.1.1/cdn-cgi/trace", "IPv4"),
    ("https://[2606:4700:4700::1111]/cdn-cgi/trace", "IPv6"),
    ("https://one.one.one.one/cdn-cgi/trace", "WWW"),
]
TIMEOUT = 30
RETRIES = 3
SLEEP_BETWEEN = 1
RETRY_INTERVAL = 30
PROXY_FILE = "proxies.txt"
IGNORE_PORTS = {"3128", "8118", "18080", "7777", "6699"}

# Полный список известных PoP локаций Cloudflare
POP_LOCATIONS = {
    # Европа
    "AMS": "Amsterdam, Netherlands",
    "ARN": "Stockholm, Sweden",
    "ATH": "Athens, Greece",
    "BCN": "Barcelona, Spain",
    "BER": "Berlin, Germany",
    "BLL": "Billund, Denmark",
    "BOD": "Bordeaux, France",
    "BRE": "Bremen, Germany",
    "BRU": "Brussels, Belgium",
    "BTS": "Bratislava, Slovakia",
    "BUD": "Budapest, Hungary",
    "CPH": "Copenhagen, Denmark",
    "DUB": "Dublin, Ireland",
    "DUS": "Düsseldorf, Germany",
    "EDI": "Edinburgh, UK",
    "FCO": "Rome, Italy",
    "FRA": "Frankfurt, Germany",
    "GVA": "Geneva, Switzerland",
    "HAJ": "Hannover, Germany",
    "HAM": "Hamburg, Germany",
    "HEL": "Helsinki, Finland",
    "IST": "Istanbul, Turkey",
    "KBP": "Kyiv, Ukraine",
    "KIV": "Chișinău, Moldova",
    "KRK": "Krakow, Poland",
    "LCA": "Larnaca, Cyprus",
    "LHR": "London, UK",
    "LIS": "Lisbon, Portugal",
    "LUX": "Luxembourg",
    "LYS": "Lyon, France",
    "MAD": "Madrid, Spain",
    "MAN": "Manchester, UK",
    "MRS": "Marseille, France",
    "MUC": "Munich, Germany",
    "MXP": "Milan, Italy",
    "NCE": "Nice, France",
    "NUE": "Nuremberg, Germany",
    "OSL": "Oslo, Norway",
    "OTP": "Bucharest, Romania",
    "PRG": "Prague, Czech Republic",
    "RIX": "Riga, Latvia",
    "SOF": "Sofia, Bulgaria",
    "STR": "Stuttgart, Germany",
    "TLL": "Tallinn, Estonia",
    "TLS": "Toulouse, France",
    "VCE": "Venice, Italy",
    "VIE": "Vienna, Austria",
    "VNO": "Vilnius, Lithuania",
    "WAW": "Warsaw, Poland",
    "ZAG": "Zagreb, Croatia",
    "ZRH": "Zurich, Switzerland",
    
    # Россия и СНГ
    "DME": "Moscow, Russia",
    "SVO": "Moscow, Russia",
    "LED": "St. Petersburg, Russia",
    "KZN": "Kazan, Russia",
    "KGD": "Kaliningrad, Russia",
    "KRR": "Krasnodar, Russia",
    "OVB": "Novosibirsk, Russia",
    "ROV": "Rostov-on-Don, Russia",
    "AER": "Sochi, Russia",
    "TAS": "Tashkent, Uzbekistan",
    "ALA": "Almaty, Kazakhstan",
    
    # Северная Америка
    "ATL": "Atlanta, USA",
    "BOS": "Boston, USA",
    "BUF": "Buffalo, USA",
    "DEN": "Denver, USA",
    "DFW": "Dallas, USA",
    "DTW": "Detroit, USA",
    "EWR": "Newark, USA",
    "IAD": "Washington, USA",
    "IAH": "Houston, USA",
    "JFK": "New York, USA",
    "LAS": "Las Vegas, USA",
    "LAX": "Los Angeles, USA",
    "MCI": "Kansas City, USA",
    "MIA": "Miami, USA",
    "MSP": "Minneapolis, USA",
    "ORD": "Chicago, USA",
    "PHL": "Philadelphia, USA",
    "PHX": "Phoenix, USA",
    "PIT": "Pittsburgh, USA",
    "PDX": "Portland, USA",
    "SAN": "San Diego, USA",
    "SEA": "Seattle, USA",
    "SFO": "San Francisco, USA",
    "SJC": "San Jose, USA",
    "SLC": "Salt Lake City, USA",
    "STL": "St. Louis, USA",
    "TPA": "Tampa, USA",
    "TOR": "Toronto, Canada",
    "VAN": "Vancouver, Canada",
    "YUL": "Montreal, Canada",
    "YYZ": "Toronto, Canada",
    
    # Азия
    "BKK": "Bangkok, Thailand",
    "BLR": "Bangalore, India",
    "CAN": "Guangzhou, China",
    "CGK": "Jakarta, Indonesia",
    "DEL": "Delhi, India",
    "HKG": "Hong Kong",
    "HYD": "Hyderabad, India",
    "ICN": "Seoul, South Korea",
    "KIX": "Osaka, Japan",
    "MAA": "Chennai, India",
    "NRT": "Tokyo, Japan",
    "PEK": "Beijing, China",
    "PVG": "Shanghai, China",
    "SIN": "Singapore",
    "TPE": "Taipei, Taiwan",
    
    # Другие регионы
    "AKL": "Auckland, New Zealand",
    "BNE": "Brisbane, Australia",
    "MEL": "Melbourne, Australia",
    "PER": "Perth, Australia",
    "SYD": "Sydney, Australia",
    "GRU": "São Paulo, Brazil",
    "EZE": "Buenos Aires, Argentina",
    "SCL": "Santiago, Chile",
    "JNB": "Johannesburg, South Africa",
}

error_replacements = {
    "[WinError 10061] Подключение не установлено": "CONN_REFUSED",
    "Connection refused": "CONN_REFUSED",
    "502 Bad Gateway": "BAD_GATEWAY",
    "Connection closed unexpectedly": "CONN_CLOSED",
    "timed out": "TIMEOUT",
    "RemoteDisconnected": "CONN_CLOSED",
    "Connection reset": "CONN_RESET",
    "Read timed out": "TIMEOUT",
    "Max retries exceeded": "RETRIES_EXCEEDED",
    "NewConnectionError": "CONN_ERROR",
    "Failed to establish a new connection": "CONN_ERROR"
}

# ========== ЗАГРУЗКА ПРОКСИ С ГРУППАМИ ==========
def load_proxies_from_file(filename=PROXY_FILE):
    """Загружает прокси из файла, разделяя на группы по пустым строкам"""
    all_groups = []
    current_group = []

    try:
        with open(filename, encoding="utf-8") as f:
            for line in f:
                line = line.strip()
                if not line:
                    if current_group:
                        all_groups.append(current_group)
                        current_group = []
                    continue
                if line.startswith("#"):
                    continue
                try:
                    proto, host, port = ast.literal_eval(line)
                    current_group.append(f"{proto} {host}:{port}")
                except Exception:
                    print(f"Ошибка в строке: {line}", file=sys.stderr)
                    continue
        if current_group:
            all_groups.append(current_group)
    except FileNotFoundError:
        print(f"Файл {filename} не найден!", file=sys.stderr)
        sys.exit(1)

    return all_groups

# ========== ПРОВЕРКА ПРОКСИ ==========
def check_proxy(proxy, url, timeout=TIMEOUT):
    proto, addr = proxy.split(" ", 1)
    proxies = {"http": f"{proto}://{addr}", "https": f"{proto}://{addr}"}
    try:
        r = requests.get(url, proxies=proxies, timeout=timeout, verify=True)
        if r.status_code == 200 and "ip=" in r.text:
            return r.text
        return f"FAIL {r.status_code}"
    except RequestException as e:
        return str(e).strip().split(":")[-1].strip()
    except Exception as e:
        return str(e)

def parse_result(text):
    result = {"ip": text}
    if "ip=" not in text:
        return result
    for line in text.splitlines():
        if "=" not in line:
            continue
        key, val = line.split("=", 1)
        result[key.strip()] = val.strip()
    return {
        "ip": result.get("ip", "N/A"),
        "loc": result.get("loc", "N/A"),
        "colo": result.get("colo", "N/A")
    }

def check_proxy_connectivity(proxy, verbose=False):
    result = {"proxy": proxy}
    for url, label in CHECK_URLS:
        retries = 0
        while retries < RETRIES:
            if verbose:
                print(f"[{proxy}] {label} check {retries + 1}/{RETRIES}...")
            res = check_proxy(proxy, url)
            parsed = parse_result(res)
            ip = parsed["ip"]
            result[label] = ip
            if "fail" not in ip.lower() and result.get("Loc", "N/A") == "N/A":
                result["Loc"] = parsed.get("loc", "N/A")
                result["PoP"] = parsed.get("colo", "N/A")
            if "fail" not in ip.lower():
                break
            time.sleep(RETRY_INTERVAL)
            retries += 1
        time.sleep(SLEEP_BETWEEN)
    return result

# ========== ВЫВОД РЕЗУЛЬТАТОВ ==========
def print_grouped_results(groups, results):
    result_map = {r["proxy"]: r for r in results}
    seen_proxies = set()
    display_rows = []

    # Подготовка всех строк (без повторов)
    for group in groups:
        for proxy in group:
            if proxy in seen_proxies:
                continue
            seen_proxies.add(proxy)

            r = result_map.get(proxy, {})
            proxy_str = proxy
            ipv4 = r.get("IPv4", "N/A")
            ipv6 = r.get("IPv6", "N/A")
            loc = r.get("Loc", "N/A")
            cf = r.get("PoP", "N/A")
            
            # Замена кода PoP на полное название локации
            if cf in POP_LOCATIONS:
                cf = POP_LOCATIONS[cf]
            elif cf != "N/A":
                cf = f"{cf} (Unknown)"

            www_val = r.get("WWW", "").strip().lower()
            if "fail" in www_val or "error" in www_val or not www_val:
                www_type = "ERR"
            elif ":" in www_val:
                www_type = "IPv6"
            elif "." in www_val:
                www_type = "IPv4"
            else:
                www_type = "ERR"

            # Сокращение IPv6: первые 2 блока + последний ненулевой
            if ":" in ipv6 and not any(err in ipv6 for err in error_replacements):
                parts = ipv6.split(":")
                while len(parts) < 8:
                    parts.append("0")
                prefix = ":".join(parts[:2]) + ":"
                suffix = next((p for p in reversed(parts) if p and p != "0"), "")
                ipv6 = f"{prefix} :{suffix}"

            for err, short in error_replacements.items():
                if err in ipv4:
                    ipv4 = short
                    break
            for err, short in error_replacements.items():
                if err in ipv6:
                    ipv6 = short
                    break

            display_rows.append({
                "proxy": proxy_str,
                "ipv4": ipv4,
                "ipv6": ipv6,
                "www": www_type,
                "loc": loc,
                "cf": cf,
            })

    # Автоширина колонок
    widths = {
        "proxy": max(len("Proxy"), max(len(r["proxy"]) for r in display_rows)),
        "ipv4":  max(len("IPv4"), max(len(r["ipv4"]) for r in display_rows)),
        "ipv6":  max(len("IPv6"), max(len(r["ipv6"]) for r in display_rows)),
        "www":   4,
        "loc":   max(len("Loc"), max(len(r["loc"]) for r in display_rows)),
        "cf":    max(len("Location"), max(len(r["cf"]) for r in display_rows)),
    }

    # Заголовок
    header = f"{'Proxy':<{widths['proxy']}} | {'IPv4':<{widths['ipv4']}} | {'IPv6':<{widths['ipv6']}} | {'WWW':<{widths['www']}} | {'Loc':<{widths['loc']}} | {'Location':<{widths['cf']}}"
    print("\nIPv4/IPv6 доступность через прокси")
    print(header)
    print("-" * len(header))

    # Группированный вывод
    displayed = set()
    for group in groups:
        group_output = False
        for proxy in group:
            if proxy not in seen_proxies or proxy in displayed:
                continue
            displayed.add(proxy)
            r = next((x for x in display_rows if x["proxy"] == proxy), None)
            if not r:
                continue
            line = f"{r['proxy']:<{widths['proxy']}} | {r['ipv4']:<{widths['ipv4']}} | {r['ipv6']:<{widths['ipv6']}} | {r['www']:<{widths['www']}} | {r['loc']:<{widths['loc']}} | {r['cf']:<{widths['cf']}}"
            print(line)
            group_output = True
        if group_output:
            print()

# ========== ОСНОВНОЙ КОД ==========
def main(full=False, verbose=False):
    groups = load_proxies_from_file()
    results = []
    flat = []

    total = sum(len(g) for g in groups)
    counter = 0
    seen = set()

    for group in groups:
        for proxy in group:
            if proxy in seen:
                continue  # пропустить дубликаты
            seen.add(proxy)
            flat.append(proxy)
            counter += 1
            if verbose:
                print(f"[{counter}/{total}] Проверка {proxy}")
            r = check_proxy_connectivity(proxy, verbose=verbose)
            results.append(r)

    seen = set()
    deduped = []
    for r in results:
        key = (r["proxy"], r.get("IPv4", ""), r.get("IPv6", ""))
        if key not in seen:
            seen.add(key)
            deduped.append(r)

    print_grouped_results(groups, deduped)

# ========== ЗАПУСК ==========
if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Проверка прокси на поддержку IPv4/IPv6")
    parser.add_argument("--full", action="store_true", help="Показать все прокси и скрыть дубликаты")
    parser.add_argument("--verbose", action="store_true", help="Показывать прогресс при проверке")
    parser.add_argument("--file", default=PROXY_FILE, help="Файл с прокси (по умолчанию: proxies.txt)")
    args = parser.parse_args()

    PROXY_FILE = args.file
    main(full=args.full, verbose=args.verbose)