import os
import sys
import subprocess
import importlib.util
import random
import re
import time
import hashlib
import webbrowser
from datetime import datetime
from html import escape as html_esc
from concurrent.futures import ThreadPoolExecutor, as_completed
from urllib.parse import quote_plus

def auto_install_modules():
    required = {
        'requests': 'requests',
        'bs4': 'beautifulsoup4',
        'colorama': 'colorama',
        'geopy': 'geopy',
        'pystyle': 'pystyle',
    }
    for import_name, pip_name in required.items():
        try:
            if importlib.util.find_spec(import_name) is None:
                subprocess.check_call(
                    [sys.executable, '-m', 'pip', 'install', pip_name, '--quiet']
                )
                print(f"[+] Установлен: {pip_name}")
        except Exception as e:
            print(f"[!] Ошибка установки {pip_name}: {e}")

auto_install_modules()

import requests
from colorama import init, Fore, Style
from geopy.geocoders import Nominatim
from pystyle import Colorate, Colors, Center

init(autoreset=True)

AUTHOR = "zxclinux"
TG_CHANNEL = "https://t.me/zxclinuxchannel"
TG_AUTHOR = "https://t.me/zxclinux"
VERSION = "3.2"
BASE_DIR = "base"
RESULTS_DIR = "results"
_FIRST_BOOT = True

CYAN = Fore.CYAN
BLUE = Fore.BLUE
WHITE = Fore.WHITE
YELLOW = Fore.YELLOW
GREEN = Fore.GREEN
RED = Fore.RED
MAGENTA = Fore.MAGENTA
DIM = Style.DIM
BRIGHT = Style.BRIGHT

BANNER_ART = r"""
███████╗██╗  ██╗ ██████╗██╗     ██╗███╗   ██╗██╗   ██╗██╗  ██╗
╚══███╔╝╚██╗██╔╝██╔════╝██║     ██║████╗  ██║██║   ██║╚██╗██╔╝
  ███╔╝  ╚███╔╝ ██║     ██║     ██║██╔██╗ ██║██║   ██║ ╚███╔╝ 
 ███╔╝   ██╔██╗ ██║     ██║     ██║██║╚██╗██║██║   ██║ ██╔██╗ 
███████╗██╔╝ ██╗╚██████╗███████╗██║██║ ╚████║╚██████╔╝██╔╝ ██╗
╚══════╝╚═╝  ╚═╝ ╚═════╝╚══════╝╚═╝╚═╝  ╚═══╝ ╚═════╝ ╚═╝  ╚═╝
"""


# ─── UI / animations ────────────────────────────────────────────────────────

def clear():
    os.system('cls' if os.name == 'nt' else 'clear')


def tag(symbol, color=CYAN):
    return f"{BLUE}[{color}{symbol}{BLUE}]"


def pause():
    input(f"\n    {DIM}{CYAN}▸{WHITE} Enter — назад в меню...{Style.RESET_ALL}")


def type_out(text, delay=0.012):
    for ch in text:
        sys.stdout.write(ch)
        sys.stdout.flush()
        time.sleep(delay)
    print()


def sleep_ms(ms):
    time.sleep(ms / 1000.0)


def progress_bar(current, total, width=28, label=""):
    ratio = 0 if total == 0 else current / total
    filled = int(width * ratio)
    bar = f"{CYAN}{'█' * filled}{DIM}{'░' * (width - filled)}"
    pct = int(ratio * 100)
    print(f"\r    {tag('~')} {label}{bar}{WHITE} {pct:3d}%  {CYAN}{current}/{total}   ", end='', flush=True)
    if current >= total:
        print()


def spinner_once(frame, text):
    frames = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
    print(f"\r    {CYAN}{frames[frame % len(frames)]}{WHITE} {text}   ", end='', flush=True)


def loading_bar(title="загрузка", steps=24, delay=0.035):
    print(f"    {tag('*')} {WHITE}{title}")
    for i in range(steps + 1):
        progress_bar(i, steps, width=32, label="")
        sleep_ms(int(delay * 1000))


def reveal_line(text, delay=0.04):
    print(text)
    sleep_ms(int(delay * 1000))


def section_title(icon, title):
    print()
    print(f"    {BLUE}╭{'─' * 52}╮")
    print(f"    {BLUE}│  {CYAN}{icon}  {BRIGHT}{WHITE}{title:<46}{BLUE}│")
    print(f"    {BLUE}╰{'─' * 52}╯")
    print()


def result_box_open():
    print(f"    {BLUE}┌{'─' * 52}┐")


def result_box_close():
    print(f"    {BLUE}└{'─' * 52}┘")


def anim_boot():
    clear()
    print()
    type_out(f"    {DIM}{CYAN}initializing zxclinux osint...{Style.RESET_ALL}", 0.02)
    sleep_ms(200)
    for i, msg in enumerate(["modules", "network", "engines", "ui layer"]):
        for f in range(6):
            spinner_once(f, f"loading {msg}")
            sleep_ms(40)
        print(f"\r    {GREEN}✓{WHITE} {msg:<12} {DIM}ok{Style.RESET_ALL}          ")
    print()
    loading_bar("boot sequence", steps=20, delay=0.028)
    sleep_ms(150)


def print_banner(animated=False):
    lines = BANNER_ART.strip('\n').split('\n')
    print()
    for i, line in enumerate(lines):
        colored = Colorate.Horizontal(Colors.cyan_to_blue, Center.XCenter(line))
        if animated:
            print(colored)
            sleep_ms(45)
        else:
            print(colored)

    info_lines = [
        f"╔══════════════════════════════════════════════════════════╗",
        f"║  ZXCLINUX OSINT  v{VERSION:<6}        author: {AUTHOR:<12} ║",
        f"║  TG: t.me/zxclinuxchannel                                ║",
        f"╚══════════════════════════════════════════════════════════╝",
    ]
    print()
    for line in info_lines:
        colored = Colorate.Horizontal(Colors.blue_to_cyan, Center.XCenter(line))
        if animated:
            print(colored)
            sleep_ms(35)
        else:
            print(colored)


