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

import time
import logging
import random
import requests
import socket
from contextlib import contextmanager
from collections import defaultdict
import re
import urllib3
from urllib.parse import urlparse
import ipaddress
import argparse

# Отключаем SSL-варнинги
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

# ========== ЛОГГЕРЫ ==========

logger = logging.getLogger("proxy_tester")
logger.setLevel(logging.INFO)
file_handler = logging.FileHandler("proxy_test_250805.log", mode="w", encoding="utf-8")
file_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)-8s %(message)s"))
logger.addHandler(file_handler)
console_handler = logging.StreamHandler()
console_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)-8s %(message)s"))
logger.addHandler(console_handler)

SUMMARY_PATH = "summary_report_250805.log"

# ========== КОНФИГУРАЦИЯ ==========

PROXIES = [
    {"name": "opera1eu",               "url": "http://192.168.1.111:18080"},
    {"name": "opera0usa",              "url": "http://192.168.0.111:18080"},
#    {"name": "warp1ipv6-http",        "url": "http://192.168.1.111:808"},
#    {"name": "warp1ipv6-socks",       "url": "socks5h://192.168.1.111:800"},
#    {"name": "warp0ipv4-http",        "url": "http://192.168.0.111:808"},
#    {"name": "warp0ipv4-socks",       "url": "socks5h://192.168.0.111:800"},
#    {"name": "usque-warp-http",       "url": "http://127.0.0.1:808"},
    {"name": "usque-warp-socks",       "url": "socks5h://127.0.0.1:800"},
    {"name": "usque-warp-socks-local", "url": "socks5://127.0.0.1:800"},
#    {"name": "CFON-warp-http",        "url": "http://127.0.0.1:8680"},
#    {"name": "CFON-warp-socks",       "url": "socks5h://127.0.0.1:8086"},
#    {"name": "VPN-http",              "url": "http://127.0.0.1:9898"},
    {"name": "VPN-socks",              "url": "socks5h://127.0.0.1:9696"},
    {"name": "VPN-socks-local",        "url": "socks5://127.0.0.1:9696"},
#    {"name": "clash_mixed-http",            "url": "http://127.0.0.1:2222"},
#    {"name": "clash_mixed",            "url": "socks5h://127.0.0.1:2222"},
#    {"name": "clash_mixed-local",      "url": "socks5://127.0.0.1:2222"},
    {"name": "clash_socks",            "url": "socks5h://127.0.0.1:3333"},
    {"name": "clash_socks-local",      "url": "socks5://127.0.0.1:3333"},
#    {"name": "clash_http",             "url": "http://127.0.0.1:4444"},
    {"name": "clash_7770_opera",       "url": "socks5h://127.0.0.1:7770"},
    {"name": "clash_7770_opera-local",       "url": "socks5://127.0.0.1:7770"},
    {"name": "clash_1110_IPv6",        "url": "socks5h://127.0.0.1:1110"},
    {"name": "clash_1110_IPv6-local",        "url": "socks5://127.0.0.1:1110"},
    {"name": "clash_5550_warp",        "url": "socks5h://127.0.0.1:5550"},
    {"name": "clash_5550_warp-local",        "url": "socks5://127.0.0.1:5550"},
    {"name": "clash_6660_VPN",         "url": "socks5h://127.0.0.1:6660"},
    {"name": "clash_6660_VPN-local",         "url": "socks5://127.0.0.1:6660"},
#    {"name": "PD_1122_opera1eu",      "url": "http://127.0.0.1:1122"},
#    {"name": "PD_1322_opera0usa",     "url": "http://127.0.0.1:1322"},
#    {"name": "PD_1522_warp1ipv6-http","url": "http://127.0.0.1:1522"},
#    {"name": "PD_1722_warp0ipv4-http","url": "http://127.0.0.1:1722"},
#    {"name": "PD_1922_usque-warp-http","url": "http://127.0.0.1:1922"},
#    {"name": "PD_2122_CFON-warp-http","url": "http://127.0.0.1:2122"},
#    {"name": "PD_2322_VPN-http",      "url": "http://127.0.0.1:2322"},
#    {"name": "PD_1155_warp1ipv6-socks","url": "socks5://127.0.0.1:1155"},
#    {"name": "PD_1355_warp0ipv4-socks","url": "socks5://127.0.0.1:1355"},
#    {"name": "PD_1555_usque-warp-socks","url": "socks5://127.0.0.1:1555"},
#    {"name": "PD_1755_CFON-warp-socks","url": "socks5://127.0.0.1:1755"},
#    {"name": "PD_1955_VPN-socks",     "url": "socks5://127.0.0.1:1955"},
#    {"name": "PD_1157_warp1ipv6-socks_rdr","url": "socks5h://127.0.0.1:1157"},
#    {"name": "PD_1357_warp0ipv4-socks_rdr","url": "socks5h://127.0.0.1:1357"},
#    {"name": "PD_1557_usque-warp-socks_rdr","url": "socks5h://127.0.0.1:1557"},
#    {"name": "PD_1757_CFON-warp-socks_rdr","url": "socks5h://127.0.0.1:1757"},
#    {"name": "PD_1957_VPN-socks_rdr", "url": "socks5h://127.0.0.1:1957"},
    {"name": "privoxy",       "url": "http://127.0.0.1:8118"},
]

