import os
import sys
import subprocess
import importlib.util
import random
import re
import time
import hashlib
import webbrowser
from datetime import datetime
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.0"
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 _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 = [
        f"https://www.google.com/search?q=%22{q}%22",
        f"https://yandex.ru/search/?text=%22{q}%22",
        f"https://t.me/{digits}",
    ]
    for url in links:
        print(f"    {CYAN}  → {url}")
        out_lines.append(url)
        sleep_ms(40)

    if input(f"\n    {tag('?')} {WHITE}Сохранить? (y/n): ").strip().lower() == 'y':
        save_results(f'phone_{digits}', '\n'.join(out_lines))


# ─── 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"]),
    "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"]),
    "YouTube":    ("https://www.youtube.com/@{q}", ["this page isn't available", "404"]),
    "Twitch":     ("https://www.twitch.tv/{q}", ["sorry. unless"]),
    "Steam":      ("https://steamcommunity.com/id/{q}", ["the specified profile could not be found", "error_ctn"]),
    "Habr":       ("https://habr.com/ru/users/{q}", ["страница не найдена", "404"]),
    "SoundCloud": ("https://soundcloud.com/{q}", ["we can't find that user"]),
    "Pinterest":  ("https://www.pinterest.com/{q}/", ["page 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"]),
}


def _nick_exists(name: str, response, nick: str) -> bool:
    code = response.status_code
    if code in (404, 410, 451) or code >= 400 or code != 200:
        return False

    body = (response.text or '').lower()
    final = (response.url or '').lower()

    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[name][1]
    if any(m in body for m in markers):
        if name == "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 name == "GitHub" and f"github.com/{nick.lower()}" in final:
        return True
    return True


def search_nick():
    section_title("@", "Поиск по никнейму")
    print(f"    {DIM}покажу только сайты, где ник реально есть{Style.RESET_ALL}")
    nick = input(f"    {tag('?')} {WHITE}Никнейм: ").strip().lstrip('@')
    if not nick:
        print(f"    {tag('X', RED)} {RED}Никнейм не введён")
        return

    print(f"\n    {tag('!')} {WHITE}Сканирую {CYAN}{len(NICK_SITES)}{WHITE} площадок для «{CYAN}{nick}{WHITE}»\n")

    found = []
    session = requests.Session()
    session.headers.update(headers())
    total = len(NICK_SITES)

    for i, (name, (tmpl, _)) in enumerate(NICK_SITES.items(), 1):
        url = tmpl.format(q=quote_plus(nick) if 'username=' in tmpl else nick)
        progress_bar(i, total, width=30, label=f"{CYAN}{name:<12}{WHITE} ")
        try:
            r = session.get(url, timeout=8, allow_redirects=True)
            if _nick_exists(name, r, nick):
                clean_url = r.url.split('?')[0].rstrip('/')
                if name == "Roblox":
                    clean_url = url
                found.append((name, clean_url))
        except requests.RequestException:
            pass
        sleep_ms(80)

    if not found:
        print(f"\n    {tag('-')} {YELLOW}Нигде не найдено.")
        return

    print(f"\n    {GREEN}✓{WHITE} Найдено аккаунтов: {CYAN}{len(found)}\n")
    result_box_open()
    for name, url in found:
        short = url.replace('https://', '').replace('http://', '')
        print(f"    {BLUE}│  {GREEN}● {WHITE}{name:<12} {CYAN}{short}")
        sleep_ms(55)
    result_box_close()

    if input(f"\n    {tag('?')} {WHITE}Сохранить ссылки? (y/n): ").strip().lower() == 'y':
        text = f"Ник: {nick}\nАвтор: {AUTHOR}\n\n" + '\n'.join(f"{n}: {u}" for n, u in found)
        save_results(f'nick_{nick}', text)


# ─── 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

    print(f"    {tag('+')} {WHITE}Google: {CYAN}https://www.google.com/search?q=%22{quote_plus(email)}%22")


# ─── 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)