def print_menu(animated=False):
    rows = [
        ("01", "Поиск по номеру", "06", "Инфо о домене"),
        ("02", "Поиск по IP", "07", "Статистика баз"),
        ("03", "Поиск по никнейму", "08", "TG-канал"),
        ("04", "Поиск по базам", "09", "Об авторе"),
        ("05", "Проверка email", "00", "Выход"),
    ]
    top = f"    {BLUE}┌{'─' * 61}┐"
    bot = f"    {BLUE}└{'─' * 61}┘"
    if animated:
        reveal_line(top, 0.03)
    else:
        print(top)

    for a, ta, b, tb in rows:
        line = (
            f"    {BLUE}│  {CYAN}{a}{WHITE}  {ta:<22}"
            f"  {CYAN}{b}{WHITE}  {tb:<18}{BLUE}│"
        )
        if animated:
            reveal_line(line, 0.045)
        else:
            print(line)

    if animated:
        reveal_line(bot, 0.03)
    else:
        print(bot)
    print()


# ─── helpers ────────────────────────────────────────────────────────────────

def get_random_user_agent():
    agents = [
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
        'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15',
        'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0',
    ]
    return random.choice(agents)


def headers():
    return {'User-Agent': get_random_user_agent(), 'Accept-Language': 'ru-RU,ru;q=0.9,en;q=0.8'}


def ensure_dirs():
    os.makedirs(BASE_DIR, exist_ok=True)
    os.makedirs(RESULTS_DIR, exist_ok=True)


def open_url(url):
    try:
        webbrowser.open(url)
        print(f"    {tag('+')} {WHITE}Открыто: {CYAN}{url}")
    except Exception as e:
        print(f"    {tag('X', RED)} {RED}Не удалось открыть браузер: {e}")
        print(f"    {tag('!')} {WHITE}Ссылка: {CYAN}{url}")


def save_results(name, content):
    ensure_dirs()
    stamp = datetime.now().strftime('%Y%m%d_%H%M%S')
    path = os.path.join(RESULTS_DIR, f"{name}_{stamp}.txt")
    with open(path, 'w', encoding='utf-8') as f:
        f.write(content)
    print(f"    {tag('+')} {WHITE}Сохранено: {GREEN}{path}")
    return path


def save_html_report(name, title, query, kind, checks, rows, extra_html=""):
    """Self-contained HTML mini-report (zxclinux / hloridat branding)."""
    ensure_dirs()
    stamp = datetime.now().strftime('%Y%m%d_%H%M%S')
    path = os.path.join(RESULTS_DIR, f"{name}_{stamp}.html")
    check_html = ""
    for ok, label, detail in checks:
        cls = "ok" if ok else "no"
        check_html += (
            f'<div class="check {cls}"><b>{html_esc(label)}</b>'
            f'{html_esc(detail)}</div>\n'
        )
    cards = ""
    for state, src, url, info, http in rows:
        cls = "live" if state == "found" else ("dead" if state == "dead" else "maybe")
        http_s = f"HTTP {http}" if http else ("?" if state == "unknown" else "—")
        cards += f'''<article class="card {cls}">
  <div class="tag">{html_esc(src)}</div>
  <div>
    <a href="{html_esc(url)}" target="_blank" rel="noopener">{html_esc(url)}</a>
    <div class="meta">{html_esc(info or "")}</div>
  </div>
  <div class="http">{html_esc(http_s)}</div>
</article>
'''
    now = datetime.now().strftime('%Y-%m-%d %H:%M')
    html = f'''<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>{html_esc(query)} · zxclinux OSINT</title>
<style>
:root {{ --bg:#060305; --ink:#f3ecec; --muted:#9a8084; --red:#d01828; --line:#2a1518; }}
* {{ box-sizing:border-box }}
body {{ margin:0; background:radial-gradient(900px 420px at 10% -10%, rgba(208,24,40,.16), transparent 55%), var(--bg);
  color:var(--ink); font:14px/1.45 "IBM Plex Mono", Consolas, monospace; }}
.wrap {{ max-width:880px; margin:0 auto; padding:28px 18px 48px; }}
.brand {{ color:var(--red); text-transform:uppercase; letter-spacing:.08em; font-weight:700; text-decoration:none; }}
h1 {{ font-size:2rem; text-transform:uppercase; margin:18px 0 6px; }}
.sub {{ color:var(--muted); font-size:.85rem; }}
.panel {{ margin-top:22px; border:1px solid var(--line); background:#080405; padding:14px 16px; }}
h2 {{ margin:0 0 12px; font-size:.72rem; letter-spacing:.12em; text-transform:uppercase; color:#c9898e; }}
.checks {{ display:flex; flex-wrap:wrap; gap:8px; }}
.check {{ border:1px solid var(--line); padding:8px 10px; font-size:.78rem; }}
.check b {{ color:var(--red); margin-right:6px; font-size:.68rem; text-transform:uppercase; }}
.check.ok {{ border-color:rgba(47,191,113,.45); }}
.card {{ display:grid; grid-template-columns:92px 1fr auto; gap:10px; padding:10px 12px;
  border:1px solid rgba(255,255,255,.12); background:#10080a; margin-bottom:8px; }}
.card.live {{ border-color:rgba(47,191,113,.55); }}
.card.dead {{ opacity:.5; }}
.card a {{ color:#7be3a8; text-decoration:none; word-break:break-all; }}
.tag {{ font-size:.62rem; letter-spacing:.08em; text-transform:uppercase; color:#c9898e; }}
.meta {{ color:var(--muted); font-size:.72rem; margin-top:4px; }}
.http {{ font-size:.72rem; color:var(--muted); }}
.foot {{ margin-top:28px; color:var(--muted); font-size:.72rem; border-top:1px solid var(--line); padding-top:14px;
  display:flex; justify-content:space-between; gap:10px; flex-wrap:wrap; }}
nav a {{ color:var(--muted); margin-left:14px; font-size:.78rem; }}
</style>
</head>
<body>
<div class="wrap">
  <div style="display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--line);padding-bottom:14px">
    <a class="brand" href="https://hloridat.uk/#osint">zxclinux</a>
    <nav>
      <a href="https://hloridat.uk/osint/report.html">html report</a>
      <a href="https://hloridat.uk">hloridat.uk</a>
    </nav>
  </div>
  <p style="color:var(--red);font-size:.72rem;letter-spacing:.14em;text-transform:uppercase;margin:28px 0 6px">zxclinux OSINT · {html_esc(kind)}</p>
  <h1>{html_esc(title)}</h1>
  <p class="sub">query: <b>{html_esc(query)}</b> · {html_esc(now)} · public live-verify only</p>
  <section class="panel">
    <h2>validity checks</h2>
    <div class="checks">{check_html or "<div class='check'>none</div>"}</div>
  </section>
  <section class="panel">
    <h2>results</h2>
    {cards or "<p class='sub'>no hits</p>"}
    {extra_html}
  </section>
  <div class="foot"><span>zxclinux · hloridat.uk</span><span>v{VERSION}</span></div>
</div>
</body>
</html>
'''
    with open(path, 'w', encoding='utf-8') as f:
        f.write(html)
    print(f"    {tag('+')} {WHITE}HTML-отчёт: {GREEN}{path}")
    return path