# Таймауты
DEFAULT_CONNECT_TIMEOUT = 60
DEFAULT_READ_TIMEOUT = 300
LARGE_FILE_READ_TIMEOUT = 600

DOMAIN_MAPPING = {
    "example.com": {
        "fake_ip": "10.255.255.1",
        "real_ip": "93.184.216.34",
        "https": False,
        "test_redirect": False,
    },
    "opensource.org": {
        "fake_ip": "10.255.255.2",
        "real_ip": "140.211.15.130",
        "https": True,
        "test_redirect": False,
    },
    "apache.org": {
        "fake_ip": "10.255.255.4",
        "real_ip": "40.79.78.1",
        "https": True,
        "test_redirect": False,
    },
    "nginx.org": {
        "fake_ip": "10.255.255.5",
        "real_ip": "52.58.199.22",
        "https": True,
        "test_redirect": False,
    },
}

TEST_PATHS = ["/"]
RETRY_DELAY = 0.5

# Дополнительные проблемные домены
EXTRA_DOMAINS = {
#    "community.antifilter.download": {
#        "https": True,
#        "path": "/list/domains.lst",
#        "expected_status": [200, 301, 302],
#        "follow_redirects": True,
#        "large_file": True
#    },
#    "antifilter.download": {
#        "https": True,
#        "path": "/list/domains.lst",
#        "expected_status": [200, 301, 302],
#        "follow_redirects": True,
#        "large_file": True
#    },
#    "badmojr.gitlab.io": {
#        "https": True,
#        "path": "/1Hosts/Lite/domains.wildcards",
#        "expected_status": [200, 301, 302],
#        "follow_redirects": True,
#        "large_file": True
#    },
#    "badmojr.github.io": {
#        "https": True,
#        "path": "/1Hosts/Lite/domains.wildcards",
#        "expected_status": [200, 301, 302],
#        "follow_redirects": True,
#        "large_file": True
#    },
#    "raw.githubusercontent.com": {
#        "https": True,
#        "path": "/badmojr/1Hosts/master/Lite/domains.wildcards",
#        "expected_status": [200],
#        "follow_redirects": True,
#        "large_file": True
#    },
#    "cdn.jsdelivr.net": {
#        "https": True,
#        "path": "/gh/badmojr/1Hosts@master/Lite/domains.wildcards",
#        "expected_status": [200],
#        "follow_redirects": True,
#        "large_file": True
#    },
#    "download.cpuid.com": {
#        "https": True,
#        "path": "/",
#        "expected_status": [200, 301, 302],
#        "follow_redirects": True
#    },
#    "a.tampermonkey.net": {
#        "https": True,
#        "path": "/update-check.php?v=1&browser=chrome",
#        "expected_status": [200, 404],
#        "follow_redirects": False
#    },
#    "www.torproject.org": {
#        "https": True,
#        "path": "/download/tor/",
#        "expected_status": [200, 404],
#        "follow_redirects": False
#    },
    "ntc.party": {
        "https": True,
        "path": "/",
        "expected_status": [200, 301, 302],
        "follow_redirects": False
    },
}

DIAG_URLS = [
    {"url": "https://one.one.one.one/cdn-cgi/trace", "host": "one.one.one.one"},
    {"url": "https://1.1.1.1/cdn-cgi/trace", "host": "one.one.one.one"},
    {"url": "https://[2606:4700:4700::1111]/cdn-cgi/trace", "host": "one.one.one.one", "timeout": (10, 10)},
    {"url": "https://www.gstatic.com/generate_204", "host": "www.gstatic.com", "expected_status": [200, 204]},
]

# ========== ХРАНИЛИЩЕ ==========

from threading import Lock

summary_lock = Lock()
aggregate_lock = Lock()

all_results = []
counters_proxy = defaultdict(lambda: {'OK': 0, 'FAIL': defaultdict(int)})
counters_domain = defaultdict(lambda: {'OK': 0, 'FAIL': defaultdict(int)})
consistency_buffer = {}

# ========== УТИЛИТЫ ==========

def is_local_address(ip):
    """Проверяет, является ли IP адрес локальным"""
    try:
        ip = ip.split('%')[0]
        ip_obj = ipaddress.ip_address(ip)
        return ip_obj.is_private or ip_obj.is_loopback
    except ValueError:
        return False

def check_local_port(server, port):
    """Проверяет доступность локального порта"""
    try:
        family = socket.AF_INET6 if ':' in server else socket.AF_INET
        with socket.socket(family, socket.SOCK_STREAM) as s:
            s.settimeout(1)
            s.connect((server, port))
        return True
    except Exception:
        return False

def test_proxy_health(proxy_url):
    """Проверяет базовую работоспособность прокси"""
    try:
        session = requests.Session()
        session.trust_env = False
        session.proxies = {"http": proxy_url, "https": proxy_url}
        session.verify = False
        session.timeout = 10
        
        response = session.get("https://www.gstatic.com/generate_204")
        return response.status_code in [200, 204]
    except Exception:
        return False

@contextmanager
def override_dns(name, forced_ip):
    original = socket.getaddrinfo

    def patched(host, port, *args, **kwargs):
        if host == name:
            return original(forced_ip, port, *args, **kwargs)
        return original(host, port, *args, **kwargs)

    socket.getaddrinfo = patched
    try:
        yield
    finally:
        socket.getaddrinfo = original

def classify_exception(e):
    """Расширенная классификация ошибок"""
    from requests import exceptions as rexc
    if isinstance(e, rexc.ConnectTimeout):
        return "connect_timeout"
    if isinstance(e, rexc.ReadTimeout):
        return "read_timeout"
    if isinstance(e, rexc.ProxyError):
        return "proxy_error"
    if isinstance(e, rexc.SSLError):
        if "handshake failure" in str(e):
            return "ssl_handshake_failure"
        elif "certificate verify failed" in str(e):
            return "ssl_certificate_error"
        return "ssl_error"
    if isinstance(e, rexc.ConnectionError):
        return "connection_error"
    if isinstance(e, rexc.TooManyRedirects):
        return "too_many_redirects"
    return "other_error"