def _list_base_files():
    if not os.path.isdir(BASE_DIR):
        return []
    files = []
    for name in sorted(os.listdir(BASE_DIR)):
        path = os.path.join(BASE_DIR, name)
        if os.path.isfile(path):
            files.append((name, path))
    return files


# ─── Phone ──────────────────────────────────────────────────────────────────

def normalize_phone(raw: str) -> str:
    digits = re.sub(r'\D', '', raw)
    if digits.startswith('8') and len(digits) == 11:
        digits = '7' + digits[1:]
    if digits.startswith('7') and len(digits) == 11:
        return '+' + digits
    if len(digits) >= 10:
        return '+' + digits
    return digits


def phone_variants(number: str):
    digits = re.sub(r'\D', '', number)
    variants = {digits, number}
    if digits.startswith('7') and len(digits) == 11:
        variants.add('8' + digits[1:])
        variants.add('+' + digits)
        variants.add(digits[1:])
    elif digits.startswith('8') and len(digits) == 11:
        variants.add('7' + digits[1:])
        variants.add('+7' + digits[1:])
        variants.add(digits[1:])
    return [v for v in variants if v]


def search_phone():
    section_title("☎", "Поиск по номеру телефона")
    raw = input(f"    {tag('?')} {WHITE}Номер: ").strip()
    if not raw:
        print(f"    {tag('X', RED)} {RED}Номер не введён")
        return

    number = normalize_phone(raw)
    digits = re.sub(r'\D', '', number)
    print(f"    {tag('!')} {WHITE}Запрос по {CYAN}{number}{WHITE}...")
    for f in range(10):
        spinner_once(f, "опрос оператора / гео")
        sleep_ms(50)
    print(f"\r    {GREEN}✓{WHITE} данные получены                    ")

    out_lines = [f"Номер: {number}", f"Автор: {AUTHOR}", ""]

    try:
        r = requests.get(
            f'https://htmlweb.ru/geo/api.php?json&telcod={digits}',
            headers=headers(),
            timeout=12,
        )
        r.raise_for_status()
        data = r.json()
        print()
        result_box_open()
        print(f"    {BLUE}│  {WHITE}Номер: {CYAN}{number}")
        for title, key in [("Страна", "country"), ("Регион", "region"), ("Оператор", "0")]:
            block = data.get(key) or {}
            print(f"    {BLUE}│  {YELLOW}{title}")
            out_lines.append(f"[{title}]")
            if isinstance(block, dict) and block:
                for k, v in block.items():
                    print(f"    {BLUE}│    {WHITE}{k}: {CYAN}{v or '—'}")
                    out_lines.append(f"  {k}: {v or '—'}")
            else:
                print(f"    {BLUE}│    {DIM}нет данных")
                out_lines.append("  нет данных")
        result_box_close()
    except requests.RequestException as e:
        print(f"    {tag('X', RED)} {RED}API оператора: {e}")

    ensure_dirs()
    files = _list_base_files()
    db_hits = []
    if files:
        print(f"\n    {tag('!')} {WHITE}Сканирование локальных баз...")
        variants = phone_variants(number)
        for label, path in files:
            try:
                with open(path, 'r', encoding='utf-8', errors='ignore') as f:
                    for num, line in enumerate(f, 1):
                        low = line.lower()
                        if any(v.lower() in low for v in variants):
                            clean = line.strip().replace('NULL', '—')
                            db_hits.append((label, num, clean))
                            print(f"    {GREEN}▸ [{label}:{num}] {WHITE}{clean[:150]}")
                            sleep_ms(20)
                            if len(db_hits) >= 40:
                                break
                if len(db_hits) >= 40:
                    break
            except OSError:
                pass
        print(f"    {tag('+')} {WHITE}В базах: {CYAN}{len(db_hits)}")
        out_lines.append(f"\nБазы: {len(db_hits)} совпадений")
        for label, num, clean in db_hits:
            out_lines.append(f"[{label}:{num}] {clean}")
    else:
        print(f"\n    {tag('-')} {YELLOW}Папка base/ пуста")

    q = quote_plus(number)
    print(f"\n    {tag('*')} {YELLOW}Ссылки:")
    links = [
        ("Google", f"https://www.google.com/search?q=%22{q}%22"),
        ("Yandex", f"https://yandex.ru/search/?text=%22{q}%22"),
        ("DuckDuckGo", f"https://duckduckgo.com/?q={q}"),
        ("Telegram", f"https://t.me/+{digits}"),
        ("WhatsApp", f"https://wa.me/{digits}"),
        ("Truecaller", f"https://www.truecaller.com/search/ru/{digits}"),
        ("Getcontact", f"https://www.getcontact.com/en/search?q=%2B{digits}"),
        ("VK search", f"https://www.google.com/search?q=site%3Avk.com+{q}"),
        ("OK search", f"https://www.google.com/search?q=site%3Aok.ru+{q}"),
    ]
    for name, url in links:
        print(f"    {CYAN}  → {name}: {url}")
        out_lines.append(url)
        sleep_ms(20)

    variants = phone_variants(number)
    checks = [
        (len(digits) >= 10 and len(digits) <= 15, "format", f"{len(digits)} digits"),
        (True, "e164", number),
        (True, "variants", " · ".join(variants)),
        (True, "note", "t.me / wa.me = link-shape only, no inbox"),
    ]
    rows = [("unknown", n, u, "", 0) for n, u in links]
    html_path = save_html_report(f'phone_{digits}', number, number, "phone", checks, rows)
    if input(f"\n    {tag('?')} {WHITE}Сохранить txt? (y/n): ").strip().lower() == 'y':
        save_results(f'phone_{digits}', '\n'.join(out_lines))
    if input(f"    {tag('?')} {WHITE}Открыть HTML-отчёт? (y/n): ").strip().lower() == 'y':
        open_url('file:///' + os.path.abspath(html_path).replace('\\', '/'))