def fetch_with_retries(session, url, headers, timeout, allow_redirects, max_retries=2):
    backoff = 0.5
    for attempt in range(1, max_retries + 2):
        try:
            resp = session.get(
                url,
                headers=headers,
                timeout=timeout,
                allow_redirects=allow_redirects,
                stream=True
            )
            if resp.status_code == 200:
                resp.close()
            return resp, None, attempt
        except Exception as e:
            if attempt <= max_retries:
                logger.info(f"    attempt {attempt}/{max_retries+1} for {url} failed ({type(e).__name__}), backoff {backoff:.1f}s")
                time.sleep(backoff)
                backoff *= 2
                last_exc = e
                continue
            return None, e, attempt

def record_result(entry):
    with summary_lock:
        all_results.append(entry)

def update_counters(proxy_name, domain, result, reason=None):
    with aggregate_lock:
        if result == "OK":
            counters_proxy[proxy_name]['OK'] += 1
            counters_domain[domain]['OK'] += 1
        else:
            counters_proxy[proxy_name]['FAIL'][reason] += 1
            counters_domain[domain]['FAIL'][reason] += 1

def extract_ip_from_trace(text):
    """Извлекает только IP из ответа trace"""
    m = re.search(r"ip=([^\n\r ]+)", text)
    return m.group(1) if m else None

def get_redirect_chain(response):
    """Возвращает цепочку редиректов"""
    chain = []
    if response.history:
        for r in response.history:
            chain.append(f"{r.status_code} {r.url}")
        chain.append(f"{response.status_code} {response.url}")
    return chain

def analyze_results():
    """Анализирует результаты тестов"""
    ipv6_failures = sum(1 for r in all_results if '2606:4700:4700::1111' in r['domain'] and r['result'] == 'FAIL')
    large_file_failures = sum(1 for r in all_results if r['reason'] == 'read_timeout')
    
    logger.info("\nAdvanced Analysis:")
    logger.info(f"- IPv6 connectivity issues: {ipv6_failures} failures")
    logger.info(f"- Large file download issues: {large_file_failures} timeouts")
    if ipv6_failures > 0:
        logger.warning("Some proxies have IPv6 connectivity problems")
    if large_file_failures > 0:
        logger.warning("Consider increasing LARGE_FILE_READ_TIMEOUT (current: 600s)")

# ========== ТЕСТ ПРОКСИ ==========

def test_proxy(proxy, test_mode=None):
    parsed = urlparse(proxy["url"])
    server = parsed.hostname
    port = parsed.port or (80 if parsed.scheme == "http" else 443)
    
    if is_local_address(server):
        if not check_local_port(server, port):
            logger.warning(f"Proxy {proxy['name']} is not available, skipping")
            return
            
    if not test_proxy_health(proxy["url"]):
        logger.warning(f"Proxy {proxy['name']} failed health-check, skipping")
        return

    session = requests.Session()
    session.trust_env = False
    session.proxies = {"http": proxy["url"], "https": proxy["url"]}
    session.verify = False

    try:
        ua = requests.utils.default_headers().get("User-Agent", "python-requests/unknown")
        session.headers.update({"User-Agent": ua})
    except Exception:
        session.headers.update({"User-Agent": "python-requests/unknown"})

    logger.info(f"[{proxy['name']}] === start testing proxy {proxy['name']} ({proxy['url']}) ===")

    # Режимы тестирования
    run_domain_mapping = test_mode != "test" and test_mode != "all" and test_mode != "proxy"
    run_extra_domains = test_mode == "test" or test_mode == "all" or test_mode is None
    run_diag = test_mode == "proxy" or test_mode == "all" or test_mode is None

    # Проверка обычных доменов
    if run_domain_mapping:
        for domain, info in DOMAIN_MAPPING.items():
            scheme = "https" if info.get("https", False) else "http"
            timeout_tuple = (info.get("connect_timeout", DEFAULT_CONNECT_TIMEOUT), 
                           info.get("read_timeout", DEFAULT_READ_TIMEOUT))
            for label in ("fake_ip", "real_ip"):
                forced_ip = info[label]
                ip_type = "REAL" if label == "real_ip" else "FAKE"
                for path in TEST_PATHS:
                    url = f"{scheme}://{domain}{path}"
                    with override_dns(domain, forced_ip):
                        test_url(session, proxy, domain, ip_type, scheme, path, url, 
                               info.get("test_redirect", False), timeout_tuple)

    # Проверка проблемных доменов
    if run_extra_domains:
        for domain, config in EXTRA_DOMAINS.items():
            scheme = "https" if config.get("https", True) else "http"
            read_timeout = LARGE_FILE_READ_TIMEOUT if config.get("large_file", False) else DEFAULT_READ_TIMEOUT
            timeout_tuple = (DEFAULT_CONNECT_TIMEOUT, read_timeout)
            path = config.get("path", "/")
            url = f"{scheme}://{domain}{path}"
            test_url(session, proxy, domain, "N/A", scheme.upper(), path, url,
                    config.get("follow_redirects", True), timeout_tuple,
                    config.get("expected_status", [200]))

    # Диагностика специальных URL
    if run_diag:
        for diag in DIAG_URLS:
            url = diag["url"]
            host = diag["host"]
            timeout_tuple = diag.get("timeout", (DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT))
            expected_status = diag.get("expected_status", [200])
            test_url(session, proxy, urlparse(url).hostname, "N/A", "HTTPS", "/", url,
                    True, timeout_tuple, expected_status, host=host, is_diag=True)

def test_url(session, proxy, domain, ip_type, scheme, path, url, allow_redirects, 
            timeout_tuple, expected_statuses=None, host=None, is_diag=False):
    """Общая функция тестирования URL"""
    if expected_statuses is None:
        expected_statuses = [200]
    
    error_category = None
    status = None
    elapsed = None
    redirect_chain = None
    trace_ip = None
    extra_info = ""
    
    try:
        start_time = time.time()
        headers = {"Host": host} if host else {"Host": domain}
        resp = session.get(
            url,
            headers=headers,
            timeout=timeout_tuple,
            allow_redirects=allow_redirects,
            stream=True
        )
        elapsed = time.time() - start_time
        status = resp.status_code
        
        if resp.history:
            redirect_chain = get_redirect_chain(resp)
        
        if is_diag and "one.one.one.one" in url and status == 200:
            trace_ip = extract_ip_from_trace(resp.text)
            if trace_ip:
                extra_info = f"trace_ip={trace_ip}"
        
        if status in expected_statuses:
            ok = True
        else:
            ok = False
            if 300 <= status < 400:
                error_category = f"redirect_{status}"
            elif 400 <= status < 500:
                error_category = f"client_error_{status}"
            elif 500 <= status < 600:
                error_category = f"server_error_{status}"
            else:
                error_category = f"http_{status}"
                
    except Exception as e:
        error_category = classify_exception(e)
        status = "ERR"
        ok = False
    
    result = "OK" if ok else "FAIL"
    time_str = f"{elapsed:.2f}s" if elapsed is not None else "N/A"
    reason = error_category if error_category and not ok else ""
    
    log_msg = (
        f"[{proxy['name']}] {domain:30} {ip_type:4} {scheme:5} {path:15} "
        f"-> {result:4} status={status} time={time_str}"
    )
    if reason:
        log_msg += f" reason={reason}"
    if extra_info:
        log_msg += f" {extra_info}"
    if redirect_chain:
        log_msg += f" redirects={len(redirect_chain)-1} final={redirect_chain[-1].split()[1]}"
    logger.info(log_msg)

    entry = {
        "proxy": proxy["name"],
        "domain": domain,
        "ip_type": ip_type,
        "scheme": scheme,
        "path": path,
        "result": result,
        "status": status,
        "time": elapsed,
        "reason": reason,
        "consistency": extra_info,
        "trace_ip": trace_ip,
        "redirect_chain": " → ".join(redirect_chain) if redirect_chain else None,
    }
    record_result(entry)
    update_counters(proxy["name"], domain, result, reason)
    time.sleep(RETRY_DELAY)