# ─── IP ─────────────────────────────────────────────────────────────────────

def iplook():
    section_title("◈", "Поиск по IP-адресу")
    ip = input(f"    {tag('?')} {WHITE}IP-адрес: ").strip()
    if not ip:
        print(f"    {tag('X', RED)} {RED}IP не введён")
        return

    for f in range(12):
        spinner_once(f, f"lookup {ip}")
        sleep_ms(45)
    print()

    try:
        r = requests.get(f"https://ipapi.co/{ip}/json/", headers=headers(), timeout=12)
        r.raise_for_status()
        data = r.json()
        if data.get("error"):
            print(f"    {tag('X', RED)} {RED}{data.get('reason', 'Неверный IP')}")
            return

        fields = [
            ("IP", ip),
            ("Сеть", data.get("network")),
            ("Город", data.get("city")),
            ("Регион", data.get("region")),
            ("Страна", data.get("country_name")),
            ("Индекс", data.get("postal")),
            ("Широта", data.get("latitude")),
            ("Долгота", data.get("longitude")),
            ("Часовой пояс", data.get("timezone")),
            ("Организация", data.get("org")),
            ("ASN", data.get("asn")),
        ]
        out = [f"IP lookup: {ip}\n"]
        result_box_open()
        for name, val in fields:
            print(f"    {BLUE}│  {WHITE}{name:<16} {CYAN}{val or '—'}")
            out.append(f"{name}: {val or '—'}")
            sleep_ms(25)
        result_box_close()

        lat, lon = data.get("latitude"), data.get("longitude")
        if lat and lon:
            try:
                geo = Nominatim(user_agent="zxclinux-osint")
                loc = geo.reverse(f"{lat}, {lon}", language='ru', timeout=10)
                addr = loc.address if loc else '—'
                maps = f"https://www.google.com/maps/search/{lat}+{lon}"
                print(f"\n    {tag('+')} {WHITE}Адрес: {CYAN}{addr}")
                print(f"    {tag('+')} {WHITE}Карты: {CYAN}{maps}")
                out.append(f"Адрес: {addr}\nКарты: {maps}")
            except Exception:
                pass

        if input(f"\n    {tag('?')} {WHITE}Сохранить? (y/n): ").strip().lower() == 'y':
            save_results('ip', '\n'.join(out))
    except requests.RequestException as e:
        print(f"    {tag('X', RED)} {RED}Ошибка: {e}")


# ─── Nickname ───────────────────────────────────────────────────────────────

NICK_SITES = {
    "GitHub":     ("https://github.com/{q}", ["page not found", "not found"]),
    "GitLab":     ("https://gitlab.com/{q}", ["sign in", "page not found"]),
    "Bitbucket":  ("https://bitbucket.org/{q}", ["page not found", "resource not found"]),
    "Codeberg":   ("https://codeberg.org/{q}", ["page not found", "sign in"]),
    "Reddit":     ("https://www.reddit.com/user/{q}", ["page not found", "sorry, nobody"]),
    "Telegram":   ("https://t.me/{q}", ["if you have telegram", "tgme_page_description\">\n"]),
    "VK":         ("https://vk.com/{q}", ["ошибка", "page_service_wrap"]),
    "OK":         ("https://ok.ru/{q}", ["страница не найдена", "page not found"]),
    "Facebook":   ("https://www.facebook.com/{q}", ["page isn't available", "content not found"]),
    "Instagram":  ("https://www.instagram.com/{q}/", ["sorry, this page isn't available"]),
    "TikTok":     ("https://www.tiktok.com/@{q}", ["couldn't find this account"]),
    "YouTube":    ("https://www.youtube.com/@{q}", ["this page isn't available", "404"]),
    "Twitch":     ("https://www.twitch.tv/{q}", ["sorry. unless"]),
    "Kick":       ("https://kick.com/{q}", ["page not found", "doesn't exist"]),
    "Steam":      ("https://steamcommunity.com/id/{q}", ["the specified profile could not be found", "error_ctn"]),
    "Habr":       ("https://habr.com/ru/users/{q}", ["страница не найдена", "404"]),
    "Pikabu":     ("https://pikabu.ru/@{q}", ["страница не найдена", "page not found"]),
    "SoundCloud": ("https://soundcloud.com/{q}", ["we can't find that user"]),
    "Spotify":    ("https://open.spotify.com/user/{q}", ["page not found"]),
    "Bandcamp":   ("https://bandcamp.com/{q}", ["sorry, that something isn't here"]),
    "Pinterest":  ("https://www.pinterest.com/{q}/", ["page not found"]),
    "Snapchat":   ("https://www.snapchat.com/add/{q}", ["page not found", "doesn't exist"]),
    "Threads":    ("https://www.threads.net/@{q}", ["page isn't available", "not found"]),
    "Linktree":   ("https://linktr.ee/{q}", ["this page is missing", "couldn't find"]),
    "About.me":   ("https://about.me/{q}", ["page not found", "doesn't exist"]),
    "Keybase":    ("https://keybase.io/{q}", ["uh oh", "user not found"]),
    "Pastebin":   ("https://pastebin.com/u/{q}", ["not found", "error"]),
    "Roblox":     ("https://www.roblox.com/users/profile?username={q}", ["page cannot be found", "error-page"]),
    "Medium":     ("https://medium.com/@{q}", ["page not found", "out of nothing"]),
    "Dev.to":     ("https://dev.to/{q}", ["page not found", "doesn't exist"]),
    "Last.fm":    ("https://www.last.fm/user/{q}", ["page not found", "doesn't exist"]),
    "Lichess":    ("https://lichess.org/@/{q}", ["page not found"]),
    "Chess.com":  ("https://www.chess.com/member/{q}", ["page not found", "doesn't exist"]),
    "DeviantArt": ("https://www.deviantart.com/{q}", ["page not found", "doesn't exist"]),
    "Behance":    ("https://www.behance.net/{q}", ["page not found", "doesn't exist"]),
    "Dribbble":   ("https://dribbble.com/{q}", ["page not found", "doesn't exist"]),
    "npm":        ("https://www.npmjs.com/~{q}", ["not found", "couldn't find"]),
    "PyPI":       ("https://pypi.org/user/{q}/", ["not found", "404"]),
    "DockerHub":  ("https://hub.docker.com/u/{q}", ["not found", "404"]),
    "HuggingFace":("https://huggingface.co/{q}", ["page not found", "doesn't exist"]),
    "Xbox":       ("https://xboxgamertag.com/search/{q}", ["not found", "no gamertag"]),
    "PSN":        ("https://psnprofiles.com/{q}", ["user not found", "doesn't exist"]),
    "Epic":       ("https://fortnitetracker.com/profile/all/{q}", ["player not found", "no stats"]),
    "DTF":        ("https://dtf.ru/{q}", ["страница не найдена", "page not found"]),
    "Boosty":     ("https://boosty.to/{q}", ["page not found", "не найден"]),
    "Discord":    ("https://discord.com/@{q}", ["page not found"]),
}


def nick_variants(nick: str):
    s = {nick, nick.lower()}
    s.add(nick.replace('.', ''))
    s.add(nick.replace('_', '-'))
    s.add(nick.replace('-', '_'))
    s.add(re.sub(r'[.\-_]', '', nick))
    trans = str.maketrans({'0': 'o', '1': 'i', '3': 'e', '4': 'a', '5': 's', '7': 't', '8': 'b'})
    s.add(nick.lower().translate(trans))
    s.add(re.sub(r'\d+$', '', nick))
    s.add(re.sub(r'\d+', '', nick))
    if not re.search(r'\d', nick) and len(nick) >= 3:
        s.add(nick.lower().replace('o', '0').replace('e', '3').replace('a', '4'))
    return [v for v in s if 2 <= len(v) <= 39][:6]


def _site_key(name: str) -> str:
    return name.split(' · ')[0]


def _nick_exists(name: str, response, nick: str) -> bool:
    code = response.status_code
    if code in (404, 410, 451):
        return False
    if code >= 400:
        return False
    if code != 200:
        return False

    body = (response.text or '').lower()
    final = (response.url or '').lower()
    key = _site_key(name)

    bad_redirects = ('/login', '/signin', '/join', '/search', 'accounts.google')
    if any(b in final and nick.lower() not in final for b in bad_redirects):
        return False

    markers = NICK_SITES[key][1]
    if any(m in body for m in markers):
        if key == "Telegram":
            if nick.lower() in body and 'tgme_page_title' in body:
                return True
            if 'if you have' in body and 'tgme_page_photo' not in body:
                return False
        else:
            return False

    if key == "GitHub" and f"github.com/{nick.lower()}" in final:
        return True
    if key == "Discord":
        return None
    return True


def _github_extra(session, nick: str) -> str:
    try:
        r = session.get(f"https://api.github.com/users/{nick}", timeout=4)
        if r.status_code != 200:
            return ""
        j = r.json()
        bits = []
        if j.get("name"):
            bits.append(j["name"])
        if j.get("bio"):
            bits.append(str(j["bio"])[:80])
        if j.get("public_repos") is not None:
            bits.append(f"repos {j['public_repos']}")
        if j.get("followers") is not None:
            bits.append(f"fol {j['followers']}")
        if j.get("created_at"):
            bits.append(str(j["created_at"])[:10])
        return " · ".join(bits)
    except Exception:
        return ""


def _uuid_extra(session, nick: str) -> str:
    try:
        r = session.get(f"https://api.minetools.eu/uuid/{nick}", timeout=4)
        if r.status_code != 200:
            return ""
        j = r.json()
        if j.get("id") and j.get("id") != "null":
            return f"uuid {j['id']}"
        return ""
    except Exception:
        return ""


def _probe_one(name, tmpl, nick, session):
    url = tmpl.format(q=quote_plus(nick) if 'username=' in tmpl else nick)
    try:
        r = session.get(url, timeout=4, allow_redirects=True)
        exists = _nick_exists(name, r, nick)
        info = ""
        if exists and _site_key(name) == "GitHub":
            info = _github_extra(session, nick)
        if exists and _site_key(name) == "Steam" and r.text:
            m = re.search(r"<steamID64>(\d+)<", r.text)
            if m:
                info = f"steamID64 {m.group(1)}"
        clean_url = r.url.split('?')[0].rstrip('/')
        if name == "Roblox":
            clean_url = url
        return name, clean_url, exists, r.status_code, info
    except requests.RequestException:
        return name, url, None, 0, "timeout"