# ========== ОТЧЕТЫ ==========

def dump_aggregation_and_summary():
    separator = "-" * 80
    logger.info(separator)
    
    # Статистика по прокси
    logger.info("AGGREGATION BY PROXY:")
    for proxy_name, counts in sorted(counters_proxy.items()):
        ok = counts['OK']
        fail_total = sum(counts['FAIL'].values())
        total = ok + fail_total
        
        if total == 0:
            continue
            
        ratio = f"{fail_total}/{total}"
        logger.info(f"{proxy_name:25} OK={ok:4} FAIL={fail_total:4} ({ratio})")
        
        if fail_total > 0:
            logger.info("  Failure reasons:")
            for reason, count in counts['FAIL'].items():
                logger.info(f"    {reason}: {count}")
                
        if total and fail_total / total > 0.5:
            logger.warning(f"[SUSPICIOUS PROXY] {proxy_name} high failure rate: {fail_total}/{total}")
    
    logger.info(separator)
    
    # Статистика по доменам
    logger.info("AGGREGATION BY DOMAIN:")
    for domain, counts in sorted(counters_domain.items()):
        ok = counts['OK']
        fail_total = sum(counts['FAIL'].values())
        total = ok + fail_total
        
        if total == 0:
            continue
            
        ratio = f"{fail_total}/{total}"
        logger.info(f"{domain:30} OK={ok:4} FAIL={fail_total:4} ({ratio})")
        
        if fail_total > 0:
            logger.info("  Failure reasons:")
            for reason, count in counts['FAIL'].items():
                logger.info(f"    {reason}: {count}")
                
        if total and fail_total / total > 0.5:
            logger.warning(f"[SUSPICIOUS DOMAIN] {domain} high failure rate: {fail_total}/{total}")
    
    logger.info(separator)
    analyze_results()

    # Запись в файл
    with summary_lock:
        with open(SUMMARY_PATH, "w", encoding="utf-8") as f:
            f.write("proxy\tdomain\tip_type\tscheme\tpath\tresult\tstatus\ttime\treason\tconsistency\tredirect_chain\ttrace_ip\n")
            for e in sorted(all_results, key=lambda x: (x["proxy"], x["domain"], x["path"])):
                f.write(f"{e['proxy']}\t{e['domain']}\t{e['ip_type']}\t{e['scheme']}\t{e['path']}\t"
                        f"{e['result']}\t{e['status']}\t{e.get('time', 'N/A')}\t{e.get('reason', '')}\t"
                        f"{e.get('consistency', '')}\t{e.get('redirect_chain', '')}\t{e.get('trace_ip', '')}\n")

# ========== MAIN ==========

def main():
    parser = argparse.ArgumentParser(description="Proxy Tester")
    parser.add_argument("--test", action="store_true", help="Test only problematic domains")
    parser.add_argument("--proxy", action="store_true", help="Test only diagnostic URLs")
    parser.add_argument("--all", action="store_true", help="Test both problematic and diagnostic without fake DNS")
    args = parser.parse_args()

    test_mode = None
    if args.test:
        test_mode = "test"
    elif args.proxy:
        test_mode = "proxy"
    elif args.all:
        test_mode = "all"

    with open(SUMMARY_PATH, "w", encoding="utf-8") as f:
        f.write("proxy\tdomain\tip_type\tscheme\tpath\tresult\tstatus\ttime\treason\tconsistency\tredirect_chain\ttrace_ip\n")

    for proxy in PROXIES:
        try:
            test_proxy(proxy, test_mode)
        except Exception as e:
            logger.error(f"Critical error testing {proxy['name']}: {str(e)}")

    dump_aggregation_and_summary()

if __name__ == '__main__':
    main()