def search_nick():
    section_title("@", "Поиск по никнейму")
    print(f"    {DIM}параллельно · таймаут 4с · только живые профили{Style.RESET_ALL}")
    nick = input(f"    {tag('?')} {WHITE}Никнейм: ").strip().lstrip('@')
    if not nick:
        print(f"    {tag('X', RED)} {RED}Никнейм не введён")
        return

    variants = nick_variants(nick)
    print(f"\n    {tag('!')} {WHITE}Сканирую {CYAN}{len(NICK_SITES)}{WHITE} площадок · варианты: {CYAN}{', '.join(variants)}\n")

    session = requests.Session()
    session.headers.update(headers())
    try:
        from requests.adapters import HTTPAdapter
        ad = HTTPAdapter(pool_connections=20, pool_maxsize=20)
        session.mount("https://", ad)
        session.mount("http://", ad)
    except Exception:
        pass
    found = []
    dead = []
    unknown = []
    done = 0

    extra_vars = [v for v in variants if v.lower() != nick.lower()][:2]
    extra_n = len(extra_vars) * 4
    total = len(NICK_SITES) + extra_n

    with ThreadPoolExecutor(max_workers=16) as pool:
        futs = {
            pool.submit(_probe_one, name, tmpl, nick, session): name
            for name, (tmpl, _) in NICK_SITES.items()
        }
        for v in extra_vars:
            for name in ("GitHub", "Telegram", "VK", "Reddit"):
                tmpl = NICK_SITES[name][0]
                futs[pool.submit(_probe_one, f"{name} · {v}", tmpl, v, session)] = f"{name} · {v}"
        for fut in as_completed(futs):
            done += 1
            name, url, exists, code, info = fut.result()
            label = name[:12]
            progress_bar(done, total, width=30, label=f"{CYAN}{label:<12}{WHITE} ")
            row = (name, url, exists, code, info)
            if exists is True:
                found.append(row)
            elif exists is False:
                dead.append(row)
            else:
                unknown.append(row)

    mc = _uuid_extra(session, nick)
    if mc:
        found.append(("Minecraft", f"https://namemc.com/profile/{nick}", True, 200, mc))
    else:
        dead.append(("Minecraft", f"https://namemc.com/profile/{nick}", False, 200, ""))

    print(f"\n    {GREEN}✓{WHITE} Живых: {CYAN}{len(found)}{WHITE}  ·  мёртвых: {len(dead)}  ·  ? {len(unknown)}\n")
    if found:
        result_box_open()
        for name, url, _, code, info in found:
            short = url.replace('https://', '').replace('http://', '')
            extra = f" {DIM}{info}{Style.RESET_ALL}" if info else ""
            print(f"    {BLUE}│  {GREEN}● {WHITE}{name:<12} {CYAN}{short}{WHITE} {DIM}HTTP {code}{extra}")
        result_box_close()
    else:
        print(f"    {tag('-')} {YELLOW}Нигде не найдено.")

    checks = [
        (len(nick) >= 2, "format", f"username «{nick}»"),
        (2 <= len(nick) <= 39, "length", f"{len(nick)} chars"),
        (True, "variants", " · ".join(variants)),
        (len(found) > 0, "live", f"{len(found)} profiles"),
    ]
    html_rows = []
    for name, url, _, code, info in found:
        html_rows.append(("found", name, url, info, code))
    for name, url, _, code, info in unknown:
        html_rows.append(("unknown", name, url, info, code))
    for name, url, _, code, info in dead:
        html_rows.append(("dead", name, url, info, code))

    html_path = save_html_report(
        f'nick_{nick}', nick, nick, "nickname", checks, html_rows
    )
    text = f"Ник: {nick}\nАвтор: {AUTHOR}\n\n" + '\n'.join(
        f"{n}: {u}  HTTP {c}  {i}" for n, u, _, c, i in found
    )
    if input(f"\n    {tag('?')} {WHITE}Сохранить txt? (y/n): ").strip().lower() == 'y':
        save_results(f'nick_{nick}', text)
    if input(f"    {tag('?')} {WHITE}Открыть HTML-отчёт? (y/n): ").strip().lower() == 'y':
        open_url('file:///' + os.path.abspath(html_path).replace('\\', '/'))


# ─── Database ───────────────────────────────────────────────────────────────

def search_in_bases():
    section_title("☰", "Поиск по локальным базам")
    ensure_dirs()
    files = _list_base_files()
    if not files:
        print(f"    {tag('X', RED)} {RED}Папка «{BASE_DIR}» пуста")
        return

    total_lines = 0
    for _, path in files:
        try:
            with open(path, 'r', encoding='utf-8', errors='ignore') as f:
                total_lines += sum(1 for line in f if line.strip())
        except OSError:
            pass

    print(f"    {tag('+')} {WHITE}Баз: {CYAN}{len(files)}{WHITE}  ·  строк ≈ {CYAN}{total_lines}")

    query = input(f"\n    {tag('?')} {WHITE}Запрос: ").strip()
    if not query:
        print(f"    {tag('X', RED)} {RED}Пустой запрос")
        return

    print(f"""
    {BLUE}Режим:
      {CYAN}1{WHITE} — подстрока (без регистра)
      {CYAN}2{WHITE} — точное слово
      {CYAN}3{WHITE} — regex
    """)
    mode = input(f"    {tag('?')} {WHITE}Режим [1]: ").strip() or '1'
    max_hits = input(f"    {tag('?')} {WHITE}Макс. результатов [50]: ").strip()
    max_hits = int(max_hits) if max_hits.isdigit() else 50
    filter_name = input(f"    {tag('?')} {WHITE}Фильтр файла (Enter = все): ").strip().lower()

    if mode == '3':
        try:
            pattern = re.compile(query, re.IGNORECASE)
        except re.error as e:
            print(f"    {tag('X', RED)} {RED}Неверное regex: {e}")
            return
        matcher = lambda line: bool(pattern.search(line))
    elif mode == '2':
        token = re.compile(rf'(?<!\w){re.escape(query)}(?!\w)', re.IGNORECASE)
        matcher = lambda line: bool(token.search(line))
    else:
        q = query.lower()
        matcher = lambda line: q in line.lower()

    print()
    loading_bar("сканирование баз", steps=16, delay=0.02)
    print()

    hits = []
    scanned = 0
    for label, path in files:
        if filter_name and filter_name not in label.lower():
            continue
        try:
            with open(path, 'r', encoding='utf-8', errors='ignore') as f:
                for num, line in enumerate(f, 1):
                    scanned += 1
                    raw = line.rstrip('\n')
                    if not raw.strip():
                        continue
                    if matcher(raw):
                        clean = raw.replace('NULL', '—').strip()
                        hits.append((label, num, clean))
                        print(f"    {GREEN}▸ [{label}:{num}] {WHITE}{clean[:150]}")
                        sleep_ms(15)
                        if len(hits) >= max_hits:
                            break
            if len(hits) >= max_hits:
                break
        except OSError as e:
            print(f"    {tag('X', RED)} {RED}{label}: {e}")

    print(f"\n    {tag('+')} {WHITE}Строк: {CYAN}{scanned}{WHITE}  ·  найдено: {GREEN}{len(hits)}"
          + (f" {YELLOW}(лимит {max_hits})" if len(hits) >= max_hits else ""))

    if not hits:
        print(f"    {tag('-')} {WHITE}Совпадений нет.")
        return

    if input(f"\n    {tag('?')} {WHITE}Сохранить? (y/n): ").strip().lower() == 'y':
        body = [f"Query: {query}", f"Mode: {mode}", f"Author: {AUTHOR}", ""]
        for label, num, clean in hits:
            body.append(f"[{label}:{num}] {clean}")
        save_results('db_search', '\n'.join(body))


def base_stats():
    section_title("▤", "Статистика баз")
    ensure_dirs()
    files = _list_base_files()
    if not files:
        print(f"    {tag('X', RED)} {RED}Базы не найдены в «{BASE_DIR}/»")
        return

    total = 0
    result_box_open()
    for label, path in files:
        try:
            size = os.path.getsize(path)
            with open(path, 'r', encoding='utf-8', errors='ignore') as f:
                lines = sum(1 for line in f if line.strip())
            total += lines
            kb = size / 1024
            print(f"    {BLUE}│  {WHITE}{label:<26} {CYAN}{lines:>7}{WHITE} стр  {YELLOW}{kb:>7.1f} KB")
            sleep_ms(30)
        except OSError as e:
            print(f"    {BLUE}│  {RED}{label}: {e}")
    result_box_close()
    print(f"\n    {tag('*')} {WHITE}Файлов: {CYAN}{len(files)}{WHITE}  ·  строк: {CYAN}{total}")


# ─── Email ──────────────────────────────────────────────────────────────────

def email_check():
    section_title("@", "Проверка email")
    email = input(f"    {tag('?')} {WHITE}Email: ").strip().lower()
    if not email:
        print(f"    {tag('X', RED)} {RED}Пусто")
        return

    for f in range(8):
        spinner_once(f, "анализ адреса")
        sleep_ms(40)
    print()

    pattern = r'^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$'
    valid = bool(re.match(pattern, email))
    local, _, domain = email.partition('@')

    result_box_open()
    print(f"    {BLUE}│  {WHITE}Валидность: {GREEN if valid else RED}{'OK' if valid else 'НЕВАЛИДЕН'}")
    print(f"    {BLUE}│  {WHITE}Локальная:  {CYAN}{local}")
    print(f"    {BLUE}│  {WHITE}Домен:      {CYAN}{domain}")
    result_box_close()

    if not valid or not domain:
        return

    try:
        r = requests.get(
            f"https://dns.google/resolve?name={quote_plus(domain)}&type=MX",
            headers=headers(),
            timeout=10,
        )
        answers = r.json().get('Answer') or []
        if answers:
            print(f"\n    {tag('+')} {WHITE}MX-записи:")
            for a in answers[:5]:
                print(f"    {CYAN}  → {a.get('data', '—')}")
                sleep_ms(30)
        else:
            print(f"\n    {tag('-')} {YELLOW}MX не найдены")
    except requests.RequestException as e:
        print(f"    {tag('X', RED)} {RED}DNS: {e}")

    try:
        ghash = hashlib.md5(email.encode('utf-8')).hexdigest()
        grav = f"https://www.gravatar.com/avatar/{ghash}?d=404"
        gr = requests.get(grav, headers=headers(), timeout=8)
        print(f"    {tag('+')} {WHITE}Gravatar: {GREEN if gr.status_code == 200 else YELLOW}"
              f"{'есть' if gr.status_code == 200 else 'нет'}")
        if gr.status_code == 200:
            print(f"    {CYAN}  → {grav.replace('?d=404', '')}")
    except requests.RequestException:
        pass

    try:
        r = requests.get(
            f"https://dns.google/resolve?name={quote_plus(domain)}&type=A",
            headers=headers(),
            timeout=10,
        )
        answers = r.json().get('Answer') or []
        ips = [a.get('data') for a in answers if a.get('data')]
        if ips:
            print(f"    {tag('+')} {WHITE}A: {CYAN}{', '.join(ips[:3])}")
    except requests.RequestException:
        pass

    print(f"    {tag('+')} {WHITE}Google: {CYAN}https://www.google.com/search?q=%22{quote_plus(email)}%22")
    print(f"    {tag('+')} {WHITE}Yandex: {CYAN}https://yandex.ru/search/?text=%22{quote_plus(email)}%22")

    checks = [
        (valid, "format", "RFC-like" if valid else "invalid"),
        (True, "local", local),
        (True, "domain", domain),
    ]
    rows = [
        ("unknown", "Google", f"https://www.google.com/search?q=%22{quote_plus(email)}%22", "", 0),
        ("unknown", "GitHub search", f"https://github.com/search?q={quote_plus(email)}&type=users", "", 0),
    ]
    html_path = save_html_report(f'email_{local}', email, email, "email", checks, rows)
    if input(f"\n    {tag('?')} {WHITE}Открыть HTML-отчёт? (y/n): ").strip().lower() == 'y':
        open_url('file:///' + os.path.abspath(html_path).replace('\\', '/'))


# ─── Domain ─────────────────────────────────────────────────────────────────

def domain_info():
    section_title("◉", "Информация о домене")
    domain = input(f"    {tag('?')} {WHITE}Домен (example.com): ").strip().lower()
    domain = re.sub(r'^https?://', '', domain).split('/')[0]
    if not domain:
        print(f"    {tag('X', RED)} {RED}Пусто")
        return

    for f in range(10):
        spinner_once(f, f"resolve {domain}")
        sleep_ms(45)
    print()

    try:
        r = requests.get(f"https://ipapi.co/{domain}/json/", headers=headers(), timeout=12)
        data = r.json()
        result_box_open()
        for k in ("ip", "city", "region", "country_name", "org", "asn", "timezone"):
            if data.get(k):
                print(f"    {BLUE}│  {WHITE}{k:<14} {CYAN}{data[k]}")
                sleep_ms(25)
        result_box_close()
    except requests.RequestException as e:
        print(f"    {tag('X', RED)} {RED}{e}")

    try:
        r = requests.get(
            f"https://dns.google/resolve?name={quote_plus(domain)}&type=A",
            headers=headers(),
            timeout=10,
        )
        answers = r.json().get('Answer') or []
        if answers:
            print(f"\n    {tag('+')} {WHITE}A-записи:")
            for a in answers:
                print(f"    {CYAN}  → {a.get('data')}")
    except requests.RequestException:
        pass

    print(f"\n    {tag('+')} {WHITE}who.is: {CYAN}https://who.is/whois/{domain}")
    print(f"    {tag('+')} {WHITE}crt.sh:  {CYAN}https://crt.sh/?q={domain}")


# ─── TG / About ─────────────────────────────────────────────────────────────

def open_tg_channel():
    section_title("✈", "Telegram-канал")
    print(f"    {tag('+')} {CYAN}{TG_CHANNEL}")
    for f in range(6):
        spinner_once(f, "открываю браузер")
        sleep_ms(50)
    print()
    open_url(TG_CHANNEL)
    if input(f"    {tag('?')} {WHITE}Открыть профиль автора? (y/n): ").strip().lower() == 'y':
        open_url(TG_AUTHOR)


def about_author():
    section_title("★", "Об авторе")
    block = [
        f"    {BLUE}╔══════════════════════════════════════════════════╗",
        f"    {BLUE}║  {CYAN}{BRIGHT}ZXCLINUX OSINT{Style.RESET_ALL}{BLUE}                                  ║",
        f"    {BLUE}║  {WHITE}Версия: {CYAN}{VERSION:<38}{BLUE}║",
        f"    {BLUE}║  {WHITE}Автор:  {GREEN}{AUTHOR:<38}{BLUE}║",
        f"    {BLUE}╠══════════════════════════════════════════════════╣",
        f"    {BLUE}║  {WHITE}Канал:  {CYAN}t.me/zxclinuxchannel{WHITE}                 {BLUE}║",
        f"    {BLUE}║  {WHITE}Автор:  {CYAN}t.me/zxclinux{WHITE}                        {BLUE}║",
        f"    {BLUE}╚══════════════════════════════════════════════════╝",
    ]
    for line in block:
        print(line)
        sleep_ms(40)

    choice = input(f"\n    {tag('?')} {WHITE}[1] канал  [2] автор  [Enter] назад: ").strip()
    if choice == '1':
        open_url(TG_CHANNEL)
    elif choice == '2':
        open_url(TG_AUTHOR)


def goodbye():
    print()
    type_out(f"    {CYAN}shutting down zxclinux...{Style.RESET_ALL}", 0.018)
    sleep_ms(200)
    print(f"    {GREEN}✓{WHITE} bye, {CYAN}{AUTHOR}")
    print(f"    {DIM}{TG_CHANNEL}{Style.RESET_ALL}\n")


# ─── Main ───────────────────────────────────────────────────────────────────

def main():
    global _FIRST_BOOT
    ensure_dirs()

    if _FIRST_BOOT:
        anim_boot()

    while True:
        clear()
        print_banner(animated=_FIRST_BOOT)
        print_menu(animated=_FIRST_BOOT)
        _FIRST_BOOT = False

        choice = input(f"    {CYAN}╰─▸{WHITE} ").strip()

        if choice in ('0', '00'):
            key = '0'
        elif choice.isdigit():
            key = str(int(choice))
        else:
            key = choice

        actions = {
            '1': search_phone,
            '2': iplook,
            '3': search_nick,
            '4': search_in_bases,
            '5': email_check,
            '6': domain_info,
            '7': base_stats,
            '8': open_tg_channel,
            '9': about_author,
        }

        if key == '0':
            goodbye()
            break

        action = actions.get(key)
        if action:
            try:
                action()
            except KeyboardInterrupt:
                print(f"\n    {tag('!', YELLOW)} {YELLOW}Прервано")
            except Exception as e:
                print(f"    {tag('X', RED)} {RED}Ошибка: {e}")
        else:
            print(f"    {tag('X', RED)} {RED}Неверный пункт. 00–09.")
            sleep_ms(600)

        if key != '0':
            pause()


if __name__ == "__main__":
    try:
        if os.name == 'nt':
            os.system('title ZXCLINUX OSINT')
        main()
    except KeyboardInterrupt:
        print(f"\n{CYAN}Выход...\n")
        sys.exit(0)
