package main

import (
	"bufio"
	"context"
	"crypto/md5"
	"crypto/sha256"
	"crypto/tls"
	"database/sql"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"runtime"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/gdamore/tcell/v2"
	_ "modernc.org/sqlite"
)

const (
	Version   = "1.0.0"
	UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
)

// ===========================================================================
// TERMINATOR RED/ORANGE THEME
// ===========================================================================

var (
	ColorBackground = tcell.ColorBlack
	ColorText       = tcell.ColorWhite
	ColorPrimary    = tcell.NewRGBColor(255, 0, 0)     // Pure red
	ColorSecondary  = tcell.NewRGBColor(255, 140, 30)  // Orange
	ColorAccent     = tcell.NewRGBColor(0, 200, 220)   // Cyan highlight
	ColorSuccess    = tcell.NewRGBColor(80, 220, 120)  // Green
	ColorWarning    = tcell.NewRGBColor(255, 200, 60)  // Yellow
	ColorDanger     = tcell.NewRGBColor(255, 0, 0)     // Pure red
	ColorInfo       = tcell.NewRGBColor(100, 180, 255) // Blue info
	ColorBorder     = tcell.NewRGBColor(139, 0, 0)     // Dark red border
	ColorLogo       = tcell.NewRGBColor(255, 0, 0)     // Pure red logo
	ColorDim        = tcell.NewRGBColor(100, 80, 70)   // Dim
)

// ===========================================================================
// MODULE SYSTEM
// ===========================================================================

type ActiveModule int

const (
	ModuleWPDeterminer ActiveModule = iota
	ModuleHashHunter
	ModuleHashCracker
	ModulePipeline
	ModuleHuntr
	ModuleODINT
	ModulePortScanner
	ModuleSilentEye
	ModuleTerminator
)

var moduleNames = map[ActiveModule]string{
	ModuleWPDeterminer: "WordPress Determiner",
	ModuleHashHunter:   "Hash Hunter",
	ModuleHashCracker:  "Hash Cracker",
	ModulePipeline:     "Pipeline",
	ModuleHuntr:        "Huntr",
	ModuleODINT:        "ODINT Toolkit",
	ModulePortScanner:  "Port Scanner",
	ModuleSilentEye:    "Silent Eye",
	ModuleTerminator:   "TERMINATOR MODE",
}

var moduleColors = map[ActiveModule]tcell.Color{
	ModuleWPDeterminer: tcell.NewRGBColor(0, 180, 255),   // Ice blue
	ModuleHashHunter:   tcell.NewRGBColor(255, 140, 0),   // Orange
	ModuleHashCracker:  tcell.NewRGBColor(255, 255, 0),   // Yellow
	ModulePipeline:     tcell.NewRGBColor(255, 60, 40),   // Red
	ModuleHuntr:        tcell.NewRGBColor(220, 30, 30),   // Crimson
	ModuleODINT:        tcell.NewRGBColor(180, 100, 255), // Purple
	ModulePortScanner:  tcell.NewRGBColor(0, 200, 100),   // Green
	ModuleSilentEye:    tcell.NewRGBColor(0, 200, 220),   // Cyan
	ModuleTerminator:   tcell.ColorWhite,                  // Pure white
}

// ===========================================================================
// ASCII LOGO
// ===========================================================================

var terminatorLogo = []string{
	"",
	" ███╗   ███╗ ██████╗ ████████╗██╗  ██╗███████╗██████╗ ███████╗██╗   ██╗ ██████╗██╗  ██╗██╗███╗   ██╗",
	" ████╗ ████║██╔═══██╗╚══██╔══╝██║  ██║██╔════╝██╔══██╗██╔════╝██║   ██║██╔════╝██║ ██╔╝██║████╗  ██║",
	" ██╔████╔██║██║   ██║   ██║   ███████║█████╗  ██████╔╝█████╗  ██║   ██║██║     █████╔╝ ██║██╔██╗ ██║",
	" ██║╚██╔╝██║██║   ██║   ██║   ██╔══██║██╔══╝  ██╔══██╗██╔══╝  ██║   ██║██║     ██╔═██╗ ██║██║╚██╗██║",
	" ██║ ╚═╝ ██║╚██████╔╝   ██║   ██║  ██║███████╗██║  ██║██║     ╚██████╔╝╚██████╗██║  ██╗██║██║ ╚████║",
	" ╚═╝     ╚═╝ ╚═════╝    ╚═╝   ╚═╝  ╚═╝╚══════╝╚═╝  ╚═╝╚═╝      ╚═════╝  ╚═════╝╚═╝  ╚═╝╚═╝╚═╝  ╚═══╝",
	"",
	" ████████╗███████╗██████╗ ███╗   ███╗██╗███╗   ██╗ █████╗ ████████╗ ██████╗ ██████╗ ",
	" ╚══██╔══╝██╔════╝██╔══██╗████╗ ████║██║████╗  ██║██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗",
	"    ██║   █████╗  ██████╔╝██╔████╔██║██║██╔██╗ ██║███████║   ██║   ██║   ██║██████╔╝",
	"    ██║   ██╔══╝  ██╔══██╗██║╚██╔╝██║██║██║╚██╗██║██╔══██║   ██║   ██║   ██║██╔══██╗",
	"    ██║   ███████╗██║  ██║██║ ╚═╝ ██║██║██║ ╚████║██║  ██║   ██║   ╚██████╔╝██║  ██║",
	"    ╚═╝   ╚══════╝╚═╝  ╚═╝╚═╝     ╚═╝╚═╝╚═╝  ╚═══╝╚═╝  ╚═╝   ╚═╝    ╚═════╝ ╚═╝  ╚═╝",
	"",
	"            ██████╗  ██████╗ ████████╗     █████╗  ██████╗  ██████╗  ██████╗ ",
	"            ██╔══██╗██╔═══██╗╚══██╔══╝    ██╔══██╗██╔═████╗██╔═████╗██╔═████╗",
	"            ██████╔╝██║   ██║   ██║       ╚██████║██║██╔██║██║██╔██║██║██╔██║",
	"            ██╔══██╗██║   ██║   ██║        ╚═══██║████╔╝██║████╔╝██║████╔╝██║",
	"            ██████╔╝╚██████╔╝   ██║        █████╔╝╚██████╔╝╚██████╔╝╚██████╔╝",
	"            ╚═════╝  ╚═════╝    ╚═╝        ╚════╝  ╚═════╝  ╚═════╝  ╚═════╝ ",
	"",
}

// ===========================================================================
// COMMAND STATE & MENU ITEMS
// ===========================================================================

type CommandState int

const (
	StateMenu CommandState = iota
	StateInput
	StateRunning
	StateComplete
	StateSettings
	StateFilePicker
	StateModuleSelect
)

type MenuItem struct {
	Key  string
	Name string
	Desc string
}

var wpMenuItems = []MenuItem{
	{"1", "check", "Check single URL"},
	{"2", "bulk", "Bulk check from file"},
	{"3", "recheck", "Recheck errors & not-WP from DB"},
	{"4", "clear", "Clear output"},
	{"Q", "quit", "Exit"},
}

var hhMenuItems = []MenuItem{
	{"1", "scan", "Aggressive scan single target"},
	{"2", "bulk", "Bulk scan from file"},
	{"3", "rescan", "Rescan incomplete sites"},
	{"4", "stats", "Database statistics"},
	{"5", "export", "Export hashes to CSV"},
	{"6", "history", "View hash history"},
	{"7", "newdb", "New database"},
	{"8", "merge", "Merge another database"},
	{"9", "clear", "Clear output"},
	{"0", "resume", "Resume interrupted scan"},
	{"Q", "quit", "Exit"},
}

var hcMenuItems = []MenuItem{
	{"1", "load-db", "Load hashes from .db file"},
	{"2", "load-internal", "Load from internal hashes"},
	{"3", "load-wordlist", "Load custom wordlist"},
	{"4", "start", "Start cracking"},
	{"5", "view", "View cracked hashes"},
	{"6", "export", "Export results (CSV)"},
	{"7", "stats", "Database stats"},
	{"8", "clear", "Clear output"},
	{"Q", "quit", "Exit"},
}

var pipelineMenuItems = []MenuItem{
	{"1", "full", "Full: Detect WP -> Harvest -> Crack"},
	{"2", "detect-harvest", "Detect WP -> Harvest hashes"},
	{"3", "harvest-crack", "Harvest hashes -> Crack emails"},
	{"4", "targets", "View cross-module intelligence"},
	{"5", "export-all", "Unified CSV export"},
	{"6", "clear", "Clear output"},
	{"Q", "quit", "Exit"},
}

var huntrMenuItems = []MenuItem{
	{"1", "scan", "Scan single domain"},
	{"2", "bulk", "Bulk scan from file"},
	{"3", "stats", "Database statistics"},
	{"4", "export", "Export findings (CSV)"},
	{"5", "clear", "Clear output"},
	{"Q", "quit", "Exit"},
}

var odintMenuItems = []MenuItem{
	{"1", "run", "Load targets & run full suite"},
	{"2", "config", "API keys and settings"},
	{"3", "stats", "Session statistics"},
	{"4", "report", "Generate report (HTML/JSON)"},
	{"5", "export", "Export results to CSV"},
	{"C", "clear", "Clear output"},
	{"Q", "quit", "Exit ODINT Toolkit"},
}

var portMenuItems = []MenuItem{
	{"1", "scan", "Scan target (top 1000)"},
	{"2", "custom", "Custom port range"},
	{"3", "bulk", "Bulk scan from file"},
	{"4", "stats", "Database statistics"},
	{"5", "export", "Export results (CSV)"},
	{"6", "clear", "Clear output"},
	{"Q", "quit", "Exit"},
}

var silentMenuItems = []MenuItem{
	{"1", "recon", "Full passive recon (all vendors)"},
	{"2", "crtsh", "Certificate Transparency (free)"},
	{"3", "shodan", "Shodan lookup"},
	{"4", "vt", "VirusTotal subdomains"},
	{"5", "ipinfo", "IP geolocation (free)"},
	{"6", "config", "Configure API keys"},
	{"7", "stats", "Database statistics"},
	{"8", "export", "Export results (CSV)"},
	{"9", "clear", "Clear output"},
	{"Q", "quit", "Exit"},
}

var terminatorMenuItems = []MenuItem{
	{"1", "run", "FULL TERMINATOR — ALL modules on target file"},
	{"2", "stats", "Combined stats across all modules"},
	{"3", "clear", "Clear output"},
	{"Q", "quit", "Exit"},
}

// ===========================================================================
// DATABASE — UNIFIED SCHEMA
// ===========================================================================

type DB struct {
	conn *sql.DB
}

func NewDB(path string) (*DB, error) {
	conn, err := sql.Open("sqlite", path)
	if err != nil {
		return nil, err
	}
	conn.Exec("PRAGMA journal_mode=WAL")
	conn.Exec("PRAGMA busy_timeout=5000")
	conn.Exec("PRAGMA wal_checkpoint(TRUNCATE)")
	conn.SetMaxOpenConns(1)
	db := &DB{conn: conn}
	db.migrate()
	return db, nil
}

func (db *DB) migrate() {
	schema := `
	CREATE TABLE IF NOT EXISTS targets (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT UNIQUE NOT NULL,
		is_wordpress INTEGER DEFAULT 0,
		wp_method TEXT DEFAULT '',
		has_hashes INTEGER DEFAULT 0,
		hash_count INTEGER DEFAULT 0,
		cracked_count INTEGER DEFAULT 0,
		first_seen DATETIME DEFAULT CURRENT_TIMESTAMP,
		last_updated DATETIME DEFAULT CURRENT_TIMESTAMP
	);
	CREATE INDEX IF NOT EXISTS idx_targets_domain ON targets(domain);
	CREATE INDEX IF NOT EXISTS idx_targets_wp ON targets(is_wordpress);

	CREATE TABLE IF NOT EXISTS sites (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT UNIQUE NOT NULL,
		is_wordpress INTEGER NOT NULL,
		method TEXT DEFAULT '',
		status_code INTEGER DEFAULT 0,
		error TEXT DEFAULT '',
		first_checked DATETIME DEFAULT CURRENT_TIMESTAMP,
		last_checked DATETIME DEFAULT CURRENT_TIMESTAMP,
		times_checked INTEGER DEFAULT 1
	);
	CREATE INDEX IF NOT EXISTS idx_sites_domain ON sites(domain);
	CREATE INDEX IF NOT EXISTS idx_sites_wp ON sites(is_wordpress);

	CREATE TABLE IF NOT EXISTS domains (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT UNIQUE NOT NULL,
		status TEXT DEFAULT 'pending',
		hash_count INTEGER DEFAULT 0,
		error TEXT DEFAULT '',
		first_scanned DATETIME DEFAULT CURRENT_TIMESTAMP,
		last_scanned DATETIME DEFAULT CURRENT_TIMESTAMP
	);
	CREATE INDEX IF NOT EXISTS idx_domains_domain ON domains(domain);
	CREATE INDEX IF NOT EXISTS idx_domains_status ON domains(status);

	CREATE TABLE IF NOT EXISTS hashes (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		hash TEXT NOT NULL,
		username TEXT DEFAULT '',
		email TEXT DEFAULT '',
		source_url TEXT DEFAULT '',
		hash_type TEXT DEFAULT '',
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(domain, hash, username)
	);
	CREATE INDEX IF NOT EXISTS idx_hashes_domain ON hashes(domain);
	CREATE INDEX IF NOT EXISTS idx_hashes_hash ON hashes(hash);

	CREATE TABLE IF NOT EXISTS hash_history (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		hash TEXT NOT NULL,
		action TEXT DEFAULT '',
		timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
	);

	CREATE TABLE IF NOT EXISTS scan_sessions (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		started DATETIME DEFAULT CURRENT_TIMESTAMP,
		ended DATETIME,
		total_targets INTEGER DEFAULT 0,
		completed INTEGER DEFAULT 0,
		status TEXT DEFAULT 'running'
	);

	CREATE TABLE IF NOT EXISTS session_targets (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		session_id INTEGER NOT NULL,
		domain TEXT NOT NULL,
		status TEXT DEFAULT 'pending',
		FOREIGN KEY(session_id) REFERENCES scan_sessions(id)
	);

	CREATE TABLE IF NOT EXISTS cracked (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		hash TEXT UNIQUE NOT NULL,
		email TEXT NOT NULL,
		domain TEXT DEFAULT '',
		username TEXT DEFAULT '',
		method TEXT DEFAULT '',
		cracked_at DATETIME DEFAULT CURRENT_TIMESTAMP
	);
	CREATE INDEX IF NOT EXISTS idx_cracked_hash ON cracked(hash);
	CREATE INDEX IF NOT EXISTS idx_cracked_email ON cracked(email);

	-- Huntr module tables
	CREATE TABLE IF NOT EXISTS huntr_findings (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		path TEXT NOT NULL,
		full_url TEXT NOT NULL,
		status_code INTEGER DEFAULT 0,
		content_type TEXT DEFAULT '',
		content_length INTEGER DEFAULT 0,
		body_snippet TEXT DEFAULT '',
		matched_patterns TEXT DEFAULT '',
		severity TEXT DEFAULT 'info',
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(domain, path)
	);
	CREATE INDEX IF NOT EXISTS idx_huntr_domain ON huntr_findings(domain);
	CREATE INDEX IF NOT EXISTS idx_huntr_severity ON huntr_findings(severity);

	-- ODINT Toolkit module tables
	CREATE TABLE IF NOT EXISTS odint_scans (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		scan_type TEXT DEFAULT 'full',
		started DATETIME DEFAULT CURRENT_TIMESTAMP,
		ended DATETIME,
		status TEXT DEFAULT 'running',
		modules_run TEXT DEFAULT '',
		findings_count INTEGER DEFAULT 0
	);
	CREATE INDEX IF NOT EXISTS idx_odint_scans_domain ON odint_scans(domain);

	CREATE TABLE IF NOT EXISTS odint_dns (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		scan_id INTEGER,
		domain TEXT NOT NULL,
		record_type TEXT NOT NULL,
		record_value TEXT NOT NULL,
		ttl INTEGER DEFAULT 0,
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(domain, record_type, record_value)
	);
	CREATE INDEX IF NOT EXISTS idx_odint_dns_domain ON odint_dns(domain);

	CREATE TABLE IF NOT EXISTS odint_ssl (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		scan_id INTEGER,
		domain TEXT NOT NULL,
		issuer TEXT DEFAULT '',
		subject TEXT DEFAULT '',
		serial TEXT DEFAULT '',
		not_before DATETIME,
		not_after DATETIME,
		sig_algorithm TEXT DEFAULT '',
		key_size INTEGER DEFAULT 0,
		san_names TEXT DEFAULT '',
		is_expired INTEGER DEFAULT 0,
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(domain, serial)
	);

	CREATE TABLE IF NOT EXISTS odint_technologies (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		scan_id INTEGER,
		domain TEXT NOT NULL,
		tech_name TEXT NOT NULL,
		tech_category TEXT DEFAULT '',
		tech_version TEXT DEFAULT '',
		confidence INTEGER DEFAULT 0,
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(domain, tech_name)
	);
	CREATE INDEX IF NOT EXISTS idx_odint_tech_domain ON odint_technologies(domain);

	CREATE TABLE IF NOT EXISTS odint_findings (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		scan_id INTEGER,
		domain TEXT NOT NULL,
		finding_type TEXT NOT NULL,
		title TEXT NOT NULL,
		detail TEXT DEFAULT '',
		severity TEXT DEFAULT 'info',
		source_module TEXT DEFAULT '',
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP
	);
	CREATE INDEX IF NOT EXISTS idx_odint_findings_domain ON odint_findings(domain);

	CREATE TABLE IF NOT EXISTS odint_whois (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT UNIQUE NOT NULL,
		registrar TEXT DEFAULT '',
		created_date TEXT DEFAULT '',
		expiry_date TEXT DEFAULT '',
		name_servers TEXT DEFAULT '',
		registrant TEXT DEFAULT '',
		raw_data TEXT DEFAULT '',
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP
	);

	CREATE TABLE IF NOT EXISTS odint_wayback (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		url TEXT NOT NULL,
		timestamp TEXT DEFAULT '',
		status_code INTEGER DEFAULT 0,
		mime_type TEXT DEFAULT '',
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(domain, url, timestamp)
	);
	CREATE INDEX IF NOT EXISTS idx_odint_wayback_domain ON odint_wayback(domain);

	-- ODINT v4.0.0 extended tables
	CREATE TABLE IF NOT EXISTS odint_nmap_results (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		port INTEGER NOT NULL,
		protocol TEXT DEFAULT 'tcp',
		state TEXT DEFAULT '',
		service TEXT DEFAULT '',
		version TEXT DEFAULT '',
		product TEXT DEFAULT '',
		extra TEXT DEFAULT '',
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(domain, port, protocol)
	);
	CREATE INDEX IF NOT EXISTS idx_odint_nmap_domain ON odint_nmap_results(domain);

	CREATE TABLE IF NOT EXISTS odint_cve_findings (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		service TEXT DEFAULT '',
		product TEXT DEFAULT '',
		version TEXT DEFAULT '',
		cve_id TEXT NOT NULL,
		cvss_score REAL DEFAULT 0,
		severity TEXT DEFAULT '',
		description TEXT DEFAULT '',
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(domain, cve_id)
	);
	CREATE INDEX IF NOT EXISTS idx_odint_cve_domain ON odint_cve_findings(domain);

	CREATE TABLE IF NOT EXISTS odint_wordpress_sites (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT UNIQUE NOT NULL,
		is_wordpress INTEGER DEFAULT 0,
		wp_version TEXT DEFAULT '',
		wp_theme TEXT DEFAULT '',
		wp_plugins TEXT DEFAULT '',
		rest_data TEXT DEFAULT '',
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP
	);

	CREATE TABLE IF NOT EXISTS odint_wordpress_users (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		user_id INTEGER DEFAULT 0,
		username TEXT DEFAULT '',
		display_name TEXT DEFAULT '',
		gravatar_hash TEXT DEFAULT '',
		url TEXT DEFAULT '',
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(domain, username)
	);
	CREATE INDEX IF NOT EXISTS idx_odint_wp_users_domain ON odint_wordpress_users(domain);

	CREATE TABLE IF NOT EXISTS odint_subdomains (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		subdomain TEXT NOT NULL,
		source TEXT DEFAULT '',
		ip TEXT DEFAULT '',
		status_code INTEGER DEFAULT 0,
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(domain, subdomain)
	);
	CREATE INDEX IF NOT EXISTS idx_odint_subdomains_domain ON odint_subdomains(domain);

	CREATE TABLE IF NOT EXISTS odint_exposed_paths (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		path TEXT NOT NULL,
		status_code INTEGER DEFAULT 0,
		path_type TEXT DEFAULT '',
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(domain, path)
	);
	CREATE INDEX IF NOT EXISTS idx_odint_paths_domain ON odint_exposed_paths(domain);

	CREATE TABLE IF NOT EXISTS odint_jwt_tokens (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		token_hash TEXT NOT NULL,
		algorithm TEXT DEFAULT '',
		issuer TEXT DEFAULT '',
		subject TEXT DEFAULT '',
		location TEXT DEFAULT '',
		header_json TEXT DEFAULT '',
		payload_json TEXT DEFAULT '',
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(domain, token_hash)
	);

	CREATE TABLE IF NOT EXISTS odint_graphql_endpoints (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		url TEXT NOT NULL,
		introspection INTEGER DEFAULT 0,
		types TEXT DEFAULT '',
		queries TEXT DEFAULT '',
		mutations TEXT DEFAULT '',
		subscriptions TEXT DEFAULT '',
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(domain, url)
	);

	CREATE TABLE IF NOT EXISTS odint_secrets (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		secret_type TEXT NOT NULL,
		value TEXT DEFAULT '',
		location TEXT DEFAULT '',
		severity TEXT DEFAULT 'medium',
		context TEXT DEFAULT '',
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(domain, secret_type, value)
	);
	CREATE INDEX IF NOT EXISTS idx_odint_secrets_domain ON odint_secrets(domain);

	CREATE TABLE IF NOT EXISTS odint_historical_records (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		source TEXT DEFAULT '',
		record_type TEXT DEFAULT '',
		data TEXT DEFAULT '',
		record_date TEXT DEFAULT '',
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP
	);
	CREATE INDEX IF NOT EXISTS idx_odint_historical_domain ON odint_historical_records(domain);

	CREATE TABLE IF NOT EXISTS odint_hash_correlations (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		hash TEXT NOT NULL,
		hash_type TEXT DEFAULT '',
		source TEXT DEFAULT '',
		identity TEXT DEFAULT '',
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(domain, hash, source)
	);

	CREATE TABLE IF NOT EXISTS odint_error_pages (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		status_code INTEGER DEFAULT 0,
		server_info TEXT DEFAULT '',
		stack_trace INTEGER DEFAULT 0,
		internal_ips TEXT DEFAULT '',
		dev_names TEXT DEFAULT '',
		file_paths TEXT DEFAULT '',
		db_errors TEXT DEFAULT '',
		framework TEXT DEFAULT '',
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP
	);

	CREATE TABLE IF NOT EXISTS odint_cookie_fingerprints (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		cookie_name TEXT NOT NULL,
		cookie_value TEXT DEFAULT '',
		flags TEXT DEFAULT '',
		tech_match TEXT DEFAULT '',
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(domain, cookie_name)
	);

	CREATE TABLE IF NOT EXISTS odint_management_uis (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		ui_type TEXT NOT NULL,
		url TEXT NOT NULL,
		version TEXT DEFAULT '',
		status_code INTEGER DEFAULT 0,
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(domain, url)
	);

	CREATE TABLE IF NOT EXISTS odint_analytics_ids (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		platform TEXT NOT NULL,
		analytics_id TEXT NOT NULL,
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(domain, platform, analytics_id)
	);

	CREATE TABLE IF NOT EXISTS odint_csp_directives (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		directive TEXT NOT NULL,
		"values" TEXT DEFAULT '',
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(domain, directive)
	);

	CREATE TABLE IF NOT EXISTS odint_iframe_sources (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		url TEXT NOT NULL,
		sandbox TEXT DEFAULT '',
		object_type TEXT DEFAULT '',
		is_nested INTEGER DEFAULT 0,
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(domain, url)
	);

	CREATE TABLE IF NOT EXISTS odint_virtual_hosts (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		vhost_domain TEXT NOT NULL,
		ip TEXT DEFAULT '',
		source TEXT DEFAULT '',
		is_default INTEGER DEFAULT 0,
		status_code INTEGER DEFAULT 0,
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(domain, vhost_domain)
	);

	CREATE TABLE IF NOT EXISTS odint_email_security (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT UNIQUE NOT NULL,
		mta_sts_policy TEXT DEFAULT '',
		bimi_record TEXT DEFAULT '',
		smtp_tls_rpt TEXT DEFAULT '',
		dane_records TEXT DEFAULT '',
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP
	);

	CREATE TABLE IF NOT EXISTS odint_reporting_endpoints (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		endpoint_type TEXT NOT NULL,
		url TEXT DEFAULT '',
		config TEXT DEFAULT '',
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(domain, endpoint_type, url)
	);

	CREATE TABLE IF NOT EXISTS odint_webhook_urls (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		platform TEXT DEFAULT '',
		url TEXT NOT NULL,
		location TEXT DEFAULT '',
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(domain, url)
	);

	CREATE TABLE IF NOT EXISTS odint_structured_data (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		data_type TEXT DEFAULT '',
		item_type TEXT DEFAULT '',
		properties TEXT DEFAULT '',
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP
	);

	CREATE TABLE IF NOT EXISTS odint_discovered_files (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		url TEXT NOT NULL,
		filename TEXT DEFAULT '',
		file_type TEXT DEFAULT '',
		source TEXT DEFAULT '',
		sha256 TEXT DEFAULT '',
		downloaded INTEGER DEFAULT 0,
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(domain, url)
	);
	CREATE INDEX IF NOT EXISTS idx_odint_files_domain ON odint_discovered_files(domain);

	CREATE TABLE IF NOT EXISTS odint_social_profiles (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		platform TEXT NOT NULL,
		url TEXT DEFAULT '',
		username TEXT DEFAULT '',
		exists_flag INTEGER DEFAULT 0,
		status_code INTEGER DEFAULT 0,
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(domain, platform, username)
	);

	CREATE TABLE IF NOT EXISTS odint_cloud_exposures (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		provider TEXT NOT NULL,
		bucket_name TEXT DEFAULT '',
		url TEXT DEFAULT '',
		status_code INTEGER DEFAULT 0,
		accessible INTEGER DEFAULT 0,
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(domain, provider, bucket_name)
	);

	CREATE TABLE IF NOT EXISTS odint_github_findings (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		finding_type TEXT DEFAULT '',
		name TEXT DEFAULT '',
		url TEXT DEFAULT '',
		snippet TEXT DEFAULT '',
		language TEXT DEFAULT '',
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(domain, url)
	);

	CREATE TABLE IF NOT EXISTS odint_google_dorks (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		dork TEXT NOT NULL,
		category TEXT DEFAULT '',
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(domain, dork)
	);

	CREATE TABLE IF NOT EXISTS odint_network_intel (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		ip TEXT DEFAULT '',
		asn TEXT DEFAULT '',
		asn_org TEXT DEFAULT '',
		bgp_prefix TEXT DEFAULT '',
		rir TEXT DEFAULT '',
		country TEXT DEFAULT '',
		city TEXT DEFAULT '',
		isp TEXT DEFAULT '',
		lat REAL DEFAULT 0,
		lon REAL DEFAULT 0,
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(domain, ip)
	);

	CREATE TABLE IF NOT EXISTS odint_hibp_breaches (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		domain TEXT NOT NULL,
		email TEXT DEFAULT '',
		breach_name TEXT NOT NULL,
		breach_date TEXT DEFAULT '',
		data_classes TEXT DEFAULT '',
		pwn_count INTEGER DEFAULT 0,
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(domain, email, breach_name)
	);

	-- Port Scanner module tables
	CREATE TABLE IF NOT EXISTS port_targets (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		target TEXT UNIQUE NOT NULL,
		scan_type TEXT DEFAULT 'top1000',
		port_spec TEXT DEFAULT '',
		open_count INTEGER DEFAULT 0,
		closed_count INTEGER DEFAULT 0,
		filtered_count INTEGER DEFAULT 0,
		first_scanned DATETIME DEFAULT CURRENT_TIMESTAMP,
		last_scanned DATETIME DEFAULT CURRENT_TIMESTAMP
	);
	CREATE INDEX IF NOT EXISTS idx_port_targets ON port_targets(target);

	CREATE TABLE IF NOT EXISTS port_results (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		target TEXT NOT NULL,
		port INTEGER NOT NULL,
		state TEXT DEFAULT 'closed',
		service TEXT DEFAULT '',
		banner TEXT DEFAULT '',
		protocol TEXT DEFAULT 'tcp',
		response_ms INTEGER DEFAULT 0,
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(target, port, protocol)
	);
	CREATE INDEX IF NOT EXISTS idx_port_results_target ON port_results(target);
	CREATE INDEX IF NOT EXISTS idx_port_results_state ON port_results(state);

	-- Silent Eye module tables
	CREATE TABLE IF NOT EXISTS silent_targets (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		target TEXT UNIQUE NOT NULL,
		target_type TEXT DEFAULT 'domain',
		vendors_queried TEXT DEFAULT '',
		total_findings INTEGER DEFAULT 0,
		first_scanned DATETIME DEFAULT CURRENT_TIMESTAMP,
		last_scanned DATETIME DEFAULT CURRENT_TIMESTAMP
	);
	CREATE INDEX IF NOT EXISTS idx_silent_targets ON silent_targets(target);

	CREATE TABLE IF NOT EXISTS silent_recon (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		target TEXT NOT NULL,
		vendor TEXT NOT NULL,
		data_type TEXT NOT NULL,
		data_value TEXT NOT NULL,
		raw_json TEXT DEFAULT '',
		discovered DATETIME DEFAULT CURRENT_TIMESTAMP,
		UNIQUE(target, vendor, data_type, data_value)
	);
	CREATE INDEX IF NOT EXISTS idx_silent_recon_target ON silent_recon(target);
	CREATE INDEX IF NOT EXISTS idx_silent_recon_vendor ON silent_recon(vendor);
	`
	db.conn.Exec(schema)
}

// --- WP Determiner DB methods ---

func (db *DB) UpsertSiteResult(result CheckResult) {
	isWP := 0
	if result.IsWordPress {
		isWP = 1
	}
	errStr := ""
	if result.Error != nil {
		errStr = result.Error.Error()
	}

	db.conn.Exec(`
		INSERT INTO sites (domain, is_wordpress, method, status_code, error, first_checked, last_checked, times_checked)
		VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 1)
		ON CONFLICT(domain) DO UPDATE SET
			is_wordpress = excluded.is_wordpress,
			method = excluded.method,
			status_code = excluded.status_code,
			error = excluded.error,
			last_checked = CURRENT_TIMESTAMP,
			times_checked = times_checked + 1`,
		result.Domain, isWP, result.Method, result.StatusCode, errStr)

	// Also update master targets table
	db.conn.Exec(`
		INSERT INTO targets (domain, is_wordpress, wp_method, last_updated)
		VALUES (?, ?, ?, CURRENT_TIMESTAMP)
		ON CONFLICT(domain) DO UPDATE SET
			is_wordpress = excluded.is_wordpress,
			wp_method = excluded.wp_method,
			last_updated = CURRENT_TIMESTAMP`,
		result.Domain, isWP, result.Method)
}

func (db *DB) GetSiteStats() (total, wordpress, notWP, errors int) {
	db.conn.QueryRow("SELECT COUNT(*) FROM sites").Scan(&total)
	db.conn.QueryRow("SELECT COUNT(*) FROM sites WHERE is_wordpress = 1").Scan(&wordpress)
	db.conn.QueryRow("SELECT COUNT(*) FROM sites WHERE is_wordpress = 0 AND error = ''").Scan(&notWP)
	db.conn.QueryRow("SELECT COUNT(*) FROM sites WHERE error != ''").Scan(&errors)
	return
}

func (db *DB) GetRecheckDomains() []string {
	rows, err := db.conn.Query("SELECT domain FROM sites WHERE is_wordpress = 0 ORDER BY domain")
	if err != nil {
		return nil
	}
	defer rows.Close()

	var domains []string
	for rows.Next() {
		var d string
		if rows.Scan(&d) == nil {
			domains = append(domains, d)
		}
	}
	return domains
}

// --- Hash Hunter DB methods ---

func (db *DB) InsertHashIfNew(domain, hash, username, email, sourceURL, hashType string) bool {
	res, err := db.conn.Exec(`
		INSERT OR IGNORE INTO hashes (domain, hash, username, email, source_url, hash_type, discovered)
		VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
		domain, hash, username, email, sourceURL, hashType)
	if err != nil {
		return false
	}
	rows, _ := res.RowsAffected()
	return rows > 0
}

func (db *DB) UpdateDomain(domain, status string, hashCount int) {
	db.conn.Exec(`
		INSERT INTO domains (domain, status, hash_count, last_scanned)
		VALUES (?, ?, ?, CURRENT_TIMESTAMP)
		ON CONFLICT(domain) DO UPDATE SET
			status = excluded.status,
			hash_count = excluded.hash_count,
			last_scanned = CURRENT_TIMESTAMP`,
		domain, status, hashCount)
}

func (db *DB) GetTotalHashes() int {
	var count int
	db.conn.QueryRow("SELECT COUNT(*) FROM hashes").Scan(&count)
	return count
}

func (db *DB) GetTotalDomains() int {
	var count int
	db.conn.QueryRow("SELECT COUNT(*) FROM domains").Scan(&count)
	return count
}

func (db *DB) GetIncompleteCount() int {
	var count int
	db.conn.QueryRow("SELECT COUNT(*) FROM domains WHERE status != 'complete'").Scan(&count)
	return count
}

func (db *DB) GetIncompleteDomains() []string {
	rows, err := db.conn.Query("SELECT domain FROM domains WHERE status != 'complete' ORDER BY domain")
	if err != nil {
		return nil
	}
	defer rows.Close()
	var domains []string
	for rows.Next() {
		var d string
		if rows.Scan(&d) == nil {
			domains = append(domains, d)
		}
	}
	return domains
}

// --- Hash Cracker DB methods ---

func (db *DB) InsertCracked(hash, email, domain, username, method string) {
	db.conn.Exec(`
		INSERT OR IGNORE INTO cracked (hash, email, domain, username, method, cracked_at)
		VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
		hash, email, domain, username, method)
}

func (db *DB) GetCrackedStats() (total, cracked int) {
	db.conn.QueryRow("SELECT COUNT(*) FROM hashes").Scan(&total)
	db.conn.QueryRow("SELECT COUNT(*) FROM cracked").Scan(&cracked)
	return
}

func (db *DB) LoadCrackedHashes() map[string]string {
	m := make(map[string]string)
	rows, err := db.conn.Query("SELECT hash, email FROM cracked")
	if err != nil {
		return m
	}
	defer rows.Close()
	for rows.Next() {
		var h, e string
		if rows.Scan(&h, &e) == nil {
			m[h] = e
		}
	}
	return m
}

// --- Cross-module methods ---

func (db *DB) GetConfirmedWPSites() []string {
	rows, err := db.conn.Query("SELECT domain FROM sites WHERE is_wordpress = 1 ORDER BY domain")
	if err != nil {
		return nil
	}
	defer rows.Close()
	var domains []string
	for rows.Next() {
		var d string
		if rows.Scan(&d) == nil {
			domains = append(domains, d)
		}
	}
	return domains
}

// GetUncracked returns hashes from the hashes table that are NOT in the cracked table
func (db *DB) GetUncracked() []HashRecord {
	rows, err := db.conn.Query(`
		SELECT h.domain, h.hash, h.username, h.hash_type
		FROM hashes h
		LEFT JOIN cracked c ON h.hash = c.hash
		WHERE c.hash IS NULL AND h.hash != '' AND h.hash IS NOT NULL
		ORDER BY h.domain`)
	if err != nil {
		return nil
	}
	defer rows.Close()
	var records []HashRecord
	for rows.Next() {
		var r HashRecord
		if rows.Scan(&r.Domain, &r.Hash, &r.Username, &r.HashType) == nil {
			records = append(records, r)
		}
	}
	return records
}

// GetWPSitesNotScanned returns confirmed WP sites that haven't been scanned by Hash Hunter yet
func (db *DB) GetWPSitesNotScanned() []string {
	rows, err := db.conn.Query(`
		SELECT s.domain FROM sites s
		WHERE s.is_wordpress = 1
		AND s.domain NOT IN (SELECT domain FROM domains)
		ORDER BY s.domain`)
	if err != nil {
		return nil
	}
	defer rows.Close()
	var domains []string
	for rows.Next() {
		var d string
		if rows.Scan(&d) == nil {
			domains = append(domains, d)
		}
	}
	return domains
}

// GetFullIntel returns a summary of all data across all modules for a specific domain
func (db *DB) GetFullIntel(domain string) (wpChecked bool, isWP bool, wpMethod string, hashCount int, crackedCount int) {
	db.conn.QueryRow("SELECT is_wordpress, method FROM sites WHERE domain = ?", domain).Scan(&isWP, &wpMethod)
	wpChecked = isWP || wpMethod != ""
	db.conn.QueryRow("SELECT COUNT(*) FROM hashes WHERE domain = ?", domain).Scan(&hashCount)
	db.conn.QueryRow("SELECT COUNT(*) FROM cracked WHERE domain = ?", domain).Scan(&crackedCount)
	// if site was queried but no row exists, wpChecked should check if row exists
	var siteExists int
	db.conn.QueryRow("SELECT COUNT(*) FROM sites WHERE domain = ?", domain).Scan(&siteExists)
	wpChecked = siteExists > 0
	return
}

func (db *DB) Close() { db.conn.Close() }

// ===========================================================================
// CHECKER — WordPress Detection (10 methods)
// ===========================================================================

type CheckResult struct {
	Domain      string
	IsWordPress bool
	Method      string
	Error       error
	StatusCode  int
}

type Checker struct {
	client     *http.Client
	userAgent  string
	maxRetries int
	retryDelay time.Duration
}

func NewChecker(timeout time.Duration) *Checker {
	transport := &http.Transport{
		TLSClientConfig:       &tls.Config{InsecureSkipVerify: true},
		MaxIdleConns:          500,
		MaxIdleConnsPerHost:   50,
		MaxConnsPerHost:       50,
		IdleConnTimeout:       90 * time.Second,
		DisableKeepAlives:     false,
		ForceAttemptHTTP2:     false,
		ResponseHeaderTimeout: timeout,
		TLSHandshakeTimeout:   5 * time.Second,
	}
	return &Checker{
		client:     &http.Client{Timeout: timeout, Transport: transport},
		userAgent:  UserAgent,
		maxRetries: 2,
		retryDelay: 500 * time.Millisecond,
	}
}

func (c *Checker) doRequest(ctx context.Context, method, rawURL string) (*http.Response, error) {
	var lastErr error
	for attempt := 0; attempt <= c.maxRetries; attempt++ {
		if attempt > 0 {
			select {
			case <-ctx.Done():
				return nil, ctx.Err()
			case <-time.After(c.retryDelay * time.Duration(attempt)):
			}
		}

		req, err := http.NewRequestWithContext(ctx, method, rawURL, nil)
		if err != nil {
			return nil, err
		}
		req.Header.Set("User-Agent", c.userAgent)
		req.Header.Set("Accept", "text/html, application/json, */*")

		resp, err := c.client.Do(req)
		if err != nil {
			lastErr = err
			continue
		}

		if resp.StatusCode == 429 {
			resp.Body.Close()
			lastErr = fmt.Errorf("rate limited")
			time.Sleep(2 * time.Second)
			continue
		}

		if resp.StatusCode >= 500 {
			resp.Body.Close()
			lastErr = fmt.Errorf("server error %d", resp.StatusCode)
			continue
		}

		return resp, nil
	}
	return nil, lastErr
}

func (c *Checker) normalizeURL(target string) string {
	target = strings.TrimSpace(target)
	target = strings.TrimSuffix(target, "/")
	target = strings.TrimPrefix(target, "https://")
	target = strings.TrimPrefix(target, "http://")
	return "https://" + target
}

func (c *Checker) extractDomain(targetURL string) string {
	parsed, err := url.Parse(c.normalizeURL(targetURL))
	if err != nil {
		return targetURL
	}
	host := parsed.Host
	host = strings.TrimPrefix(host, "www.")
	return host
}

func (c *Checker) checkAll(ctx context.Context, target string) CheckResult {
	domain := c.extractDomain(target)
	result := CheckResult{Domain: domain}

	methods := []struct {
		name string
		fn   func(context.Context, string) *CheckResult
	}{
		{"wp-login", c.checkWPLogin},
		{"wp-json", c.checkWPJSON},
		{"html", c.checkHomepage},
		{"xmlrpc", c.checkXMLRPC},
		{"wp-cron", c.checkWPCron},
		{"wp-links-opml", c.checkWPLinksOPML},
		{"wp-sitemap", c.checkWPSitemap},
		{"feed", c.checkWPFeed},
		{"wp-admin", c.checkWPAdmin},
		{"wp-includes", c.checkWPIncludes},
	}

	for _, m := range methods {
		if res := m.fn(ctx, target); res != nil {
			return *res
		}
	}

	result.IsWordPress = false
	return result
}

func (c *Checker) Check(ctx context.Context, target string) CheckResult {
	target = c.normalizeURL(target)
	result := c.checkAll(ctx, target)

	if result.IsWordPress || result.Error == nil {
		return result
	}

	errStr := result.Error.Error()
	networkErr := strings.Contains(errStr, "tls:") ||
		strings.Contains(errStr, "certificate") ||
		strings.Contains(errStr, "connection refused") ||
		strings.Contains(errStr, "no such host") ||
		strings.Contains(errStr, "timeout") ||
		strings.Contains(errStr, "dial tcp")

	if !networkErr {
		return result
	}

	httpTarget := "http://" + strings.TrimPrefix(target, "https://")
	httpResult := c.checkAll(ctx, httpTarget)
	httpResult.Domain = result.Domain
	if httpResult.IsWordPress || httpResult.Error == nil {
		return httpResult
	}

	return result
}

func (c *Checker) checkWPLogin(ctx context.Context, target string) *CheckResult {
	domain := c.extractDomain(target)
	loginURL := target + "/wp-login.php"

	noRedirectClient := &http.Client{
		Timeout:   c.client.Timeout,
		Transport: c.client.Transport,
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}

	req, err := http.NewRequestWithContext(ctx, "HEAD", loginURL, nil)
	if err != nil {
		return nil
	}
	req.Header.Set("User-Agent", c.userAgent)

	resp, err := noRedirectClient.Do(req)
	if err != nil {
		return nil
	}
	resp.Body.Close()

	if resp.StatusCode == 200 || resp.StatusCode == 302 || resp.StatusCode == 301 {
		if resp.StatusCode == 301 || resp.StatusCode == 302 {
			loc := resp.Header.Get("Location")
			if loc != "" && !strings.Contains(loc, "wp-login") && !strings.Contains(loc, "wp-admin") {
				return nil
			}
		}
		return &CheckResult{
			Domain:      domain,
			IsWordPress: true,
			Method:      "wp-login",
			StatusCode:  resp.StatusCode,
		}
	}
	return nil
}

func (c *Checker) checkWPJSON(ctx context.Context, target string) *CheckResult {
	domain := c.extractDomain(target)
	jsonURL := target + "/wp-json/"

	resp, err := c.doRequest(ctx, "GET", jsonURL)
	if err != nil {
		return nil
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		return nil
	}

	body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
	if err != nil {
		return nil
	}

	var data map[string]interface{}
	if json.Unmarshal(body, &data) != nil {
		return nil
	}

	if _, ok := data["name"]; ok {
		return &CheckResult{
			Domain:      domain,
			IsWordPress: true,
			Method:      "wp-json",
			StatusCode:  200,
		}
	}
	return nil
}

func (c *Checker) checkHomepage(ctx context.Context, target string) *CheckResult {
	domain := c.extractDomain(target)

	resp, err := c.doRequest(ctx, "GET", target+"/")
	if err != nil {
		return &CheckResult{
			Domain: domain,
			Error:  err,
		}
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 400 {
		return &CheckResult{
			Domain:     domain,
			Error:      fmt.Errorf("HTTP %d", resp.StatusCode),
			StatusCode: resp.StatusCode,
		}
	}

	body, err := io.ReadAll(io.LimitReader(resp.Body, 512*1024))
	if err != nil {
		return nil
	}

	html := strings.ToLower(string(body))

	wpSignals := []string{
		"wp-content",
		"wp-includes",
		`<meta name="generator" content="wordpress`,
	}

	for _, signal := range wpSignals {
		if strings.Contains(html, signal) {
			return &CheckResult{
				Domain:      domain,
				IsWordPress: true,
				Method:      "html",
				StatusCode:  resp.StatusCode,
			}
		}
	}

	return nil
}

func (c *Checker) checkXMLRPC(ctx context.Context, target string) *CheckResult {
	domain := c.extractDomain(target)

	noRedirectClient := &http.Client{
		Timeout:   c.client.Timeout,
		Transport: c.client.Transport,
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}

	req, err := http.NewRequestWithContext(ctx, "POST", target+"/xmlrpc.php", strings.NewReader("<methodCall><methodName>system.listMethods</methodName></methodCall>"))
	if err != nil {
		return nil
	}
	req.Header.Set("User-Agent", c.userAgent)
	req.Header.Set("Content-Type", "text/xml")

	resp, err := noRedirectClient.Do(req)
	if err != nil {
		return nil
	}
	defer resp.Body.Close()

	if resp.StatusCode == 405 {
		return &CheckResult{
			Domain:      domain,
			IsWordPress: true,
			Method:      "xmlrpc",
			StatusCode:  405,
		}
	}

	if resp.StatusCode == 200 {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 16*1024))
		content := string(body)
		if strings.Contains(content, "<?xml") && (strings.Contains(content, "wp.") || strings.Contains(content, "blogger.") || strings.Contains(content, "metaWeblog.")) {
			return &CheckResult{
				Domain:      domain,
				IsWordPress: true,
				Method:      "xmlrpc",
				StatusCode:  200,
			}
		}
	}
	return nil
}

func (c *Checker) checkWPCron(ctx context.Context, target string) *CheckResult {
	domain := c.extractDomain(target)

	resp, err := c.doRequest(ctx, "GET", target+"/wp-cron.php")
	if err != nil {
		return nil
	}
	defer resp.Body.Close()

	if resp.StatusCode == 200 {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
		if len(body) < 100 {
			return &CheckResult{
				Domain:      domain,
				IsWordPress: true,
				Method:      "wp-cron",
				StatusCode:  200,
			}
		}
	}
	return nil
}

func (c *Checker) checkWPLinksOPML(ctx context.Context, target string) *CheckResult {
	domain := c.extractDomain(target)

	resp, err := c.doRequest(ctx, "GET", target+"/wp-links-opml.php")
	if err != nil {
		return nil
	}
	defer resp.Body.Close()

	if resp.StatusCode == 200 {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 8*1024))
		content := strings.ToLower(string(body))
		if strings.Contains(content, "opml") || strings.Contains(content, "generator=\"wordpress") {
			return &CheckResult{
				Domain:      domain,
				IsWordPress: true,
				Method:      "wp-links-opml",
				StatusCode:  200,
			}
		}
	}
	return nil
}

func (c *Checker) checkWPSitemap(ctx context.Context, target string) *CheckResult {
	domain := c.extractDomain(target)

	resp, err := c.doRequest(ctx, "GET", target+"/wp-sitemap.xml")
	if err != nil {
		return nil
	}
	defer resp.Body.Close()

	if resp.StatusCode == 200 {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 16*1024))
		content := string(body)
		if strings.Contains(content, "wp-sitemap") || strings.Contains(content, "wp-sitemap-posts") || strings.Contains(content, "wp-sitemap-taxonomies") || strings.Contains(content, "wp-sitemap-users") {
			return &CheckResult{
				Domain:      domain,
				IsWordPress: true,
				Method:      "wp-sitemap",
				StatusCode:  200,
			}
		}
	}
	return nil
}

func (c *Checker) checkWPFeed(ctx context.Context, target string) *CheckResult {
	domain := c.extractDomain(target)

	resp, err := c.doRequest(ctx, "GET", target+"/feed/")
	if err != nil {
		return nil
	}
	defer resp.Body.Close()

	if resp.StatusCode == 200 {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 32*1024))
		content := strings.ToLower(string(body))
		if strings.Contains(content, "generator>https://wordpress.org") || strings.Contains(content, "generator>http://wordpress.org") || (strings.Contains(content, "<rss") && strings.Contains(content, "wp-content")) {
			return &CheckResult{
				Domain:      domain,
				IsWordPress: true,
				Method:      "feed",
				StatusCode:  200,
			}
		}
	}
	return nil
}

func (c *Checker) checkWPAdmin(ctx context.Context, target string) *CheckResult {
	domain := c.extractDomain(target)

	noRedirectClient := &http.Client{
		Timeout:   c.client.Timeout,
		Transport: c.client.Transport,
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}

	req, err := http.NewRequestWithContext(ctx, "HEAD", target+"/wp-admin/", nil)
	if err != nil {
		return nil
	}
	req.Header.Set("User-Agent", c.userAgent)

	resp, err := noRedirectClient.Do(req)
	if err != nil {
		return nil
	}
	resp.Body.Close()

	if resp.StatusCode == 301 || resp.StatusCode == 302 {
		loc := resp.Header.Get("Location")
		if strings.Contains(loc, "wp-login") || strings.Contains(loc, "wp-admin") {
			return &CheckResult{
				Domain:      domain,
				IsWordPress: true,
				Method:      "wp-admin",
				StatusCode:  resp.StatusCode,
			}
		}
	}
	return nil
}

func (c *Checker) checkWPIncludes(ctx context.Context, target string) *CheckResult {
	domain := c.extractDomain(target)

	noRedirectClient := &http.Client{
		Timeout:   c.client.Timeout,
		Transport: c.client.Transport,
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}

	req, err := http.NewRequestWithContext(ctx, "HEAD", target+"/wp-includes/", nil)
	if err != nil {
		return nil
	}
	req.Header.Set("User-Agent", c.userAgent)

	resp, err := noRedirectClient.Do(req)
	if err != nil {
		return nil
	}
	resp.Body.Close()

	if resp.StatusCode == 403 || resp.StatusCode == 200 {
		return &CheckResult{
			Domain:      domain,
			IsWordPress: true,
			Method:      "wp-includes",
			StatusCode:  resp.StatusCode,
		}
	}
	return nil
}

// ===========================================================================
// HUNTER — Hash Hunter Scanning Engine
// ===========================================================================

type WPUser struct {
	ID         int               `json:"id"`
	Name       string            `json:"name"`
	Slug       string            `json:"slug"`
	Link       string            `json:"link"`
	AvatarURLs map[string]string `json:"avatar_urls"`
}

type UserResult struct {
	Domain      string
	UserID      int
	Username    string
	DisplayName string
	Hash        string
	HashType    string
	AvatarURL   string
	ProfileURL  string
	Source      string
}

type ScanResult struct {
	Target     string
	Domain     string
	Users      []UserResult
	Status     string
	Error      error
	NewHashes  int
	SkippedOld int
	APITotal   int
	APIFetched int
}

var (
	gravatarHashRegex = regexp.MustCompile(`gravatar\.com/avatar/([a-f0-9]{32,64})`)
	authorLinkRegex   = regexp.MustCompile(`/author/([^/"]+)`)
	wpHashInHTML      = regexp.MustCompile(`avatar-([a-f0-9]{32,64})`)
	wpAvatarHashRegex = regexp.MustCompile(`/avatar/([a-f0-9]{32,64})`)
)

type Hunter struct {
	client       *http.Client
	userAgent    string
	maxRetries   int
	retryDelay   time.Duration
	enableScrape bool
	onProgress   func(msg string)
}

func (h *Hunter) SetProgressCallback(fn func(string)) {
	h.onProgress = fn
}

func (h *Hunter) progress(msg string) {
	if h.onProgress != nil {
		h.onProgress(msg)
	}
}

func NewHunter(timeout time.Duration) *Hunter {
	transport := &http.Transport{
		TLSClientConfig:       &tls.Config{InsecureSkipVerify: true},
		MaxIdleConns:          500,
		MaxIdleConnsPerHost:   50,
		MaxConnsPerHost:       50,
		IdleConnTimeout:       90 * time.Second,
		DisableKeepAlives:     false,
		ForceAttemptHTTP2:     false,
		ResponseHeaderTimeout: timeout,
		TLSHandshakeTimeout:   5 * time.Second,
	}
	return &Hunter{
		client:       &http.Client{Timeout: timeout, Transport: transport},
		userAgent:    UserAgent,
		maxRetries:   3,
		retryDelay:   500 * time.Millisecond,
		enableScrape: false,
	}
}

func (h *Hunter) doRequestWithRetry(ctx context.Context, url string) (*http.Response, error) {
	var lastErr error
	for attempt := 0; attempt <= h.maxRetries; attempt++ {
		if attempt > 0 {
			select {
			case <-ctx.Done():
				return nil, ctx.Err()
			case <-time.After(h.retryDelay * time.Duration(attempt)):
			}
		}

		req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
		if err != nil {
			return nil, err
		}
		req.Header.Set("User-Agent", h.userAgent)
		req.Header.Set("Accept", "application/json, text/html, */*")

		resp, err := h.client.Do(req)
		if err != nil {
			lastErr = err
			continue
		}

		if resp.StatusCode == 429 {
			resp.Body.Close()
			lastErr = fmt.Errorf("rate limited")
			time.Sleep(2 * time.Second)
			continue
		}

		if resp.StatusCode >= 500 {
			resp.Body.Close()
			lastErr = fmt.Errorf("server error %d", resp.StatusCode)
			continue
		}

		return resp, nil
	}
	return nil, lastErr
}

func (h *Hunter) Scan(ctx context.Context, target string) ScanResult {
	target = h.normalizeURL(target)
	result := ScanResult{Domain: h.extractDomain(target)}
	seenHashes := make(map[string]bool)

	apiUsers, rawFetched, totalExpected := h.fetchFromAPIComplete(ctx, target)

	if len(apiUsers) == 0 && !strings.Contains(target, "://www.") {
		wwwTarget := strings.Replace(target, "://", "://www.", 1)
		apiUsers, rawFetched, totalExpected = h.fetchFromAPIComplete(ctx, wwwTarget)
		if len(apiUsers) > 0 {
			target = wwwTarget
			result.Domain = h.extractDomain(wwwTarget)
		}
	}

	result.APITotal = totalExpected
	result.APIFetched = rawFetched

	for _, u := range apiUsers {
		if u.Hash != "" && !seenHashes[u.Hash] {
			seenHashes[u.Hash] = true
			result.Users = append(result.Users, u)
		}
	}

	if ctx.Err() == nil {
		sitemapUsers := h.parseForAuthors(ctx, target)
		for _, u := range sitemapUsers {
			if u.Hash != "" && !seenHashes[u.Hash] {
				seenHashes[u.Hash] = true
				result.Users = append(result.Users, u)
			}
		}
	}

	if ctx.Err() == nil {
		scrapeUsers := h.scrapeAuthorPages(ctx, target, result.Users)
		for _, u := range scrapeUsers {
			if u.Hash != "" && !seenHashes[u.Hash] {
				seenHashes[u.Hash] = true
				result.Users = append(result.Users, u)
			}
		}
	}

	if ctx.Err() == nil {
		deepUsers := h.scrapeDeep(ctx, target)
		for _, u := range deepUsers {
			if u.Hash != "" && !seenHashes[u.Hash] {
				seenHashes[u.Hash] = true
				result.Users = append(result.Users, u)
			}
		}
	}

	if len(result.Users) > 0 {
		result.Status = "found"
	} else if result.APITotal > 0 {
		result.Status = "incomplete"
	} else {
		result.Status = "no_hashes"
	}
	return result
}

func (h *Hunter) fetchFromAPIComplete(ctx context.Context, target string) ([]UserResult, int, int) {
	var allUsers []UserResult
	var totalUsers int
	var rawFetched int

	endpoints := []string{
		"/wp-json/wp/v2/users",
		"/?rest_route=/wp/v2/users",
		"/index.php?rest_route=/wp/v2/users",
	}

	for _, endpoint := range endpoints {
		baseURL := target + endpoint

		firstURL := baseURL + "?per_page=100&page=1"
		if strings.Contains(endpoint, "?") {
			firstURL = baseURL + "&per_page=100&page=1"
		}

		users, totalPages, total, err := h.fetchUsersPageWithTotal(ctx, firstURL)
		if err != nil || len(users) == 0 {
			continue
		}

		totalUsers = total
		rawFetched += len(users)
		allUsers = append(allUsers, h.convertUsers(users, "api", h.extractDomain(target))...)

		fetchedPages := make(map[int]bool)
		pageRetries := make(map[int]int)
		fetchedPages[1] = true

		maxPageRetries := 3

		for {
			allFetched := true
			anyProgress := false

			for page := 2; page <= totalPages; page++ {
				if fetchedPages[page] {
					continue
				}

				if pageRetries[page] >= maxPageRetries {
					continue
				}

				select {
				case <-ctx.Done():
					return allUsers, rawFetched, totalUsers
				default:
				}

				pageURL := baseURL
				if strings.Contains(endpoint, "?") {
					pageURL += fmt.Sprintf("&per_page=100&page=%d", page)
				} else {
					pageURL += fmt.Sprintf("?per_page=100&page=%d", page)
				}

				pageUsers, _, _, err := h.fetchUsersPageWithTotal(ctx, pageURL)
				if err != nil {
					pageRetries[page]++
					allFetched = false
					continue
				}

				if len(pageUsers) > 0 {
					fetchedPages[page] = true
					rawFetched += len(pageUsers)
					allUsers = append(allUsers, h.convertUsers(pageUsers, "api", h.extractDomain(target))...)
					anyProgress = true
				} else {
					pageRetries[page]++
					allFetched = false
				}
			}

			if allFetched {
				break
			}

			allExhausted := true
			for page := 2; page <= totalPages; page++ {
				if !fetchedPages[page] && pageRetries[page] < maxPageRetries {
					allExhausted = false
					break
				}
			}

			if allExhausted {
				break
			}

			if !anyProgress {
				time.Sleep(200 * time.Millisecond)
			}
		}

		if len(allUsers) > 0 {
			break
		}
	}

	return allUsers, rawFetched, totalUsers
}

func (h *Hunter) fetchUsersPageWithTotal(ctx context.Context, pageURL string) ([]WPUser, int, int, error) {
	resp, err := h.doRequestWithRetry(ctx, pageURL)
	if err != nil {
		return nil, 0, 0, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		return nil, 0, 0, fmt.Errorf("status %d", resp.StatusCode)
	}

	totalPages := 1
	totalUsers := 0

	if tp := resp.Header.Get("X-WP-TotalPages"); tp != "" {
		fmt.Sscanf(tp, "%d", &totalPages)
	}
	if tu := resp.Header.Get("X-WP-Total"); tu != "" {
		fmt.Sscanf(tu, "%d", &totalUsers)
	}

	body, _ := io.ReadAll(io.LimitReader(resp.Body, 10*1024*1024))
	var users []WPUser
	json.Unmarshal(body, &users)
	return users, totalPages, totalUsers, nil
}

func (h *Hunter) parseForAuthors(ctx context.Context, target string) []UserResult {
	var allUsers []UserResult
	authorURLs := make(map[string]bool)

	sitemapURLs := []string{
		target + "/sitemap.xml",
		target + "/sitemap_index.xml",
		target + "/wp-sitemap.xml",
		target + "/wp-sitemap-users-1.xml",
	}

	for _, sitemapURL := range sitemapURLs {
		resp, err := h.doRequestWithRetry(ctx, sitemapURL)
		if err != nil {
			continue
		}

		if resp.StatusCode == 200 {
			body, _ := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
			resp.Body.Close()

			matches := authorLinkRegex.FindAllStringSubmatch(string(body), -1)
			for _, m := range matches {
				if len(m) > 1 {
					authorURL := target + "/author/" + m[1] + "/"
					authorURLs[authorURL] = true
				}
			}

			if strings.Contains(string(body), "wp-sitemap-users") {
				userSitemapRegex := regexp.MustCompile(`(https?://[^<"]+wp-sitemap-users[^<"]+)`)
				userSitemaps := userSitemapRegex.FindAllString(string(body), -1)
				for _, usm := range userSitemaps {
					resp2, err := h.doRequestWithRetry(ctx, usm)
					if err == nil && resp2.StatusCode == 200 {
						body2, _ := io.ReadAll(resp2.Body)
						resp2.Body.Close()
						locRegex := regexp.MustCompile(`<loc>([^<]+)</loc>`)
						locs := locRegex.FindAllStringSubmatch(string(body2), -1)
						for _, loc := range locs {
							if len(loc) > 1 {
								authorURLs[loc[1]] = true
							}
						}
					}
				}
			}
		} else {
			resp.Body.Close()
		}
	}

	for authorURL := range authorURLs {
		users := h.scrapePageForHashes(ctx, authorURL, "sitemap", h.extractDomain(target))
		allUsers = append(allUsers, users...)
	}

	return allUsers
}

func (h *Hunter) scrapeAuthorPages(ctx context.Context, target string, knownUsers []UserResult) []UserResult {
	var allUsers []UserResult

	for _, user := range knownUsers {
		if user.ProfileURL != "" {
			scraped := h.scrapePageForHashes(ctx, user.ProfileURL, "scrape", user.Domain)
			allUsers = append(allUsers, scraped...)
		}
	}

	commonPages := []string{
		target + "/author/",
		target + "/authors/",
		target + "/team/",
		target + "/about/",
		target + "/staff/",
	}

	for _, pageURL := range commonPages {
		resp, err := h.doRequestWithRetry(ctx, pageURL)
		if err != nil || resp.StatusCode != 200 {
			if resp != nil {
				resp.Body.Close()
			}
			continue
		}

		body, _ := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
		resp.Body.Close()

		matches := gravatarHashRegex.FindAllStringSubmatch(string(body), -1)
		for _, m := range matches {
			if len(m) > 1 {
				allUsers = append(allUsers, UserResult{
					Domain:   h.extractDomain(target),
					Hash:     m[1],
					HashType: detectHashType(m[1]),
					Source:   "scrape",
				})
			}
		}

		avatarMatches := wpHashInHTML.FindAllStringSubmatch(string(body), -1)
		for _, m := range avatarMatches {
			if len(m) > 1 {
				allUsers = append(allUsers, UserResult{
					Domain:   h.extractDomain(target),
					Hash:     m[1],
					HashType: detectHashType(m[1]),
					Source:   "scrape",
				})
			}
		}
	}

	return allUsers
}

func (h *Hunter) scrapeDeep(ctx context.Context, target string) []UserResult {
	var allUsers []UserResult
	seen := make(map[string]bool)
	scrapedURLs := make(map[string]bool)

	addResults := func(users []UserResult) {
		for _, u := range users {
			if u.Hash != "" && !seen[u.Hash] {
				seen[u.Hash] = true
				allUsers = append(allUsers, u)
			}
		}
	}

	domain := h.extractDomain(target)

	resp, err := h.doRequestWithRetry(ctx, target)
	if err != nil || resp.StatusCode != 200 {
		if resp != nil {
			resp.Body.Close()
		}
		return allUsers
	}

	body, _ := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
	resp.Body.Close()
	bodyStr := string(body)

	addResults(h.extractHashesFromHTML(bodyStr, domain))

	linkRegex := regexp.MustCompile(`href=["'](` + regexp.QuoteMeta(target) + `/[^"'#?]+)["']`)
	links := linkRegex.FindAllStringSubmatch(bodyStr, -1)

	var postURLs []string
	for _, m := range links {
		if len(m) > 1 && !scrapedURLs[m[1]] {
			link := m[1]
			if strings.Contains(link, "/wp-content/") || strings.Contains(link, "/wp-admin/") ||
				strings.Contains(link, "/feed/") || strings.Contains(link, "/wp-includes/") ||
				strings.Contains(link, ".css") || strings.Contains(link, ".js") ||
				strings.Contains(link, ".png") || strings.Contains(link, ".jpg") {
				continue
			}
			scrapedURLs[link] = true
			postURLs = append(postURLs, link)
		}
	}

	if len(postURLs) > 20 {
		postURLs = postURLs[:20]
	}

	for _, postURL := range postURLs {
		if ctx.Err() != nil {
			break
		}
		users := h.scrapePageForHashes(ctx, postURL, "scrape", domain)
		addResults(users)
	}

	commentFeeds := []string{
		target + "/comments/feed/",
		target + "/?feed=comments-rss2",
	}
	for _, feedURL := range commentFeeds {
		if ctx.Err() != nil {
			break
		}
		resp, err := h.doRequestWithRetry(ctx, feedURL)
		if err != nil || resp.StatusCode != 200 {
			if resp != nil {
				resp.Body.Close()
			}
			continue
		}
		feedBody, _ := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
		resp.Body.Close()
		addResults(h.extractHashesFromHTML(string(feedBody), domain))
	}

	return allUsers
}

func (h *Hunter) extractHashesFromHTML(body string, domain string) []UserResult {
	var users []UserResult
	seen := make(map[string]bool)

	matches := gravatarHashRegex.FindAllStringSubmatch(body, -1)
	for _, m := range matches {
		if len(m) > 1 && !seen[m[1]] {
			seen[m[1]] = true
			users = append(users, UserResult{
				Domain:   domain,
				Hash:     m[1],
				HashType: detectHashType(m[1]),
				Source:   "scrape",
			})
		}
	}

	avatarMatches := wpHashInHTML.FindAllStringSubmatch(body, -1)
	for _, m := range avatarMatches {
		if len(m) > 1 && !seen[m[1]] {
			seen[m[1]] = true
			users = append(users, UserResult{
				Domain:   domain,
				Hash:     m[1],
				HashType: detectHashType(m[1]),
				Source:   "scrape",
			})
		}
	}

	wpMatches := wpAvatarHashRegex.FindAllStringSubmatch(body, -1)
	for _, m := range wpMatches {
		if len(m) > 1 && !seen[m[1]] {
			seen[m[1]] = true
			users = append(users, UserResult{
				Domain:   domain,
				Hash:     m[1],
				HashType: detectHashType(m[1]),
				Source:   "scrape",
			})
		}
	}

	return users
}

func (h *Hunter) scrapePageForHashes(ctx context.Context, pageURL, source, domain string) []UserResult {
	var users []UserResult

	resp, err := h.doRequestWithRetry(ctx, pageURL)
	if err != nil || resp.StatusCode != 200 {
		if resp != nil {
			resp.Body.Close()
		}
		return users
	}

	body, _ := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
	resp.Body.Close()

	matches := gravatarHashRegex.FindAllStringSubmatch(string(body), -1)
	seen := make(map[string]bool)
	for _, m := range matches {
		if len(m) > 1 && !seen[m[1]] {
			seen[m[1]] = true
			users = append(users, UserResult{
				Domain:   domain,
				Hash:     m[1],
				HashType: detectHashType(m[1]),
				Source:   source,
			})
		}
	}

	avatarMatches := wpAvatarHashRegex.FindAllStringSubmatch(string(body), -1)
	for _, m := range avatarMatches {
		if len(m) > 1 && !seen[m[1]] {
			seen[m[1]] = true
			users = append(users, UserResult{
				Domain:   domain,
				Hash:     m[1],
				HashType: detectHashType(m[1]),
				Source:   source,
			})
		}
	}

	return users
}

func (h *Hunter) convertUsers(wpUsers []WPUser, source, domain string) []UserResult {
	var results []UserResult
	for _, user := range wpUsers {
		result := UserResult{
			Domain:      domain,
			UserID:      user.ID,
			Username:    user.Slug,
			DisplayName: user.Name,
			ProfileURL:  user.Link,
			Source:      source,
		}

		for _, avatarURL := range user.AvatarURLs {
			if hash := h.extractHash(avatarURL); hash != "" {
				result.Hash = hash
				result.HashType = detectHashType(hash)
				result.AvatarURL = avatarURL
				break
			}
		}

		if result.Hash != "" {
			results = append(results, result)
		}
	}
	return results
}

func (h *Hunter) extractHash(avatarURL string) string {
	matches := gravatarHashRegex.FindStringSubmatch(avatarURL)
	if len(matches) >= 2 {
		return matches[1]
	}
	matches = wpAvatarHashRegex.FindStringSubmatch(avatarURL)
	if len(matches) >= 2 {
		return matches[1]
	}
	return ""
}

func detectHashType(hash string) string {
	if len(hash) == 64 {
		return "sha256"
	}
	return "md5"
}

func (h *Hunter) normalizeURL(target string) string {
	target = strings.TrimSpace(target)
	target = strings.TrimSuffix(target, "/")
	target = strings.TrimPrefix(target, "https://")
	target = strings.TrimPrefix(target, "http://")
	target = "https://" + target
	return target
}

func (h *Hunter) extractDomain(targetURL string) string {
	parsed, err := url.Parse(h.normalizeURL(targetURL))
	if err != nil {
		return targetURL
	}
	host := parsed.Host
	host = strings.TrimPrefix(host, "www.")
	return host
}

// ===========================================================================
// HASH RECORD (for Hash Cracker)
// ===========================================================================

type HashRecord struct {
	ID          int64
	Domain      string
	UserID      int
	Username    string
	DisplayName string
	Hash        string
	HashType    string
	Email       string
}

// ===========================================================================
// EMAIL PROVIDERS (for Hash Cracker)
// ===========================================================================

var defaultProviders = []string{
	// TIER 1: MAJOR GLOBAL PROVIDERS
	"gmail.com", "yahoo.com", "hotmail.com", "outlook.com", "live.com",
	"icloud.com", "aol.com", "protonmail.com", "mail.com", "zoho.com",

	// MICROSOFT DOMAINS
	"msn.com", "hotmail.co.uk", "hotmail.fr", "hotmail.de", "hotmail.es",
	"hotmail.it", "hotmail.ca", "hotmail.com.au", "hotmail.co.jp",
	"hotmail.com.br", "hotmail.com.mx", "hotmail.com.ar", "hotmail.co.nz",
	"hotmail.be", "hotmail.nl", "hotmail.se", "hotmail.no", "hotmail.dk",
	"hotmail.fi", "hotmail.at", "hotmail.ch", "hotmail.cl", "hotmail.co.th",
	"hotmail.co.id", "hotmail.co.kr", "hotmail.co.za", "hotmail.gr",
	"hotmail.hu", "hotmail.cz", "hotmail.pl", "hotmail.pt", "hotmail.ru",
	"hotmail.sg", "hotmail.ph", "hotmail.my", "hotmail.in",
	"outlook.co.uk", "outlook.fr", "outlook.de", "outlook.es", "outlook.it",
	"outlook.com.au", "outlook.com.br", "outlook.co.jp", "outlook.in",
	"outlook.ca", "outlook.com.mx", "outlook.com.ar", "outlook.co.nz",
	"outlook.be", "outlook.nl", "outlook.se", "outlook.no", "outlook.dk",
	"outlook.at", "outlook.ch", "outlook.ie", "outlook.pt", "outlook.cl",
	"outlook.co.th", "outlook.co.id", "outlook.sg", "outlook.ph", "outlook.my",
	"outlook.sa", "outlook.ae", "outlook.co.za", "outlook.kr",
	"live.co.uk", "live.fr", "live.de", "live.nl", "live.be", "live.it",
	"live.se", "live.no", "live.dk", "live.at", "live.ca", "live.com.au",
	"live.cl", "live.com.mx", "live.com.ar", "live.com.br", "live.co.za",
	"live.ie", "live.in", "live.jp", "live.cn", "live.hk",

	// YAHOO DOMAINS
	"ymail.com", "rocketmail.com", "yahoo.co.uk", "yahoo.fr", "yahoo.de",
	"yahoo.es", "yahoo.it", "yahoo.ca", "yahoo.com.au", "yahoo.co.jp",
	"yahoo.com.br", "yahoo.com.mx", "yahoo.com.ar", "yahoo.co.nz",
	"yahoo.be", "yahoo.nl", "yahoo.se", "yahoo.no", "yahoo.dk", "yahoo.fi",
	"yahoo.at", "yahoo.ch", "yahoo.ie", "yahoo.pt", "yahoo.gr", "yahoo.pl",
	"yahoo.co.in", "yahoo.in", "yahoo.co.id", "yahoo.com.sg", "yahoo.com.ph",
	"yahoo.com.my", "yahoo.co.th", "yahoo.com.vn", "yahoo.com.tw",
	"yahoo.com.hk", "yahoo.co.kr", "yahoo.cn", "yahoo.com.cn",
	"yahoo.co.za", "yahoo.com.tr", "yahoo.ro", "yahoo.hu", "yahoo.cz",

	// GOOGLE DOMAINS
	"googlemail.com", "google.com",

	// APPLE DOMAINS
	"me.com", "mac.com",

	// PROTON DOMAINS
	"proton.me", "pm.me", "protonmail.ch",

	// USA - ISPs & TELECOM
	"comcast.net", "verizon.net", "att.net", "sbcglobal.net", "cox.net",
	"charter.net", "earthlink.net", "juno.com", "netzero.net", "bellsouth.net",
	"roadrunner.com", "rr.com", "twc.com", "optimum.net", "optonline.net",
	"frontier.com", "frontiernet.net", "windstream.net", "centurylink.net",
	"centurytel.net", "embarqmail.com", "q.com", "suddenlink.net",
	"mediacombb.net", "cableone.net", "wowway.com", "rcn.com",
	"ptd.net", "zoominternet.net", "consolidated.net", "snet.net",

	// USA - OTHER
	"usa.com", "email.com", "writeme.com", "post.com", "mail.usa.com",
	"usa.net", "netscape.net", "cs.com", "aim.com", "compuserve.com",

	// UK PROVIDERS
	"btinternet.com", "btopenworld.com", "talk21.com", "talktalk.net",
	"talktalk.co.uk", "virginmedia.com", "virgin.net", "ntlworld.com",
	"sky.com", "blueyonder.co.uk", "plusnet.com", "zen.co.uk",
	"mail.co.uk", "email.co.uk", "fsmail.net", "freeserve.co.uk",
	"lineone.net", "tiscali.co.uk", "orange.co.uk", "orangehome.co.uk",
	"wanadoo.co.uk", "o2.co.uk", "o2online.de", "vodafone.co.uk",
	"ee.co.uk", "three.co.uk", "postmaster.co.uk", "ukgateway.net",

	// GERMANY
	"gmx.de", "gmx.net", "gmx.at", "gmx.ch", "gmx.com",
	"web.de", "t-online.de", "freenet.de", "arcor.de", "vodafone.de",
	"1und1.de", "online.de", "email.de", "mail.de", "posteo.de",
	"mailbox.org", "kabelmail.de", "kabelbw.de", "unity-mail.de",
	"unitybox.de", "alice.de", "ewe.net", "ewetel.net", "osnanet.de",
	"netcologne.de", "mnet-mail.de", "versanet.de", "bluewin.ch",

	// FRANCE
	"orange.fr", "wanadoo.fr", "free.fr", "sfr.fr", "laposte.net",
	"bbox.fr", "numericable.fr", "neuf.fr", "club-internet.fr",
	"aliceadsl.fr", "cegetel.net", "noos.fr", "voila.fr",
	"libertysurf.fr", "infonie.fr", "nordnet.fr", "dartybox.com",

	// SPAIN
	"telefonica.net", "movistar.es", "terra.es", "ya.com", "ono.com",
	"euskaltel.es", "jazztel.es", "orange.es", "vodafone.es",
	"eresmas.com", "wanadoo.es", "able.es", "mixmail.com",

	// ITALY
	"libero.it", "virgilio.it", "tin.it", "alice.it", "tim.it",
	"tiscali.it", "fastweb.it", "fastwebnet.it", "vodafone.it",
	"wind.it", "inwind.it", "iol.it", "kataweb.it", "supereva.it",
	"email.it", "poste.it", "pec.it", "aruba.it",

	// NETHERLANDS
	"kpnmail.nl", "kpnplanet.nl", "ziggo.nl", "upcmail.nl", "casema.nl",
	"chello.nl", "home.nl", "hetnet.nl", "planet.nl", "quicknet.nl",
	"xs4all.nl", "tele2.nl", "online.nl", "wxs.nl", "versatel.nl",

	// BELGIUM
	"telenet.be", "skynet.be", "proximus.be", "belgacom.net",
	"voo.be", "brutele.be", "scarlet.be", "base.be", "edpnet.be",

	// NORDIC COUNTRIES
	// Denmark
	"mail.dk", "jubii.dk", "ofir.dk", "stofanet.dk", "tdcadsl.dk",
	"tele.dk", "get2net.dk", "c.teledk.dk", "youmail.dk", "444.dk",
	// Sweden
	"telia.com", "spray.se", "bredband.net", "comhem.se", "swipnet.se",
	"home.se", "glocalnet.net", "passagen.se", "bahnhof.se", "ownit.se",
	// Norway
	"online.no", "frisurf.no", "start.no", "telenor.com", "getmail.no",
	"broadpark.no", "c2i.net", "nextgentel.com", "altibox.no",
	// Finland
	"saunalahti.fi", "kolumbus.fi", "welho.com", "suomi24.fi",
	"elisa.fi", "pp.inet.fi", "jippii.fi", "netti.fi", "luukku.com",
	// Iceland
	"simnet.is", "internet.is", "hive.is", "visir.is",

	// GREENLAND & FAROE ISLANDS
	"greennet.gl", "tele.gl", "inu.gl", "telepost.gl",
	"telepost.fo", "foroya.fo", "olivant.fo",

	// EASTERN EUROPE
	// Russia
	"mail.ru", "yandex.ru", "yandex.com", "ya.ru", "rambler.ru",
	"inbox.ru", "list.ru", "bk.ru", "internet.ru", "pochta.ru",
	// Poland
	"wp.pl", "o2.pl", "onet.pl", "interia.pl", "gazeta.pl",
	"poczta.fm", "op.pl", "tlen.pl", "vp.pl", "go2.pl",
	// Czech Republic
	"seznam.cz", "centrum.cz", "email.cz", "atlas.cz", "volny.cz",
	"post.cz", "quick.cz", "tiscali.cz",
	// Hungary
	"citromail.hu", "freemail.hu", "vipmail.hu", "t-online.hu",
	"mail.datanet.hu", "tvn.hu", "index.hu", "indamail.hu",
	// Romania
	"mail.com.ro", "rdslink.ro", "clicknet.ro", "personal.ro",
	"home.ro", "fx.ro", "k.ro", "mymail.ro", "dfrn.ro",
	// Ukraine
	"ukr.net", "i.ua", "meta.ua", "bigmir.net", "email.ua",
	"online.ua", "ua.fm", "mail.ua",
	// Bulgaria
	"abv.bg", "mail.bg", "gbg.bg", "dir.bg", "netissat.bg",

	// PORTUGAL
	"sapo.pt", "mail.pt", "clix.pt", "iol.pt", "netcabo.pt",
	"telepac.pt", "oninet.pt", "portugalmail.pt", "portugalmail.com",

	// SWITZERLAND & AUSTRIA
	"hispeed.ch", "sunrise.ch", "swissonline.ch",
	"vtxmail.ch", "green.ch", "datacomm.ch", "ticino.com",
	"aon.at", "chello.at", "kabsi.at", "a1.net", "inode.at",
	"liwest.at", "tele2.at", "utanet.at", "vienna.at",

	// GREECE & TURKEY
	"otenet.gr", "forthnet.gr", "hol.gr", "in.gr", "pathfinder.gr",
	"mailbox.gr", "mycosmos.gr", "freemail.gr", "vivodi.gr",
	"mynet.com", "mynet.com.tr", "turk.net", "superonline.com",
	"ttnet.net.tr", "hotmail.com.tr", "yahoo.com.tr", "isnet.net.tr",

	// LATIN AMERICA
	// Brazil
	"uol.com.br", "bol.com.br", "terra.com.br", "ig.com.br",
	"globo.com", "globomail.com", "oi.com.br", "r7.com",
	"zipmail.com.br", "ibest.com.br", "pop.com.br", "brturbo.com.br",
	// Mexico
	"prodigy.net.mx", "telmex.com", "infinitum.com.mx",
	"mexico.com", "mail.com.mx", "live.com.mx", "msn.com.mx",
	// Argentina
	"fibertel.com.ar", "speedy.com.ar", "arnet.com.ar", "ciudad.com.ar",
	"telecentro.com.ar", "argentina.com", "uolsinectis.com.ar",
	// Chile
	"vtr.net", "entelchile.net", "terra.cl", "mi.cl", "chile.com",
	// Colombia
	"une.net.co", "etb.net.co", "emcali.net.co", "telecom.com.co",
	// Venezuela — ISPs and Telecoms
	"cantv.net", "cantv.com.ve", "movistar.com.ve", "inter.net.ve", "telcel.net.ve",
	"supercable.net.ve", "digitel.com.ve", "netuno.net.ve", "airtek.com.ve",
	"gnetwork.net.ve", "iamnet.com", "movilnet.com.ve", "intercable.net.ve",
	"telcel.net", "reacciun.ve", "etheron.net", "viptel.com.ve",
	// Venezuela — Free/Webmail
	"venezuela.com", "correo.com.ve", "mail.com.ve", "ve.com",
	"tutopia.com", "latinmail.com", "starmedia.com",
	// Venezuela — Universities
	"ucv.ve", "usb.ve", "ula.ve", "uc.edu.ve", "luz.edu.ve", "unexpo.edu.ve",
	"ucab.edu.ve", "unimet.edu.ve", "urbe.edu.ve", "una.edu.ve", "unesr.edu.ve",
	"uneg.edu.ve", "unefm.edu.ve", "unellez.edu.ve", "unet.edu.ve",
	"ujap.edu.ve", "umc.edu.ve", "uny.edu.ve", "unefa.edu.ve", "udo.edu.ve",
	"uptm.edu.ve", "iupsm.edu.ve", "iutirla.edu.ve",
	// Venezuela — Government
	"gmail.gob.ve", "gobierno.ve", "mppre.gob.ve", "minci.gob.ve",
	"mpps.gob.ve", "mppef.gob.ve", "mpppst.gob.ve", "mpprijp.gob.ve",
	"mppd.gob.ve", "mppeuct.gob.ve", "mppt.gob.ve", "mppat.gob.ve",
	// Venezuela — Media (common employer domains)
	"elpitazo.net", "ultimasnoticias.com.ve", "diariodelosandes.com",
	"efectococuyo.com", "talcualdigital.com", "lapatilla.com",
	"telesurtv.net", "prensa-latina.cu", "vpitv.com", "larazon.net",
	"contrapunto.com", "elchiguirebipolar.net", "elnacional.com",
	"laprensalara.com.ve", "reporteconfidencial.info", "diariolavoz.net",
	"correodelorinoco.gob.ve", "elsiglo.com.ve",
	// Cuba (commonly found in VE datasets)
	"nauta.cu", "infomed.sld.cu", "jovenclub.cu", "cubarte.cult.cu",
	// Peru
	"terra.com.pe", "speedy.com.pe", "amauta.rcp.net.pe",

	// ASIA - CHINA
	"qq.com", "163.com", "126.com", "sina.com", "sina.cn",
	"sohu.com", "aliyun.com", "foxmail.com", "yeah.net", "vip.qq.com",
	"tom.com", "21cn.com", "189.cn", "139.com", "wo.cn",

	// ASIA - JAPAN
	"docomo.ne.jp", "ezweb.ne.jp", "softbank.ne.jp", "i.softbank.jp",
	"nifty.com", "nifty.ne.jp", "biglobe.ne.jp", "biglobe.jp",
	"so-net.ne.jp", "ocn.ne.jp", "plala.or.jp", "infoseek.jp",
	"excite.co.jp", "goo.jp", "ybb.ne.jp",

	// ASIA - KOREA
	"naver.com", "daum.net", "hanmail.net", "nate.com", "korea.com",
	"chol.com", "dreamwiz.com", "empal.com", "freechal.com",
	"hanmir.com", "hitel.net", "jungle.co.kr", "korea.kr",
	"paran.com", "lycos.co.kr",

	// ASIA - INDIA
	"rediffmail.com", "sify.com", "indiatimes.com", "vsnl.net",
	"sancharnet.in", "dataone.in", "airtelmail.in", "in.com",

	// ASIA - SOUTHEAST
	"telkom.net", "plasa.com", "indo.net.id", "centrin.net.id",
	"thaimail.com", "sanook.com", "chaiyo.com", "siammail.com",
	"vnn.vn", "fpt.vn", "hcm.vnn.vn", "hn.vnn.vn",
	"pldtdsl.net", "globe.com.ph", "smart.com.ph", "info.com.ph",
	"streamyx.com", "tm.net.my", "pd.jaring.my", "maxis.net.my",
	"singnet.com.sg", "starhub.net.sg", "pacific.net.sg",

	// ASIA - TAIWAN & HONG KONG
	"hinet.net", "pchome.com.tw", "seed.net.tw", "ms1.hinet.net",
	"ms2.hinet.net", "yahoo.com.tw", "yam.com", "kimo.com",
	"netvigator.com", "biznetvigator.com", "hknet.com", "i-cable.com",

	// MIDDLE EAST
	"emirates.net.ae", "eim.ae", "etisalat.ae", "du.ae",
	"saudi.net.sa", "saudimail.com", "gawab.com", "maktoob.com",
	"arabtop.net", "tedata.net.eg", "link.net.eg", "egyptmail.com",
	"israe.li", "walla.co.il", "netvision.net.il", "zahav.net.il",
	"bezeqint.net", "012.net.il", "actcom.co.il",

	// AFRICA
	"mweb.co.za", "telkomsa.net", "webmail.co.za", "afrihost.co.za",
	"vodamail.co.za", "iafrica.com", "lantic.net", "absamail.co.za",
	"cybersmart.co.za", "imaginet.co.za",
	"vodafone.com.eg", "link.com.eg", "mailer.com.eg",
	"kenya.com", "wananchi.com", "jambomail.com",
	"nigeria.com", "naijamailbox.com",

	// AUSTRALIA & NEW ZEALAND
	"bigpond.com", "bigpond.net.au", "optusnet.com.au", "ozemail.com.au",
	"iinet.net.au", "internode.on.net", "westnet.com.au", "dodo.com.au",
	"tpg.com.au", "adam.com.au", "netspace.net.au", "primus.com.au",
	"aapt.net.au", "eftel.net.au", "aussiebroadband.com.au",
	"xtra.co.nz", "slingshot.co.nz", "vodafone.co.nz", "clear.net.nz",
	"paradise.net.nz", "orcon.net.nz", "spark.co.nz", "snap.net.nz",

	// CANADA
	"rogers.com", "shaw.ca", "bell.net", "sympatico.ca", "telus.net",
	"sasktel.net", "videotron.ca", "cogeco.ca", "eastlink.ca",
	"uniserve.com", "distributel.ca", "primus.ca", "look.ca",

	// IRELAND
	"eircom.net", "eir.ie", "indigo.ie", "ireland.com", "iolfree.ie",

	// PRIVACY & SECURE EMAIL PROVIDERS
	"tutanota.com", "tutanota.de", "tutamail.com", "tuta.io",
	"startmail.com", "mailfence.com", "disroot.org", "riseup.net",
	"autistici.org", "inventati.org", "aktivix.org", "espiv.net",
	"hushmail.com", "hushmail.me", "hush.com", "hush.ai",
	"countermail.com", "runbox.com", "fastmail.com", "fastmail.fm",
	"kolabnow.com", "posteo.net",
	"ctemplar.com", "dismail.de", "elude.in",

	// WORK & PROFESSIONAL
	"outlook.office365.com", "office365.com",

	// LEGACY & MISC PROVIDERS
	"excite.com", "lycos.com", "lycos.co.uk", "lycos.de", "lycos.es",
	"inbox.com", "lavabit.com", "care2.com", "pobox.com",
	"safe-mail.net", "xmail.net", "zmail.ru",
	"mailinator.com", "guerrillamail.com", "tempail.com",
	"gmx.us", "gmx.co.uk", "gmx.fr", "gmx.es", "gmx.it",
	"caramail.com", "caramail.fr", "laposte.fr",
	"nym.hush.com", "bigfoot.com", "usa.net",

	// EDUCATIONAL COMMON PATTERNS
	"edu", "ac.uk", "edu.au", "edu.cn", "edu.mx", "edu.br",

	// TECH COMPANY DOMAINS
	"amazon.com", "apple.com", "microsoft.com", "google.com",
	"facebook.com", "meta.com", "twitter.com", "x.com",
	"linkedin.com", "github.com", "mozilla.com", "mozilla.org",
	"redhat.com", "ibm.com", "oracle.com", "salesforce.com",
	"adobe.com", "nvidia.com", "intel.com", "amd.com",
	"spotify.com", "netflix.com", "uber.com", "airbnb.com",
	"dropbox.com", "slack.com", "zoom.us", "atlassian.com",
}

// ===========================================================================
// NAME LIST LOADING (OSINT-driven pattern expansion)
// ===========================================================================

func (tui *TUI) loadNameLists() {
	// Load firstnames.txt
	firstNamesPath := filepath.Join(tui.namesDir, "firstnames.txt")
	if file, err := os.Open(firstNamesPath); err == nil {
		defer file.Close()
		scanner := bufio.NewScanner(file)
		total := 0
		for scanner.Scan() {
			line := strings.TrimSpace(scanner.Text())
			if line == "" || strings.HasPrefix(line, "#") {
				continue
			}
			name := strings.ToLower(line)
			if len(name) < 2 {
				continue
			}
			initial := string(name[0])
			tui.hcFirstNames[initial] = append(tui.hcFirstNames[initial], name)
			total++
		}
		if total > 0 {
			_ = total // loaded silently — reported when cracking starts
		}
	}

	// Load surnames.txt if it exists
	surnamesPath := filepath.Join(tui.namesDir, "surnames.txt")
	if file, err := os.Open(surnamesPath); err == nil {
		defer file.Close()
		// Future: load surnames for reverse lookups
		_ = file
	}
}

// totalFirstNames returns the count of loaded first names
func (tui *TUI) totalFirstNames() int {
	count := 0
	for _, names := range tui.hcFirstNames {
		count += len(names)
	}
	return count
}

// ===========================================================================
// PATTERN GENERATION (Hash Cracker)
// ===========================================================================

func generateEmailPatterns(username, displayName, siteDomain string, firstNames map[string][]string) []string {
	patterns := []string{}
	seen := make(map[string]bool)

	addPattern := func(p string) {
		p = strings.ToLower(strings.TrimSpace(p))
		if p != "" && len(p) >= 2 && !seen[p] {
			seen[p] = true
			patterns = append(patterns, p)
		}
	}

	addWithSeparators := func(part1, part2 string) {
		if part1 == "" || part2 == "" {
			return
		}
		addPattern(part1 + part2)
		addPattern(part1 + "." + part2)
		addPattern(part1 + "_" + part2)
		addPattern(part1 + "-" + part2)
	}

	username = strings.ToLower(strings.TrimSpace(username))
	displayName = strings.ToLower(strings.TrimSpace(displayName))

	// If no display name, try to extract name from dash-separated username
	// e.g. "abraham-pineda-bello" -> "abraham pineda bello"
	if displayName == "" && strings.Contains(username, "-") {
		parts := strings.Split(username, "-")
		allAlpha := true
		for _, p := range parts {
			if len(p) < 2 {
				allAlpha = false
				break
			}
			for _, c := range p {
				if c < 'a' || c > 'z' {
					allAlpha = false
					break
				}
			}
		}
		if allAlpha && len(parts) >= 2 {
			displayName = strings.Join(parts, " ")
		}
	}

	// OSINT DECOMPOSITION: Detect "initiallastname" format (e.g. "aacevedo" → initial=a, surname=acevedo)
	// Then expand the initial to all matching first names from our name lists
	expandedFirstNames := []string{} // all possible first names from name list
	detectedInitial := ""
	detectedSurname := ""

	if displayName == "" && firstNames != nil && len(username) >= 4 {
		cleanUser := regexp.MustCompile(`[0-9]+$`).ReplaceAllString(username, "")
		if regexp.MustCompile(`^[a-z]+$`).MatchString(cleanUser) && len(cleanUser) >= 4 {
			// Try 1-char initial + surname (most common: aacevedo → a + acevedo)
			initial := string(cleanUser[0])
			possibleSurname := cleanUser[1:]
			if len(possibleSurname) >= 3 {
				if names, ok := firstNames[initial]; ok && len(names) > 0 {
					detectedInitial = initial
					detectedSurname = possibleSurname
					expandedFirstNames = names
				}
			}

			// Also try 2-char prefix for compound initials (jcperez → jc + perez = Juan Carlos Perez)
			// Limited to top 8 names per initial to avoid combinatorial explosion
			if len(cleanUser) >= 5 {
				prefix2 := cleanUser[:2]
				possibleSurname2 := cleanUser[2:]
				if len(possibleSurname2) >= 3 {
					if names1, ok := firstNames[string(prefix2[0])]; ok {
						if names2, ok2 := firstNames[string(prefix2[1])]; ok2 {
							cap1 := len(names1)
							if cap1 > 8 { cap1 = 8 }
							cap2 := len(names2)
							if cap2 > 8 { cap2 = 8 }
							for _, fn1 := range names1[:cap1] {
								for _, fn2 := range names2[:cap2] {
									addPattern(fn1 + fn2 + possibleSurname2)
									addPattern(fn1 + "." + fn2 + "." + possibleSurname2)
									addPattern(fn1 + fn2)
									addPattern(string(fn1[0]) + fn2 + possibleSurname2)
									addPattern(fn1 + string(fn2[0]) + possibleSurname2)
								}
							}
						}
					}
				}
			}
		}
	}

	nameParts := parseNameParts(displayName)
	firstName := ""
	lastName := ""
	middleName := ""
	allMiddleNames := []string{}

	if len(nameParts) > 0 {
		firstName = nameParts[0]
	}
	if len(nameParts) > 1 {
		lastName = nameParts[len(nameParts)-1]
	}
	if len(nameParts) > 2 {
		middleName = nameParts[1]
		allMiddleNames = nameParts[1 : len(nameParts)-1]
	}

	// If we detected initial+surname pattern but have no display name,
	// use the detected surname as lastName for pattern generation
	if lastName == "" && detectedSurname != "" {
		lastName = detectedSurname
	}
	if firstName == "" && detectedInitial != "" {
		firstName = detectedInitial // single letter — patterns below will use it
	}

	firstInitial := ""
	first2 := ""
	first3 := ""
	first4 := ""
	if len(firstName) > 0 {
		firstInitial = string(firstName[0])
		if len(firstName) >= 2 {
			first2 = firstName[:2]
		}
		if len(firstName) >= 3 {
			first3 = firstName[:3]
		}
		if len(firstName) >= 4 {
			first4 = firstName[:4]
		}
	}
	lastInitial := ""
	last2 := ""
	last3 := ""
	last4 := ""
	if len(lastName) > 0 {
		lastInitial = string(lastName[0])
		if len(lastName) >= 2 {
			last2 = lastName[:2]
		}
		if len(lastName) >= 3 {
			last3 = lastName[:3]
		}
		if len(lastName) >= 4 {
			last4 = lastName[:4]
		}
	}

	// SECTION 1: USERNAME-BASED PATTERNS
	addPattern(username)
	addPattern(strings.ReplaceAll(username, "-", "."))
	addPattern(strings.ReplaceAll(username, "-", ""))
	addPattern(strings.ReplaceAll(username, "_", "."))
	addPattern(strings.ReplaceAll(username, "_", ""))
	addPattern(strings.ReplaceAll(username, ".", "-"))
	addPattern(strings.ReplaceAll(username, ".", "_"))
	addPattern(strings.ReplaceAll(username, ".", ""))

	usernameNoNums := regexp.MustCompile(`[0-9]+`).ReplaceAllString(username, "")
	if usernameNoNums != username && usernameNoNums != "" {
		addPattern(usernameNoNums)
		addPattern(strings.ReplaceAll(usernameNoNums, "-", "."))
		addPattern(strings.ReplaceAll(usernameNoNums, "-", ""))
	}

	if len(username) > 2 && regexp.MustCompile(`^[a-z]+$`).MatchString(username) {
		addPattern(string(username[0]) + "." + username[1:])
		if len(username) > 3 {
			addPattern(username[:2] + "." + username[2:])
		}
	}

	// SECTION 2: FIRST NAME ONLY PATTERNS
	if firstName != "" {
		addPattern(firstName)
		addPattern(firstName + "1")
		addPattern(firstName + "01")
		addPattern(firstName + "123")
		addPattern(firstName + firstName)
	}

	// SECTION 3: LAST NAME ONLY PATTERNS
	if lastName != "" {
		addPattern(lastName)
		addPattern(lastName + "1")
		addPattern(lastName + "01")
	}

	// SECTION 4: FIRST + LAST NAME COMBINATIONS
	if firstName != "" && lastName != "" {
		addWithSeparators(firstName, lastName)
		addWithSeparators(lastName, firstName)

		addWithSeparators(firstInitial, lastName)
		addWithSeparators(firstName, lastInitial)
		addWithSeparators(lastInitial, firstName)
		addWithSeparators(lastName, firstInitial)

		addPattern(firstInitial + lastInitial)
		addPattern(firstInitial + "." + lastInitial)
		addPattern(lastInitial + firstInitial)
		addPattern(lastInitial + "." + firstInitial)

		addPattern(first2 + last2)
		addPattern(first2 + last3)
		addPattern(first2 + last4)
		addPattern(first3 + last2)
		addPattern(first3 + last3)
		addPattern(first4 + last2)
		addPattern(last2 + first2)
		addPattern(last3 + first3)
		addPattern(first3 + lastInitial)
		addPattern(first4 + lastInitial)
		addPattern(last3 + firstInitial)
		addPattern(last4 + firstInitial)

		years := []string{
			"1", "2", "3", "4", "5", "6", "7", "8", "9",
			"01", "02", "03", "04", "05", "06", "07", "08", "09",
			"10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20",
			"21", "22", "23", "24", "25", "26",
			"50", "51", "52", "53", "54", "55", "56", "57", "58", "59",
			"60", "61", "62", "63", "64", "65", "66", "67", "68", "69",
			"70", "71", "72", "73", "74", "75", "76", "77", "78", "79",
			"80", "81", "82", "83", "84", "85", "86", "87", "88", "89",
			"90", "91", "92", "93", "94", "95", "96", "97", "98", "99",
			"00", "000", "007",
			"1990", "1991", "1992", "1993", "1994", "1995", "1996", "1997", "1998", "1999",
			"2000", "2001", "2002", "2003", "2004", "2005", "2006", "2007", "2008", "2009",
			"2010", "2011", "2012", "2013", "2014", "2015", "2016", "2017", "2018", "2019",
			"2020", "2021", "2022", "2023", "2024", "2025", "2026",
			"123", "1234", "12345", "321", "111", "222", "333", "444", "555", "666", "777", "888", "999",
			"101", "202", "303", "404", "505", "606", "707", "808", "909",
			"007", "247", "365", "360", "420", "911",
		}
		basePatterns := []string{
			firstName + lastName,
			firstName + "." + lastName,
			firstName + "_" + lastName,
			lastName + firstName,
			lastName + "." + firstName,
			firstInitial + lastName,
			firstInitial + "." + lastName,
			firstName + lastInitial,
			lastName + firstInitial,
			firstName,
			lastName,
		}
		for _, base := range basePatterns {
			if base == "" {
				continue
			}
			for _, year := range years {
				addPattern(base + year)
			}
		}

		prefixes := []string{
			"the", "real", "official", "im", "iam", "its", "hey", "hi", "mr", "mrs", "ms",
			"dr", "prof", "sir", "el", "la", "le", "de", "da", "di", "del", "van", "von",
			"mc", "mac", "o", "saint", "st", "don", "donna", "san",
		}
		for _, prefix := range prefixes {
			addPattern(prefix + firstName)
			addPattern(prefix + lastName)
			addPattern(prefix + firstName + lastName)
			addPattern(prefix + "." + firstName)
			addPattern(prefix + "_" + firstName)
		}

		suffixes := []string{
			"official", "real", "tv", "music", "art", "photo", "photos", "pics",
			"news", "media", "press", "pr", "online", "web", "net", "digital",
			"works", "studio", "studios", "design", "designs", "creative",
			"dev", "code", "tech", "hq", "inc", "co", "llc", "ltd", "corp",
			"usa", "uk", "eu", "de", "fr", "es", "it", "nl", "au", "ca", "mx", "br",
			"nyc", "la", "sf", "dc", "chi", "atl", "mia", "dal", "hou", "phx",
			"jr", "sr", "ii", "iii", "iv", "esq",
			"x", "xx", "xxx", "xo", "xoxo",
		}
		for _, suffix := range suffixes {
			addPattern(firstName + suffix)
			addPattern(lastName + suffix)
			addPattern(firstName + lastName + suffix)
			addPattern(firstName + "." + suffix)
			addPattern(firstName + "_" + suffix)
		}
	}

	// SECTION 5: MIDDLE NAME PATTERNS
	if firstName != "" && lastName != "" && middleName != "" {
		middleInitial := string(middleName[0])
		middle2 := ""
		if len(middleName) >= 2 {
			middle2 = middleName[:2]
		}

		addPattern(firstName + middleName)
		addPattern(firstName + middleName + lastName)
		addPattern(firstName + "." + middleName + "." + lastName)
		addWithSeparators(firstName+middleName, lastName)

		addPattern(firstName + middleInitial + lastName)
		addPattern(firstName + "." + middleInitial + "." + lastName)
		addPattern(firstName + middleInitial + "." + lastName)
		addPattern(firstInitial + middleInitial + lastName)
		addPattern(firstInitial + "." + middleInitial + "." + lastName)
		addPattern(firstInitial + middleInitial + "." + lastName)

		addPattern(first2 + middle2 + last2)
		addPattern(firstInitial + middleInitial + lastInitial)
		addPattern(firstInitial + "." + middleInitial + "." + lastInitial)

		if len(allMiddleNames) > 1 {
			allMiddleInitials := ""
			for _, mn := range allMiddleNames {
				if len(mn) > 0 {
					allMiddleInitials += string(mn[0])
				}
			}
			addPattern(firstInitial + allMiddleInitials + lastName)
			addPattern(firstName + allMiddleInitials + lastName)
		}
	}

	// SECTION 6: NICKNAME & DIMINUTIVE PATTERNS
	if firstName != "" {
		nicknames := generateNicknames(firstName)
		for _, nick := range nicknames {
			addPattern(nick)
			if lastName != "" {
				addWithSeparators(nick, lastName)
				addPattern(nick + lastInitial)
			}
		}
	}

	// SECTION 7: SPECIAL ROLE PATTERNS
	if strings.Contains(username, "master") || strings.Contains(username, "admin") ||
		strings.Contains(username, "editor") || strings.Contains(username, "author") ||
		strings.Contains(displayName, "admin") || strings.Contains(displayName, "editor") {
		rolePatterns := []string{
			"webmaster", "admin", "administrator", "info", "contact", "hello",
			"support", "help", "service", "services", "team", "staff",
			"editor", "author", "writer", "news", "press", "media",
			"social", "marketing", "sales", "hr", "jobs", "careers",
			"noreply", "no-reply", "donotreply", "mailer", "newsletter",
			"office", "mail", "inbox", "postmaster", "hostmaster",
		}
		for _, role := range rolePatterns {
			addPattern(role)
		}
	}

	// SECTION 8: INTERNATIONAL NAME PATTERNS
	if firstName != "" && lastName != "" {
		addWithSeparators(lastName, firstName)
		addPattern(lastName + "." + firstInitial)

		if strings.Contains(firstName, "-") {
			parts := strings.Split(firstName, "-")
			if len(parts) == 2 {
				addPattern(parts[0] + parts[1])
				addPattern(parts[0] + "." + parts[1])
				addWithSeparators(parts[0]+parts[1], lastName)
				addPattern(string(parts[0][0]) + string(parts[1][0]) + lastName)
				addPattern(string(parts[0][0]) + string(parts[1][0]) + "." + lastName)
			}
		}

		if strings.HasPrefix(lastName, "o") && len(lastName) > 1 {
			addPattern(firstName + "." + "o" + lastName[1:])
			addPattern(firstName + ".o." + lastName[1:])
		}
		if strings.HasPrefix(lastName, "mc") && len(lastName) > 2 {
			addWithSeparators(firstName, "mac"+lastName[2:])
		}
		if strings.HasPrefix(lastName, "mac") && len(lastName) > 3 {
			addWithSeparators(firstName, "mc"+lastName[3:])
		}
	}

	// SECTION 9: KEYBOARD & LEET PATTERNS
	if firstName != "" {
		leetFirst := strings.NewReplacer(
			"a", "4", "e", "3", "i", "1", "o", "0", "s", "5", "t", "7",
		).Replace(firstName)
		if leetFirst != firstName {
			addPattern(leetFirst)
			if lastName != "" {
				addPattern(leetFirst + lastName)
				addPattern(leetFirst + "." + lastName)
			}
		}
	}

	// SECTION 10: PHONETIC & TYPO PATTERNS
	if firstName != "" && lastName != "" {
		addPattern(firstName + firstName[len(firstName)-1:] + lastName)

		consonantsFirst := regexp.MustCompile(`[aeiou]`).ReplaceAllString(firstName, "")
		consonantsLast := regexp.MustCompile(`[aeiou]`).ReplaceAllString(lastName, "")
		if len(consonantsFirst) >= 2 && len(consonantsLast) >= 2 {
			addPattern(consonantsFirst + consonantsLast)
			addPattern(consonantsFirst + "." + consonantsLast)
		}
	}

	// SECTION 11: NAME EXPANSION (from name lists)
	// For initial+surname usernames like "aacevedo", expand the initial
	// to all first names starting with that letter — core combos only, no bloat
	if len(expandedFirstNames) > 0 && detectedSurname != "" {
		surname := detectedSurname
		for _, fn := range expandedFirstNames {
			addPattern(fn + surname)
			addPattern(fn + "." + surname)
			addPattern(fn + "_" + surname)
			addPattern(surname + fn)
			addPattern(surname + "." + fn)
		}
	}

	return patterns
}

func generateNicknames(firstName string) []string {
	nicknames := []string{}

	nicknameMap := map[string][]string{
		"william":     {"will", "willy", "willie", "bill", "billy", "liam"},
		"robert":      {"rob", "robbie", "robby", "bob", "bobby", "bert"},
		"richard":     {"rich", "richie", "rick", "ricky", "dick", "dickie"},
		"michael":     {"mike", "mikey", "mick", "mickey"},
		"james":       {"jim", "jimmy", "jamie", "jem"},
		"john":        {"johnny", "jack", "jackie", "jon"},
		"joseph":      {"joe", "joey", "jo"},
		"thomas":      {"tom", "tommy", "thom"},
		"charles":     {"charlie", "chuck", "chas", "char"},
		"christopher": {"chris", "topher", "kit"},
		"daniel":      {"dan", "danny", "dani"},
		"matthew":     {"matt", "matty"},
		"anthony":     {"tony", "ant", "anton"},
		"donald":      {"don", "donny", "donnie"},
		"steven":      {"steve", "stevie"},
		"stephen":     {"steve", "stevie", "steph"},
		"edward":      {"ed", "eddie", "eddy", "ted", "teddy", "ned"},
		"brian":       {"bri"},
		"ronald":      {"ron", "ronny", "ronnie"},
		"timothy":     {"tim", "timmy"},
		"jason":       {"jay", "jase"},
		"jeffrey":     {"jeff", "geoff"},
		"benjamin":    {"ben", "benny", "benji"},
		"nicholas":    {"nick", "nicky", "nico", "klaus"},
		"alexander":   {"alex", "al", "xander", "sasha", "sacha", "sandy", "lex"},
		"samuel":      {"sam", "sammy"},
		"patrick":     {"pat", "paddy", "rick"},
		"jonathan":    {"jon", "jonny", "nathan"},
		"andrew":      {"andy", "drew"},
		"elizabeth":   {"liz", "lizzy", "beth", "betty", "eliza", "ellie", "ella"},
		"margaret":    {"maggie", "meg", "peggy", "marge", "margie", "rita"},
		"jennifer":    {"jen", "jenny", "jenn"},
		"katherine":   {"kate", "katie", "kathy", "kat", "kitty"},
		"catherine":   {"cate", "cathy", "cat"},
		"patricia":    {"pat", "patty", "trish", "tricia"},
		"barbara":     {"barb", "barbie", "babs"},
		"susan":       {"sue", "suzy", "susie"},
		"jessica":     {"jess", "jessie"},
		"sarah":       {"sara"},
		"rebecca":     {"becca", "becky", "beck"},
		"deborah":     {"deb", "debbie", "debby"},
		"stephanie":   {"steph", "stephie"},
		"christina":   {"chris", "chrissy", "tina"},
		"victoria":    {"vicky", "vicki", "tori"},
		"samantha":    {"sam", "sammy"},
		"alexandra":   {"alex", "alexa", "lexi", "sasha"},
		"natalie":     {"nat", "nattie"},
		"amanda":      {"mandy", "manda"},
		"melissa":     {"mel", "missy", "lissa"},
		"abigail":     {"abby", "gail"},
		"madeline":    {"maddie", "maddy"},
		"caroline":    {"carol", "carrie"},
		"jacqueline":  {"jackie", "jacqui"},
		"frederick":   {"fred", "freddy", "freddie", "rick"},
		"gregory":     {"greg", "gregg"},
		"lawrence":    {"larry", "lars", "laurie"},
		"leonard":     {"leo", "leon", "lenny"},
		"philip":      {"phil"},
		"raymond":     {"ray"},
		"vincent":     {"vince", "vinny", "vin"},
		"theodore":    {"ted", "teddy", "theo"},
		"francisco":   {"frank", "frankie", "paco", "pancho"},
		"miguel":      {"mike", "micky"},
		"jose":        {"joe", "pepe"},
		"juan":        {"johnny"},
		"guillermo":   {"will", "willie", "memo"},
		"roberto":     {"rob", "robbie", "beto"},
		"ricardo":     {"rick", "ricky"},
		"alberto":     {"al", "bert", "beto"},
		"alejandro":   {"alex", "al"},
		"andreas":     {"andy", "andre"},
		"johannes":    {"john", "hans", "jan"},
		"heinrich":    {"henry", "hank", "heinz"},
		"friedrich":   {"fred", "fritz"},
		"wilhelm":     {"will", "willy", "bill"},
		"giuseppe":    {"joe", "beppe"},
		"giovanni":    {"john", "gianni", "gio"},
		"antonio":     {"tony", "toni"},
		"francois":    {"frank", "franck"},
		"pierre":      {"peter", "pete"},
		"jean":        {"john", "johnny"},
		"dmitri":      {"dima", "mitya"},
		"vladimir":    {"vlad", "vova"},
		"sergei":      {"serge"},
		"nikolai":     {"nick", "kolya"},
	}

	if nicks, exists := nicknameMap[firstName]; exists {
		nicknames = append(nicknames, nicks...)
	}

	if len(firstName) > 3 {
		nicknames = append(nicknames, firstName[:3]+"y")
		nicknames = append(nicknames, firstName[:3]+"ie")
		if len(firstName) > 4 {
			nicknames = append(nicknames, firstName[:4]+"y")
			nicknames = append(nicknames, firstName[:4]+"ie")
		}
	}

	return nicknames
}

func parseNameParts(name string) []string {
	name = strings.ToLower(name)
	name = regexp.MustCompile(`[^a-z\s]`).ReplaceAllString(name, " ")
	parts := strings.Fields(name)

	filtered := []string{}
	skipWords := map[string]bool{"mr": true, "mrs": true, "ms": true, "dr": true, "jr": true, "sr": true, "iii": true, "ii": true}
	for _, p := range parts {
		if !skipWords[p] && len(p) > 0 {
			filtered = append(filtered, p)
		}
	}
	return filtered
}

// formatNumber adds commas to large numbers for readability
func formatNumber(n int64) string {
	s := fmt.Sprintf("%d", n)
	if len(s) <= 3 {
		return s
	}
	// Insert commas from right
	result := ""
	for i, c := range s {
		if i > 0 && (len(s)-i)%3 == 0 {
			result += ","
		}
		result += string(c)
	}
	return result
}

func hashEmail(email, hashType string) string {
	email = strings.ToLower(strings.TrimSpace(email))
	if hashType == "sha256" || (len(hashType) == 0 && len(email) > 40) {
		h := sha256.Sum256([]byte(email))
		return hex.EncodeToString(h[:])
	}
	h := md5.Sum([]byte(email))
	return hex.EncodeToString(h[:])
}

// ===========================================================================
// CLIPBOARD
// ===========================================================================

func getClipboard() string {
	var cmd *exec.Cmd
	switch runtime.GOOS {
	case "windows":
		cmd = exec.Command("powershell", "-command", "Get-Clipboard")
	case "darwin":
		cmd = exec.Command("pbpaste")
	default:
		cmd = exec.Command("xclip", "-selection", "clipboard", "-o")
	}
	out, _ := cmd.Output()
	return strings.TrimSpace(string(out))
}

// ===========================================================================
// TUI STRUCT
// ===========================================================================

type TUI struct {
	screen     tcell.Screen
	width      int
	height     int
	running    bool
	showSplash bool
	splashFrame float64

	// Module system
	activeModule     ActiveModule
	showModuleSelect bool
	moduleSelected   int

	// Menu
	selectedItem int
	menuItems    []MenuItem

	// Input
	inputBuffer     string
	inputPrompt     string
	inputCursor     int
	commandState    CommandState
	currentCmd      string
	collectedInputs map[string]string
	inputFields     []string
	currentField    int

	// Output
	outputLines  []string
	outputScroll int
	outputMutex  sync.Mutex
	autoScroll   bool
	focusOutput  bool

	// Database
	db *DB

	// WordPress Determiner
	checker   *Checker
	wpWorkers int
	wpTimeout time.Duration
	wpTotal   int64
	wpChecked int64
	wpWordpress int64
	wpNotWP   int64
	wpErrors  int64

	// Hash Hunter
	hunter    *Hunter
	hhWorkers int
	hhTimeout time.Duration

	// Hash Cracker
	hcHashes       []HashRecord
	hcCrackedMap   map[string]string
	hcProviders    []string
	hcCustomWords  []string
	hcFirstNames   map[string][]string // key = first letter, value = list of names
	hcTotalHashes  int64
	hcTestedEmails int64
	hcCrackedCount int64
	hcCurrentHash  string

	// Pipeline
	pipelinePhase    string // current phase: "detect", "harvest", "crack", "complete"
	pipelineTotal    int64
	pipelineProgress int64

	// Huntr (Module 5)
	huntrWorkers     int
	huntrTimeout     time.Duration
	huntrTotal       int64
	huntrChecked     int64
	huntrFindings    int64
	huntrCurrentDomain string

	// ODINT Toolkit (Module 6)
	odintWorkers       int
	odintTimeout       time.Duration
	odintTotal         int64
	odintProgress      int64
	odintCurrentDomain string
	odintModulesRun    string
	odintDownloadTotal int64
	odintDownloadDone  int64
	odintCurrentModule string

	// Port Scanner (Module 7)
	portWorkers      int
	portTimeout      time.Duration
	portTotal        int64
	portScanned      int64
	portOpen         int64
	portClosed       int64
	portFiltered     int64
	portCurrentTarget string

	// Silent Eye (Module 8)
	silentTotal      int64
	silentProgress   int64
	silentCurrentTarget string
	silentVendorsRun string

	// Terminator Mode (Module 9)
	terminatorPhase string

	// Settings
	selectedSetting int

	// File picker
	pickerFiles    []string
	pickerSelected int

	// Cancellation
	cancelScan    context.CancelFunc
	skipNextEvent bool

	// Logging
	logFile    *os.File
	logMutex   sync.Mutex
	logsDir    string
	listsDir   string
	targetsDir string
	namesDir   string
}

// ===========================================================================
// TUI CONSTRUCTOR
// ===========================================================================

func NewTUI(dbPath string, workers int, timeout time.Duration) (*TUI, error) {
	screen, err := tcell.NewScreen()
	if err != nil {
		return nil, err
	}
	if err := screen.Init(); err != nil {
		return nil, err
	}

	width, height := screen.Size()

	exePath, _ := os.Executable()
	baseDir := filepath.Dir(exePath)
	logsDir := filepath.Join(baseDir, "logs")
	listsDir := filepath.Join(baseDir, "lists")
	targetsDir := filepath.Join(baseDir, "targets")
	// names/ lives inside src/ next to the source code
	namesDir := filepath.Join(baseDir, "names")
	if _, err := os.Stat(namesDir); os.IsNotExist(err) {
		namesDir = filepath.Join(baseDir, "src", "names")
	}
	os.MkdirAll(logsDir, 0755)
	os.MkdirAll(listsDir, 0755)
	os.MkdirAll(targetsDir, 0755)

	db, err := NewDB(dbPath)
	if err != nil {
		screen.Fini()
		return nil, fmt.Errorf("failed to open database: %v", err)
	}

	tui := &TUI{
		screen:          screen,
		width:           width,
		height:          height,
		running:         true,
		showSplash:      true,
		splashFrame:     0,
		selectedItem:    0,
		autoScroll:      true,
		commandState:    StateMenu,
		collectedInputs: make(map[string]string),
		outputLines:     []string{},
		activeModule:    ModuleWPDeterminer,
		showModuleSelect: false,
		moduleSelected:  0,
		menuItems:       wpMenuItems,
		checker:         NewChecker(timeout),
		hunter:          NewHunter(timeout),
		db:              db,
		wpWorkers:       workers,
		wpTimeout:       timeout,
		hhWorkers:       workers,
		hhTimeout:       timeout,
		hcProviders:     defaultProviders,
		hcCrackedMap:    make(map[string]string),
		hcFirstNames:   make(map[string][]string),
		huntrWorkers:   workers,
		huntrTimeout:   timeout,
		odintWorkers:   workers,
		odintTimeout:   timeout,
		portWorkers:    workers,
		portTimeout:    timeout,
		logsDir:         logsDir,
		listsDir:        listsDir,
		targetsDir:      targetsDir,
		namesDir:        namesDir,
	}

	// Load name lists for OSINT-driven pattern expansion
	tui.loadNameLists()

	screen.SetStyle(tcell.StyleDefault.Background(ColorBackground).Foreground(ColorText))
	screen.Clear()
	return tui, nil
}

func (tui *TUI) Close() {
	tui.closeLog()
	tui.db.Close()
	tui.screen.Fini()
}

// ===========================================================================
// LOGGING
// ===========================================================================

func (tui *TUI) startLog(scanType string) {
	tui.logMutex.Lock()
	defer tui.logMutex.Unlock()

	timestamp := time.Now().Format("2006-01-02_150405")
	modName := strings.ReplaceAll(strings.ToLower(moduleNames[tui.activeModule]), " ", "-")
	filename := filepath.Join(tui.logsDir, fmt.Sprintf("%s_%s_%s.log", timestamp, modName, scanType))

	f, err := os.Create(filename)
	if err != nil {
		return
	}
	tui.logFile = f

	tui.writeLogRaw("===================================================================")
	tui.writeLogRaw(fmt.Sprintf(" MOTHERFUCKIN TERMINATOR BOT 9000 v%s - %s - Scan Log", Version, moduleNames[tui.activeModule]))
	tui.writeLogRaw(fmt.Sprintf(" Started: %s", time.Now().Format("2006-01-02 15:04:05")))
	tui.writeLogRaw(fmt.Sprintf(" Type: %s", scanType))
	tui.writeLogRaw("===================================================================")
	tui.writeLogRaw("")
}

func (tui *TUI) writeLog(format string, args ...interface{}) {
	tui.logMutex.Lock()
	defer tui.logMutex.Unlock()

	if tui.logFile == nil {
		return
	}

	timestamp := time.Now().Format("15:04:05")
	msg := fmt.Sprintf(format, args...)
	tui.logFile.WriteString(fmt.Sprintf("[%s] %s\n", timestamp, msg))
}

func (tui *TUI) writeLogRaw(msg string) {
	if tui.logFile == nil {
		return
	}
	tui.logFile.WriteString(msg + "\n")
}

func (tui *TUI) closeLog() {
	tui.logMutex.Lock()
	defer tui.logMutex.Unlock()

	if tui.logFile != nil {
		tui.writeLogRaw("")
		tui.writeLogRaw("===================================================================")
		tui.writeLogRaw(fmt.Sprintf(" Finished: %s", time.Now().Format("2006-01-02 15:04:05")))
		tui.writeLogRaw("===================================================================")
		tui.logFile.Close()
		tui.logFile = nil
	}
}

// ===========================================================================
// DRAWING HELPERS
// ===========================================================================

func (tui *TUI) drawString(x, y int, text string, style tcell.Style) {
	for i, ch := range text {
		if x+i >= 0 && x+i < tui.width && y >= 0 && y < tui.height {
			tui.screen.SetContent(x+i, y, ch, nil, style)
		}
	}
}

func (tui *TUI) drawStringClipped(x, y, maxWidth int, text string, style tcell.Style) {
	runes := []rune(text)
	for i, ch := range runes {
		if i >= maxWidth {
			break
		}
		if x+i >= 0 && x+i < tui.width && y >= 0 && y < tui.height {
			tui.screen.SetContent(x+i, y, ch, nil, style)
		}
	}
}

func (tui *TUI) drawBox(x, y, width, height int, title string, style tcell.Style) {
	if width < 2 || height < 2 {
		return
	}
	tui.screen.SetContent(x, y, '\u250C', nil, style)
	tui.screen.SetContent(x+width-1, y, '\u2510', nil, style)
	tui.screen.SetContent(x, y+height-1, '\u2514', nil, style)
	tui.screen.SetContent(x+width-1, y+height-1, '\u2518', nil, style)

	for i := 1; i < width-1; i++ {
		tui.screen.SetContent(x+i, y, '\u2500', nil, style)
		tui.screen.SetContent(x+i, y+height-1, '\u2500', nil, style)
	}
	for i := 1; i < height-1; i++ {
		tui.screen.SetContent(x, y+i, '\u2502', nil, style)
		tui.screen.SetContent(x+width-1, y+i, '\u2502', nil, style)
	}

	if title != "" {
		titleStr := " " + title + " "
		titleRunes := []rune(titleStr)
		titleX := x + (width-len(titleRunes))/2
		for i, ch := range titleRunes {
			if titleX+i >= 0 && titleX+i < tui.width && y >= 0 && y < tui.height {
				tui.screen.SetContent(titleX+i, y, ch, nil, style.Bold(true))
			}
		}
	}
}

func (tui *TUI) fillRect(x, y, width, height int, ch rune, style tcell.Style) {
	for row := y; row < y+height; row++ {
		for col := x; col < x+width; col++ {
			if col >= 0 && col < tui.width && row >= 0 && row < tui.height {
				tui.screen.SetContent(col, row, ch, nil, style)
			}
		}
	}
}

func (tui *TUI) centerX(text string) int {
	return (tui.width - len([]rune(text))) / 2
}

func (tui *TUI) wrapText(text string, maxWidth int) []string {
	if maxWidth <= 0 {
		maxWidth = 60
	}
	runes := []rune(text)
	if len(runes) <= maxWidth {
		return []string{text}
	}

	var lines []string
	for len(runes) > maxWidth {
		breakAt := maxWidth
		for i := maxWidth; i > maxWidth/2; i-- {
			if i < len(runes) && runes[i] == ' ' {
				breakAt = i
				break
			}
		}
		lines = append(lines, string(runes[:breakAt]))
		runes = append([]rune("    "), runes[breakAt:]...)
		// trim leading space after break
		for len(runes) > 4 && runes[4] == ' ' {
			runes = append(runes[:4], runes[5:]...)
		}
	}
	if len(runes) > 0 {
		lines = append(lines, string(runes))
	}
	return lines
}

func (tui *TUI) addOutput(line string) {
	tui.outputMutex.Lock()
	defer tui.outputMutex.Unlock()

	menuWidth := 52
	outputWidth := tui.width - menuWidth - 4
	if outputWidth < 40 {
		outputWidth = 40
	}

	wrappedLines := tui.wrapText(line, outputWidth)
	tui.outputLines = append(tui.outputLines, wrappedLines...)

	if tui.autoScroll {
		maxVisible := tui.height - 8
		if len(tui.outputLines) > maxVisible {
			tui.outputScroll = len(tui.outputLines) - maxVisible
		}
	}
}

func (tui *TUI) clearOutput() {
	tui.outputMutex.Lock()
	defer tui.outputMutex.Unlock()
	tui.outputLines = []string{}
	tui.outputScroll = 0
}

func (tui *TUI) scrollOutput(delta int) {
	tui.outputMutex.Lock()
	defer tui.outputMutex.Unlock()
	maxScroll := len(tui.outputLines) - (tui.height - 10)
	if maxScroll < 0 {
		maxScroll = 0
	}
	tui.outputScroll += delta
	if tui.outputScroll < 0 {
		tui.outputScroll = 0
	}
	if tui.outputScroll > maxScroll {
		tui.outputScroll = maxScroll
	}
	if tui.outputScroll >= maxScroll {
		tui.autoScroll = true
	} else {
		tui.autoScroll = false
	}
}

func (tui *TUI) scrollOutputTo(pos string) {
	tui.outputMutex.Lock()
	defer tui.outputMutex.Unlock()
	maxScroll := len(tui.outputLines) - (tui.height - 10)
	if maxScroll < 0 {
		maxScroll = 0
	}
	if pos == "top" {
		tui.outputScroll = 0
		tui.autoScroll = false
	} else {
		tui.outputScroll = maxScroll
		tui.autoScroll = true
	}
}

// ===========================================================================
// SPLASH SCREEN
// ===========================================================================

func (tui *TUI) renderSplashScreen() {
	tui.screen.Clear()
	logoHeight := len(terminatorLogo)
	startY := (tui.height - logoHeight - 6) / 2
	if startY < 1 {
		startY = 1
	}

	logoStyle := tcell.StyleDefault.Foreground(ColorLogo).Bold(true)

	totalChars := 0
	for _, line := range terminatorLogo {
		totalChars += len([]rune(line))
	}

	// Phase 0: delay (0.0 → 0.3 = 1.5 seconds of blank screen)
	// Phase 1: trickle in (0.3 → 1.3 = 5 seconds of logo reveal)
	// Phase 2: tagline (1.3 → 2.5 = tagline + hold)
	trickleProgress := 0.0
	if tui.splashFrame > 0.3 {
		trickleProgress = (tui.splashFrame - 0.3) / 1.0
		if trickleProgress > 1.0 {
			trickleProgress = 1.0
		}
	}

	visibleChars := int(trickleProgress * float64(totalChars))
	charCount := 0

	for i, line := range terminatorLogo {
		y := startY + i
		runes := []rune(line)
		x := (tui.width - len(runes)) / 2
		if x < 1 {
			x = 1
		}

		for j, ch := range runes {
			charCount++
			if charCount <= visibleChars && x+j < tui.width && y < tui.height {
				tui.screen.SetContent(x+j, y, ch, nil, logoStyle)
			}
		}
	}

	if tui.splashFrame > 1.3 {
		infoY := startY + logoHeight + 2
		tagline := "WordPress + Hash OSINT Pipeline"
		author := "by Ringmast4r"
		version := "v" + Version

		tui.drawString(tui.centerX(tagline), infoY, tagline, tcell.StyleDefault.Foreground(ColorAccent))
		tui.drawString(tui.centerX(author), infoY+2, author, tcell.StyleDefault.Foreground(ColorText))
		tui.drawString(tui.centerX(version), infoY+3, version, tcell.StyleDefault.Foreground(ColorWarning))
	}

	if tui.splashFrame < 2.5 {
		tui.splashFrame += 0.01
	}
}

// ===========================================================================
// MODULE SELECTOR OVERLAY
// ===========================================================================

func (tui *TUI) renderModuleSelectOverlay() {
	boxWidth := 38
	boxHeight := 16
	boxX := (tui.width - boxWidth) / 2
	boxY := (tui.height - boxHeight) / 2

	borderStyle := tcell.StyleDefault.Foreground(ColorPrimary)
	bgStyle := tcell.StyleDefault.Background(tcell.ColorBlack)
	textStyle := tcell.StyleDefault.Foreground(ColorText)
	dimStyle := tcell.StyleDefault.Foreground(ColorDim)

	tui.fillRect(boxX-1, boxY-1, boxWidth+2, boxHeight+2, ' ', bgStyle)
	tui.drawBox(boxX, boxY, boxWidth, boxHeight, "SELECT MODULE", borderStyle)

	modules := []struct {
		mod  ActiveModule
		name string
	}{
		{ModuleWPDeterminer, "WordPress Determiner"},
		{ModuleHashHunter, "Hash Hunter"},
		{ModuleHashCracker, "Hash Cracker"},
		{ModulePipeline, "Pipeline"},
		{ModuleHuntr, "Huntr"},
		{ModuleODINT, "ODINT Toolkit"},
		{ModulePortScanner, "Port Scanner"},
		{ModuleSilentEye, "Silent Eye"},
		{ModuleTerminator, "TERMINATOR MODE"},
	}

	for i, m := range modules {
		y := boxY + 2 + i
		modColor := moduleColors[m.mod]
		keyStr := fmt.Sprintf("[%d]", i+1)
		nameStr := " " + m.name

		if i == tui.moduleSelected {
			highlightBg := tcell.StyleDefault.Background(ColorBorder)
			tui.fillRect(boxX+1, y, boxWidth-2, 1, ' ', highlightBg)
			tui.drawString(boxX+2, y, keyStr, tcell.StyleDefault.Foreground(modColor).Bold(true).Background(ColorBorder))
			tui.drawString(boxX+2+len(keyStr), y, nameStr, tcell.StyleDefault.Foreground(ColorText).Bold(true).Background(ColorBorder))
		} else {
			tui.drawString(boxX+2, y, keyStr, tcell.StyleDefault.Foreground(modColor))
			tui.drawString(boxX+2+len(keyStr), y, nameStr, textStyle)
		}
	}

	helpY := boxY + boxHeight - 2
	tui.drawStringClipped(boxX+2, helpY, boxWidth-4, "Tab: Switch  Enter: Select", dimStyle)
}

// ===========================================================================
// DASHBOARD RENDERERS
// ===========================================================================

func (tui *TUI) getActiveMenuItems() []MenuItem {
	switch tui.activeModule {
	case ModuleWPDeterminer:
		return wpMenuItems
	case ModuleHashHunter:
		return hhMenuItems
	case ModuleHashCracker:
		return hcMenuItems
	case ModulePipeline:
		return pipelineMenuItems
	case ModuleHuntr:
		return huntrMenuItems
	case ModuleODINT:
		return odintMenuItems
	case ModulePortScanner:
		return portMenuItems
	case ModuleSilentEye:
		return silentMenuItems
	case ModuleTerminator:
		return terminatorMenuItems
	}
	return wpMenuItems
}

func (tui *TUI) renderWPDeterminerDashboard() {
	modColor := moduleColors[ModuleWPDeterminer]
	borderStyle := tcell.StyleDefault.Foreground(modColor)
	titleStyle := tcell.StyleDefault.Foreground(ColorPrimary).Bold(true)
	textStyle := tcell.StyleDefault.Foreground(ColorText)
	highlightStyle := tcell.StyleDefault.Foreground(modColor)
	successStyle := tcell.StyleDefault.Foreground(ColorSuccess)
	dimStyle := tcell.StyleDefault.Foreground(ColorDim)

	menuWidth := 52
	outputX := menuWidth + 1
	outputWidth := tui.width - menuWidth - 2
	inputHeight := 3
	outputHeight := tui.height - inputHeight - 2

	// Header
	header := fmt.Sprintf(" MOTHERFUCKIN TERMINATOR BOT 9000 v%s | MODULE: WordPress Determiner ", Version)
	tui.drawString(1, 0, header, titleStyle)

	dbTotal, dbWP, dbNotWP, dbErr := tui.db.GetSiteStats()
	statsStr := fmt.Sprintf("DB: %d sites | %d WP | %d Not WP | %d Err ", dbTotal, dbWP, dbNotWP, dbErr)
	tui.drawString(tui.width-len(statsStr)-1, 0, statsStr, dimStyle)

	checkedVal := atomic.LoadInt64(&tui.wpChecked)
	wpVal := atomic.LoadInt64(&tui.wpWordpress)
	notWPVal := atomic.LoadInt64(&tui.wpNotWP)
	errVal := atomic.LoadInt64(&tui.wpErrors)

	// Menu box
	tui.drawBox(0, 1, menuWidth, tui.height-inputHeight-1, "COMMANDS", borderStyle)

	menuY := 3
	items := tui.menuItems
	for i, item := range items {
		y := menuY + i
		if y >= tui.height-inputHeight-2 {
			break
		}

		keyStyle := highlightStyle
		nameStyle := textStyle
		if i == tui.selectedItem && !tui.focusOutput {
			tui.fillRect(1, y, menuWidth-2, 1, ' ', tcell.StyleDefault.Background(ColorBorder))
			keyStyle = successStyle.Bold(true).Background(ColorBorder)
			nameStyle = tcell.StyleDefault.Foreground(ColorText).Bold(true).Background(ColorBorder)
		}

		tui.drawString(2, y, fmt.Sprintf("[%s]", item.Key), keyStyle)
		tui.drawStringClipped(6, y, menuWidth-8, item.Name+" - "+item.Desc, nameStyle)
	}

	// Detection info
	modeY := menuY + len(items) + 1
	tui.drawString(2, modeY, "--- 10 Detection Methods ---", dimStyle)
	methods := []string{
		" 1. /wp-login.php",
		" 2. /wp-json/",
		" 3. Homepage HTML signals",
		" 4. /xmlrpc.php",
		" 5. /wp-cron.php",
		" 6. /wp-links-opml.php",
		" 7. /wp-sitemap.xml",
		" 8. /feed/ (RSS)",
		" 9. /wp-admin/ redirect",
		"10. /wp-includes/ dir",
	}
	for i, m := range methods {
		if modeY+1+i < tui.height-inputHeight-2 {
			tui.drawString(2, modeY+1+i, m, tcell.StyleDefault.Foreground(ColorInfo))
		}
	}

	shortcutY := modeY + len(methods) + 2
	if shortcutY < tui.height-inputHeight-2 {
		tui.drawString(2, shortcutY, "--- Shortcuts ---", dimStyle)
		if shortcutY+1 < tui.height-inputHeight-2 {
			tui.drawString(2, shortcutY+1, "F1: Settings  Tab: Module Switch", tcell.StyleDefault.Foreground(ColorInfo))
		}
		if shortcutY+2 < tui.height-inputHeight-2 {
			tui.drawString(2, shortcutY+2, "ESC: Cancel / Back", tcell.StyleDefault.Foreground(ColorInfo))
		}
		if shortcutY+3 < tui.height-inputHeight-2 {
			tui.drawString(2, shortcutY+3, "PgUp/PgDn: Scroll", tcell.StyleDefault.Foreground(ColorInfo))
		}
	}

	// Output box
	outputTitle := "OUTPUT"
	outputBorder := borderStyle
	if tui.focusOutput {
		outputTitle = "OUTPUT [FOCUSED]"
		outputBorder = tcell.StyleDefault.Foreground(ColorAccent)
	}
	tui.drawBox(outputX, 1, outputWidth, outputHeight, outputTitle, outputBorder)

	tui.outputMutex.Lock()
	maxLines := outputHeight - 2
	startLine := tui.outputScroll
	for i := 0; i < maxLines && startLine+i < len(tui.outputLines); i++ {
		line := tui.outputLines[startLine+i]
		y := 2 + i

		lineStyle := tcell.StyleDefault.Foreground(tcell.ColorWhite)
		if strings.HasPrefix(line, "[WP]") || strings.HasPrefix(line, "[+]") {
			lineStyle = tcell.StyleDefault.Foreground(ColorSuccess)
		} else if strings.HasPrefix(line, "[NOT]") {
			lineStyle = tcell.StyleDefault.Foreground(ColorDanger)
		} else if strings.HasPrefix(line, "[ERR]") || strings.HasPrefix(line, "[-]") {
			lineStyle = tcell.StyleDefault.Foreground(ColorWarning)
		} else if strings.HasPrefix(line, "[*]") {
			lineStyle = tcell.StyleDefault.Foreground(ColorInfo)
		} else if strings.HasPrefix(line, "[!]") {
			lineStyle = tcell.StyleDefault.Foreground(ColorSecondary)
		}

		tui.drawStringClipped(outputX+1, y, outputWidth-2, line, lineStyle)
	}

	if len(tui.outputLines) > maxLines {
		scrollInfo := fmt.Sprintf(" %d/%d ", tui.outputScroll+maxLines, len(tui.outputLines))
		tui.drawString(outputX+outputWidth-len(scrollInfo)-1, 1, scrollInfo, dimStyle)
	}
	tui.outputMutex.Unlock()

	// Input box
	inputY := tui.height - inputHeight - 1
	tui.drawBox(0, inputY, tui.width, inputHeight+1, "INPUT", borderStyle)

	promptStyle := tcell.StyleDefault.Foreground(ColorWarning)
	inputStyle := tcell.StyleDefault.Foreground(ColorText)

	if tui.commandState == StateInput {
		prompt := tui.inputPrompt + ": "
		tui.drawString(2, inputY+1, prompt, promptStyle)
		tui.drawString(2+len(prompt), inputY+1, tui.inputBuffer, inputStyle)
		cursorX := 2 + len(prompt) + tui.inputCursor
		if cursorX < tui.width-2 {
			tui.screen.SetContent(cursorX, inputY+1, '\u258C', nil, tcell.StyleDefault.Foreground(ColorPrimary))
		}
	} else if tui.commandState == StateRunning {
		tui.drawString(2, inputY+1, "Checking... press ESC to cancel", tcell.StyleDefault.Foreground(ColorWarning))
	} else if tui.commandState == StateComplete {
		tui.drawString(2, inputY+1, "Complete! Press any key to continue", successStyle)
	} else {
		hint := "Tab: Module | Arrows: Navigate | Enter: Select | Q: Quit"
		tui.drawStringClipped(2, inputY+1, tui.width-4, hint, dimStyle)
	}

	// Status bar
	statusY := tui.height - 1
	statusColor := modColor
	statusStyle := tcell.StyleDefault.Foreground(tcell.ColorBlack).Background(statusColor)
	tui.fillRect(0, statusY, tui.width, 1, ' ', statusStyle)

	totalVal := atomic.LoadInt64(&tui.wpTotal)
	totalHashes := tui.db.GetTotalHashes()
	status := fmt.Sprintf(" Total: %d | Checked: %d | WordPress: %d | Not WP: %d | Errors: %d | Hashes: %d ",
		totalVal, checkedVal, wpVal, notWPVal, errVal, totalHashes)
	tui.drawString(0, statusY, status, statusStyle)
}

func (tui *TUI) renderPlaceholderDashboard(modName string, placeholderMsg string) {
	modColor := moduleColors[tui.activeModule]
	borderStyle := tcell.StyleDefault.Foreground(modColor)
	titleStyle := tcell.StyleDefault.Foreground(ColorPrimary).Bold(true)
	textStyle := tcell.StyleDefault.Foreground(ColorText)
	highlightStyle := tcell.StyleDefault.Foreground(modColor)
	successStyle := tcell.StyleDefault.Foreground(ColorSuccess)
	dimStyle := tcell.StyleDefault.Foreground(ColorDim)

	menuWidth := 52
	outputX := menuWidth + 1
	outputWidth := tui.width - menuWidth - 2
	inputHeight := 3
	outputHeight := tui.height - inputHeight - 2

	// Header
	header := fmt.Sprintf(" MOTHERFUCKIN TERMINATOR BOT 9000 v%s | MODULE: %s ", Version, modName)
	tui.drawString(1, 0, header, titleStyle)

	// Menu box
	tui.drawBox(0, 1, menuWidth, tui.height-inputHeight-1, "COMMANDS", borderStyle)

	menuY := 3
	items := tui.menuItems
	for i, item := range items {
		y := menuY + i
		if y >= tui.height-inputHeight-2 {
			break
		}

		keyStyle := highlightStyle
		nameStyle := textStyle
		if i == tui.selectedItem && !tui.focusOutput {
			tui.fillRect(1, y, menuWidth-2, 1, ' ', tcell.StyleDefault.Background(ColorBorder))
			keyStyle = successStyle.Bold(true).Background(ColorBorder)
			nameStyle = tcell.StyleDefault.Foreground(ColorText).Bold(true).Background(ColorBorder)
		}

		tui.drawString(2, y, fmt.Sprintf("[%s]", item.Key), keyStyle)
		tui.drawStringClipped(6, y, menuWidth-8, item.Name+" - "+item.Desc, nameStyle)
	}

	// Shortcuts
	shortcutY := menuY + len(items) + 2
	if shortcutY < tui.height-inputHeight-2 {
		tui.drawString(2, shortcutY, "--- Shortcuts ---", dimStyle)
		if shortcutY+1 < tui.height-inputHeight-2 {
			tui.drawString(2, shortcutY+1, "F1: Settings  Tab: Module Switch", tcell.StyleDefault.Foreground(ColorInfo))
		}
		if shortcutY+2 < tui.height-inputHeight-2 {
			tui.drawString(2, shortcutY+2, "ESC: Cancel / Back", tcell.StyleDefault.Foreground(ColorInfo))
		}
		if shortcutY+3 < tui.height-inputHeight-2 {
			tui.drawString(2, shortcutY+3, "PgUp/PgDn: Scroll", tcell.StyleDefault.Foreground(ColorInfo))
		}
	}

	// Output box
	outputTitle := "OUTPUT"
	outputBorder := borderStyle
	if tui.focusOutput {
		outputTitle = "OUTPUT [FOCUSED]"
		outputBorder = tcell.StyleDefault.Foreground(ColorAccent)
	}
	tui.drawBox(outputX, 1, outputWidth, outputHeight, outputTitle, outputBorder)

	tui.outputMutex.Lock()
	maxLines := outputHeight - 2

	// If output is empty, show placeholder
	if len(tui.outputLines) == 0 {
		tui.drawStringClipped(outputX+2, 3, outputWidth-4, placeholderMsg, tcell.StyleDefault.Foreground(ColorInfo))
	} else {
		startLine := tui.outputScroll
		for i := 0; i < maxLines && startLine+i < len(tui.outputLines); i++ {
			line := tui.outputLines[startLine+i]
			y := 2 + i

			lineStyle := tcell.StyleDefault.Foreground(tcell.ColorWhite)
			if strings.HasPrefix(line, "[+]") || strings.HasPrefix(line, "[WP]") {
				lineStyle = tcell.StyleDefault.Foreground(ColorSuccess)
			} else if strings.HasPrefix(line, "[-]") || strings.HasPrefix(line, "[NOT]") {
				lineStyle = tcell.StyleDefault.Foreground(ColorDanger)
			} else if strings.HasPrefix(line, "[ERR]") {
				lineStyle = tcell.StyleDefault.Foreground(ColorWarning)
			} else if strings.HasPrefix(line, "[*]") {
				lineStyle = tcell.StyleDefault.Foreground(ColorInfo)
			} else if strings.HasPrefix(line, "[!]") {
				lineStyle = tcell.StyleDefault.Foreground(ColorSecondary)
			}

			tui.drawStringClipped(outputX+1, y, outputWidth-2, line, lineStyle)
		}

		if len(tui.outputLines) > maxLines {
			scrollInfo := fmt.Sprintf(" %d/%d ", tui.outputScroll+maxLines, len(tui.outputLines))
			tui.drawString(outputX+outputWidth-len(scrollInfo)-1, 1, scrollInfo, dimStyle)
		}
	}
	tui.outputMutex.Unlock()

	// Input box
	inputY := tui.height - inputHeight - 1
	tui.drawBox(0, inputY, tui.width, inputHeight+1, "INPUT", borderStyle)

	if tui.commandState == StateInput {
		promptStyle := tcell.StyleDefault.Foreground(ColorWarning)
		inputStyle := tcell.StyleDefault.Foreground(ColorText)
		prompt := tui.inputPrompt + ": "
		tui.drawString(2, inputY+1, prompt, promptStyle)
		tui.drawString(2+len(prompt), inputY+1, tui.inputBuffer, inputStyle)
		cursorX := 2 + len(prompt) + tui.inputCursor
		if cursorX < tui.width-2 {
			tui.screen.SetContent(cursorX, inputY+1, '\u258C', nil, tcell.StyleDefault.Foreground(ColorPrimary))
		}
	} else if tui.commandState == StateRunning {
		tui.drawString(2, inputY+1, "Running... press ESC to cancel", tcell.StyleDefault.Foreground(ColorWarning))
	} else if tui.commandState == StateComplete {
		tui.drawString(2, inputY+1, "Complete! Press any key to continue", successStyle)
	} else {
		hint := "Tab: Module | Arrows: Navigate | Enter: Select | Q: Quit"
		tui.drawStringClipped(2, inputY+1, tui.width-4, hint, dimStyle)
	}

	// Status bar
	statusY := tui.height - 1
	statusStyle := tcell.StyleDefault.Foreground(tcell.ColorBlack).Background(modColor)
	tui.fillRect(0, statusY, tui.width, 1, ' ', statusStyle)
	totalHashes := tui.db.GetTotalHashes()
	_, totalCracked := tui.db.GetCrackedStats()
	totalDomains := tui.db.GetTotalDomains()
	statusText := fmt.Sprintf(" MOTHERFUCKIN TERMINATOR BOT 9000 | %s | Domains: %d | Hashes: %d | Cracked: %d ", modName, totalDomains, totalHashes, totalCracked)
	tui.drawString(0, statusY, statusText, statusStyle)
}

func (tui *TUI) renderHashHunterDashboard() {
	tui.renderPlaceholderDashboard("Hash Hunter", "[*] Hash Hunter ready -- Select a command to begin")
}

func (tui *TUI) renderHashCrackerDashboard() {
	tui.renderPlaceholderDashboard("Hash Cracker", "[*] Hash Cracker ready -- Load hashes, then start cracking")
}

func (tui *TUI) renderPipelineDashboard() {
	modColor := moduleColors[ModulePipeline]
	borderStyle := tcell.StyleDefault.Foreground(modColor)
	titleStyle := tcell.StyleDefault.Foreground(ColorPrimary).Bold(true)
	textStyle := tcell.StyleDefault.Foreground(ColorText)
	highlightStyle := tcell.StyleDefault.Foreground(modColor)
	successStyle := tcell.StyleDefault.Foreground(ColorSuccess)
	dimStyle := tcell.StyleDefault.Foreground(ColorDim)

	menuWidth := 52
	outputX := menuWidth + 1
	outputWidth := tui.width - menuWidth - 2
	inputHeight := 3
	outputHeight := tui.height - inputHeight - 2

	// Header
	header := fmt.Sprintf(" MOTHERFUCKIN TERMINATOR BOT 9000 v%s | MODULE: Pipeline ", Version)
	tui.drawString(1, 0, header, titleStyle)

	// Cross-module stats in header
	totalSites, wpSites, _, _ := tui.db.GetSiteStats()
	totalHashes := tui.db.GetTotalHashes()
	_, totalCracked := tui.db.GetCrackedStats()
	statsStr := fmt.Sprintf("Sites: %d | WP: %d | Hashes: %d | Cracked: %d ", totalSites, wpSites, totalHashes, totalCracked)
	tui.drawString(tui.width-len(statsStr)-1, 0, statsStr, dimStyle)

	// Menu box
	tui.drawBox(0, 1, menuWidth, tui.height-inputHeight-1, "PIPELINE COMMANDS", borderStyle)

	menuY := 3
	items := tui.menuItems
	for i, item := range items {
		y := menuY + i
		if y >= tui.height-inputHeight-2 {
			break
		}
		keyStyle := highlightStyle
		nameStyle := textStyle
		if i == tui.selectedItem && !tui.focusOutput {
			tui.fillRect(1, y, menuWidth-2, 1, ' ', tcell.StyleDefault.Background(ColorBorder))
			keyStyle = successStyle.Bold(true).Background(ColorBorder)
			nameStyle = tcell.StyleDefault.Foreground(ColorText).Bold(true).Background(ColorBorder)
		}
		tui.drawString(2, y, fmt.Sprintf("[%s]", item.Key), keyStyle)
		tui.drawStringClipped(6, y, menuWidth-8, item.Name+" - "+item.Desc, nameStyle)
	}

	// Cross-module intelligence panel
	intelY := menuY + len(items) + 1
	tui.drawString(2, intelY, "--- Cross-Module Intel ---", dimStyle)

	wpNotScanned := tui.db.GetWPSitesNotScanned()
	uncracked := tui.db.GetUncracked()

	infoStyle := tcell.StyleDefault.Foreground(ColorInfo)
	warnStyle := tcell.StyleDefault.Foreground(ColorWarning)

	if intelY+1 < tui.height-inputHeight-2 {
		tui.drawString(2, intelY+1, fmt.Sprintf("WP sites not scanned: %d", len(wpNotScanned)), infoStyle)
	}
	if intelY+2 < tui.height-inputHeight-2 {
		tui.drawString(2, intelY+2, fmt.Sprintf("Uncracked hashes: %d", len(uncracked)), infoStyle)
	}
	if intelY+4 < tui.height-inputHeight-2 {
		tui.drawString(2, intelY+4, "--- Pipeline Flow ---", dimStyle)
	}
	if intelY+5 < tui.height-inputHeight-2 {
		tui.drawString(2, intelY+5, "Detect WP -> Harvest -> Crack", warnStyle)
	}

	shortcutY := intelY + 7
	if shortcutY < tui.height-inputHeight-2 {
		tui.drawString(2, shortcutY, "--- Shortcuts ---", dimStyle)
		if shortcutY+1 < tui.height-inputHeight-2 {
			tui.drawString(2, shortcutY+1, "F1: Settings  Tab: Module Switch", infoStyle)
		}
		if shortcutY+2 < tui.height-inputHeight-2 {
			tui.drawString(2, shortcutY+2, "ESC: Cancel / Back", infoStyle)
		}
	}

	// Output box
	outputTitle := "OUTPUT"
	outputBorder := borderStyle
	if tui.focusOutput {
		outputTitle = "OUTPUT [FOCUSED]"
		outputBorder = tcell.StyleDefault.Foreground(ColorAccent)
	}
	tui.drawBox(outputX, 1, outputWidth, outputHeight, outputTitle, outputBorder)

	tui.outputMutex.Lock()
	maxLines := outputHeight - 2
	if len(tui.outputLines) == 0 {
		tui.drawStringClipped(outputX+2, 3, outputWidth-4, "[*] Pipeline ready -- chain modules together", tcell.StyleDefault.Foreground(ColorInfo))
	} else {
		startLine := tui.outputScroll
		for i := 0; i < maxLines && startLine+i < len(tui.outputLines); i++ {
			line := tui.outputLines[startLine+i]
			y := 2 + i
			lineStyle := tcell.StyleDefault.Foreground(tcell.ColorWhite)
			if strings.HasPrefix(line, "[+]") || strings.HasPrefix(line, "[WP]") || strings.HasPrefix(line, "[CRACKED]") {
				lineStyle = tcell.StyleDefault.Foreground(ColorSuccess)
			} else if strings.HasPrefix(line, "[-]") || strings.HasPrefix(line, "[NOT]") {
				lineStyle = tcell.StyleDefault.Foreground(ColorDanger)
			} else if strings.HasPrefix(line, "[ERR]") {
				lineStyle = tcell.StyleDefault.Foreground(ColorWarning)
			} else if strings.HasPrefix(line, "[*]") || strings.HasPrefix(line, "[PHASE") {
				lineStyle = tcell.StyleDefault.Foreground(ColorInfo)
			} else if strings.HasPrefix(line, "[!]") || strings.HasPrefix(line, "[PIPELINE]") {
				lineStyle = tcell.StyleDefault.Foreground(ColorSecondary)
			}
			tui.drawStringClipped(outputX+1, y, outputWidth-2, line, lineStyle)
		}
		if len(tui.outputLines) > maxLines {
			scrollInfo := fmt.Sprintf(" %d/%d ", tui.outputScroll+maxLines, len(tui.outputLines))
			tui.drawString(outputX+outputWidth-len(scrollInfo)-1, 1, scrollInfo, dimStyle)
		}
	}
	tui.outputMutex.Unlock()

	// Input box
	inputY := tui.height - inputHeight - 1
	tui.drawBox(0, inputY, tui.width, inputHeight+1, "INPUT", borderStyle)
	if tui.commandState == StateInput {
		promptStyle := tcell.StyleDefault.Foreground(ColorWarning)
		inputStyle := tcell.StyleDefault.Foreground(ColorText)
		prompt := tui.inputPrompt + ": "
		tui.drawString(2, inputY+1, prompt, promptStyle)
		tui.drawString(2+len(prompt), inputY+1, tui.inputBuffer, inputStyle)
		cursorX := 2 + len(prompt) + tui.inputCursor
		if cursorX < tui.width-2 {
			tui.screen.SetContent(cursorX, inputY+1, '\u258C', nil, tcell.StyleDefault.Foreground(ColorPrimary))
		}
	} else if tui.commandState == StateRunning {
		phase := tui.pipelinePhase
		if phase == "" {
			phase = "Running"
		}
		tui.drawString(2, inputY+1, fmt.Sprintf("[%s] Running... press ESC to cancel", phase), tcell.StyleDefault.Foreground(ColorWarning))
	} else if tui.commandState == StateComplete {
		tui.drawString(2, inputY+1, "Pipeline complete! Press any key to continue", successStyle)
	} else {
		hint := "Tab: Module | Arrows: Navigate | Enter: Select | Q: Quit"
		tui.drawStringClipped(2, inputY+1, tui.width-4, hint, dimStyle)
	}

	// Status bar
	statusY := tui.height - 1
	statusStyle := tcell.StyleDefault.Foreground(tcell.ColorBlack).Background(modColor)
	tui.fillRect(0, statusY, tui.width, 1, ' ', statusStyle)
	phase := tui.pipelinePhase
	if phase == "" {
		phase = "Ready"
	}
	status := fmt.Sprintf(" PIPELINE | Phase: %s | Sites: %d | WP: %d | Hashes: %d | Cracked: %d ",
		phase, totalSites, wpSites, totalHashes, totalCracked)
	tui.drawString(0, statusY, status, statusStyle)
}

// ===========================================================================
// SETTINGS OVERLAY
// ===========================================================================

func (tui *TUI) renderSettingsOverlay() {
	boxWidth := 40
	boxHeight := 9
	boxX := (tui.width - boxWidth) / 2
	boxY := (tui.height - boxHeight) / 2

	borderStyle := tcell.StyleDefault.Foreground(ColorPrimary)
	bgStyle := tcell.StyleDefault.Background(tcell.ColorBlack)
	textStyle := tcell.StyleDefault.Foreground(ColorText)
	highlightStyle := tcell.StyleDefault.Foreground(tcell.ColorBlack).Background(ColorPrimary)
	dimStyle := tcell.StyleDefault.Foreground(ColorDim)

	tui.fillRect(boxX-1, boxY-1, boxWidth+2, boxHeight+2, ' ', bgStyle)
	tui.drawBox(boxX, boxY, boxWidth, boxHeight, "SETTINGS", borderStyle)

	settingsCount := 2
	for i := 0; i < settingsCount; i++ {
		y := boxY + 2 + i
		name := tui.getSettingName(i)
		value := tui.getSettingValue(i)
		line := fmt.Sprintf(" %-18s: %s", name, value)

		if i == tui.selectedSetting {
			tui.fillRect(boxX+1, y, boxWidth-2, 1, ' ', highlightStyle)
			tui.drawStringClipped(boxX+2, y, boxWidth-4, line, highlightStyle)
		} else {
			tui.drawStringClipped(boxX+2, y, boxWidth-4, line, textStyle)
		}
	}

	helpY := boxY + boxHeight - 2
	tui.drawStringClipped(boxX+2, helpY, boxWidth-4, "^v:Nav Enter:Edit ESC:Close", dimStyle)
}

func (tui *TUI) getSettingName(index int) string {
	names := []string{"Workers", "Timeout (sec)"}
	if index < len(names) {
		return names[index]
	}
	return ""
}

func (tui *TUI) getSettingValue(index int) string {
	switch tui.activeModule {
	case ModuleWPDeterminer:
		switch index {
		case 0:
			return strconv.Itoa(tui.wpWorkers)
		case 1:
			return strconv.Itoa(int(tui.wpTimeout.Seconds()))
		}
	case ModuleHashHunter:
		switch index {
		case 0:
			return strconv.Itoa(tui.hhWorkers)
		case 1:
			return strconv.Itoa(int(tui.hhTimeout.Seconds()))
		}
	default:
		switch index {
		case 0:
			return strconv.Itoa(tui.wpWorkers)
		case 1:
			return strconv.Itoa(int(tui.wpTimeout.Seconds()))
		}
	}
	return ""
}

func (tui *TUI) openSettingsMenu() {
	tui.selectedSetting = 0
	tui.commandState = StateSettings
}

func (tui *TUI) editSelectedSetting() {
	tui.currentCmd = "settings"
	fields := []string{"workers_value", "timeout_value"}
	tui.inputFields = []string{fields[tui.selectedSetting]}
	tui.inputPrompt = tui.getSettingName(tui.selectedSetting) + " (current: " + tui.getSettingValue(tui.selectedSetting) + ")"
	tui.currentField = 0
	tui.inputBuffer = ""
	tui.inputCursor = 0
	tui.commandState = StateInput
}

func (tui *TUI) applySettingValue(field, value string) {
	val, err := strconv.Atoi(value)
	if err != nil || val <= 0 {
		tui.addOutput("[-] Invalid value")
		tui.commandState = StateSettings
		return
	}

	switch field {
	case "workers_value":
		switch tui.activeModule {
		case ModuleWPDeterminer:
			tui.wpWorkers = val
		case ModuleHashHunter:
			tui.hhWorkers = val
		default:
			tui.wpWorkers = val
		}
		tui.addOutput(fmt.Sprintf("[+] Workers set to %d", val))
	case "timeout_value":
		dur := time.Duration(val) * time.Second
		switch tui.activeModule {
		case ModuleWPDeterminer:
			tui.wpTimeout = dur
			tui.checker.client.Timeout = dur
		case ModuleHashHunter:
			tui.hhTimeout = dur
			tui.hunter.client.Timeout = dur
		default:
			tui.wpTimeout = dur
			tui.checker.client.Timeout = dur
		}
		tui.addOutput(fmt.Sprintf("[+] Timeout set to %d seconds", val))
	}
	tui.commandState = StateSettings
}

// ===========================================================================
// FILE PICKER
// ===========================================================================

func (tui *TUI) renderFilePickerOverlay() {
	fileCount := len(tui.pickerFiles)
	boxHeight := fileCount + 5
	if boxHeight > tui.height-4 {
		boxHeight = tui.height - 4
	}
	boxWidth := 50
	for _, f := range tui.pickerFiles {
		name := filepath.Base(f)
		if len(name)+6 > boxWidth {
			boxWidth = len(name) + 6
		}
	}
	if boxWidth > tui.width-4 {
		boxWidth = tui.width - 4
	}

	boxX := (tui.width - boxWidth) / 2
	boxY := (tui.height - boxHeight) / 2

	modColor := moduleColors[tui.activeModule]
	borderStyle := tcell.StyleDefault.Foreground(modColor)
	bgStyle := tcell.StyleDefault.Background(tcell.ColorBlack)
	textStyle := tcell.StyleDefault.Foreground(ColorText)
	highlightStyle := tcell.StyleDefault.Foreground(tcell.ColorBlack).Background(modColor)
	dimStyle := tcell.StyleDefault.Foreground(ColorDim)

	tui.fillRect(boxX-1, boxY-1, boxWidth+2, boxHeight+2, ' ', bgStyle)
	tui.drawBox(boxX, boxY, boxWidth, boxHeight, "SELECT FILE", borderStyle)

	maxVisible := boxHeight - 4
	for i := 0; i < maxVisible && i < fileCount; i++ {
		y := boxY + 2 + i
		name := filepath.Base(tui.pickerFiles[i])
		line := fmt.Sprintf(" %s", name)

		if i == tui.pickerSelected {
			tui.fillRect(boxX+1, y, boxWidth-2, 1, ' ', highlightStyle)
			tui.drawStringClipped(boxX+2, y, boxWidth-4, line, highlightStyle)
		} else {
			tui.drawStringClipped(boxX+2, y, boxWidth-4, line, textStyle)
		}
	}

	helpY := boxY + boxHeight - 2
	tui.drawStringClipped(boxX+2, helpY, boxWidth-4, "^v:Nav Enter:Select ESC:Cancel", dimStyle)
}

// ===========================================================================
// RENDER DISPATCHER
// ===========================================================================

func (tui *TUI) Render() {
	tui.width, tui.height = tui.screen.Size()
	tui.screen.Clear()

	if tui.showSplash {
		tui.renderSplashScreen()
	} else {
		switch tui.activeModule {
		case ModuleWPDeterminer:
			tui.renderWPDeterminerDashboard()
		case ModuleHashHunter:
			tui.renderHashHunterDashboard()
		case ModuleHashCracker:
			tui.renderHashCrackerDashboard()
		case ModulePipeline:
			tui.renderPipelineDashboard()
		case ModuleHuntr:
			tui.renderHuntrDashboard()
		case ModuleODINT:
			tui.renderODINTDashboard()
		case ModulePortScanner:
			tui.renderPortScannerDashboard()
		case ModuleSilentEye:
			tui.renderSilentEyeDashboard()
		case ModuleTerminator:
			tui.renderTerminatorDashboard()
		}

		if tui.commandState == StateSettings {
			tui.renderSettingsOverlay()
		}
		if tui.commandState == StateFilePicker {
			tui.renderFilePickerOverlay()
		}
		if tui.showModuleSelect {
			tui.renderModuleSelectOverlay()
		}
	}
	tui.screen.Show()
}

// ===========================================================================
// MODULE SWITCHING
// ===========================================================================

func (tui *TUI) switchToModule(mod ActiveModule) {
	tui.activeModule = mod
	tui.menuItems = tui.getActiveMenuItems()
	tui.selectedItem = 0
	tui.showModuleSelect = false
	tui.commandState = StateMenu
	tui.clearOutput()

	switch mod {
	case ModuleWPDeterminer:
		tui.addOutput("[*] WordPress Determiner module loaded")
		tui.addOutput("[*] 10-method detection with HTTPS/HTTP fallback")
	case ModuleHashHunter:
		tui.addOutput("[*] Hash Hunter module loaded")
		tui.addOutput("[*] 4-method scanning: REST API + Sitemap + Author scrape + Deep scrape")
	case ModuleHashCracker:
		tui.addOutput("[*] Hash Cracker module loaded")
		tui.addOutput(fmt.Sprintf("[*] %d email providers loaded", len(defaultProviders)))
	case ModulePipeline:
		tui.addOutput("[*] Pipeline module loaded")
		tui.addOutput("[*] Chain modules: Detect WP -> Harvest hashes -> Crack emails")
		totalSites, wpSites, _, _ := tui.db.GetSiteStats()
		totalHashes := tui.db.GetTotalHashes()
		_, cracked := tui.db.GetCrackedStats()
		tui.addOutput(fmt.Sprintf("[*] DB: %d sites | %d WP | %d hashes | %d cracked", totalSites, wpSites, totalHashes, cracked))
	case ModuleHuntr:
		tui.addOutput("[*] Huntr module loaded")
		tui.addOutput(fmt.Sprintf("[*] %d credential paths + %d detection patterns", len(huntrCredentialPaths), len(huntrCredentialPatterns)))
		var findings int
		tui.db.conn.QueryRow("SELECT COUNT(*) FROM huntr_findings").Scan(&findings)
		tui.addOutput(fmt.Sprintf("[*] DB: %d findings stored", findings))
	case ModuleODINT:
		tui.addOutput("[*] ODINT Toolkit v4.0.0 loaded")
		tui.addOutput("[*] 42 recon modules | Auto-runs full suite per target")
		var scans, techs, findings int
		tui.db.conn.QueryRow("SELECT COUNT(*) FROM odint_scans").Scan(&scans)
		tui.db.conn.QueryRow("SELECT COUNT(*) FROM odint_technologies").Scan(&techs)
		tui.db.conn.QueryRow("SELECT COUNT(*) FROM odint_findings").Scan(&findings)
		tui.addOutput(fmt.Sprintf("[*] DB: %d scans | %d techs | %d findings", scans, techs, findings))
	case ModulePortScanner:
		tui.addOutput("[*] Port Scanner module loaded")
		tui.addOutput(fmt.Sprintf("[*] Top 1000 ports + custom ranges | %d service signatures", len(portServiceNames)))
		var targets int
		tui.db.conn.QueryRow("SELECT COUNT(*) FROM port_targets").Scan(&targets)
		tui.addOutput(fmt.Sprintf("[*] DB: %d targets scanned", targets))
	case ModuleSilentEye:
		tui.addOutput("[*] Silent Eye module loaded")
		tui.addOutput("[*] 8 vendors: crt.sh, Censys, Shodan, VirusTotal, SecurityTrails, Hunter.io, BGPView, IPInfo")
		var targets int
		tui.db.conn.QueryRow("SELECT COUNT(*) FROM silent_targets").Scan(&targets)
		tui.addOutput(fmt.Sprintf("[*] DB: %d targets scanned", targets))
	case ModuleTerminator:
		tui.addOutput("[*] =========================================")
		tui.addOutput("[*]   TERMINATOR MODE — ALL MODULES ARMED")
		tui.addOutput("[*] =========================================")
		tui.addOutput("[*] Chains: ODINT → Huntr → Port Scanner → Silent Eye → Pipeline")
		totalSites, wpSites, _, _ := tui.db.GetSiteStats()
		totalHashes := tui.db.GetTotalHashes()
		_, cracked := tui.db.GetCrackedStats()
		var huntrFindings, odintScans, portTargets, silentTargets int
		tui.db.conn.QueryRow("SELECT COUNT(*) FROM huntr_findings").Scan(&huntrFindings)
		tui.db.conn.QueryRow("SELECT COUNT(*) FROM odint_scans").Scan(&odintScans)
		tui.db.conn.QueryRow("SELECT COUNT(*) FROM port_targets").Scan(&portTargets)
		tui.db.conn.QueryRow("SELECT COUNT(*) FROM silent_targets").Scan(&silentTargets)
		tui.addOutput(fmt.Sprintf("[*] DB: %d sites | %d WP | %d hashes | %d cracked", totalSites, wpSites, totalHashes, cracked))
		tui.addOutput(fmt.Sprintf("[*] DB: %d ODINT scans | %d Huntr findings | %d ports | %d silent", odintScans, huntrFindings, portTargets, silentTargets))
	}
}

// ===========================================================================
// COMMAND ROUTING
// ===========================================================================

func (tui *TUI) startCommand(cmdKey string) {
	switch tui.activeModule {
	case ModuleWPDeterminer:
		tui.startWPCommand(cmdKey)
	case ModuleHashHunter:
		tui.startHHCommand(cmdKey)
	case ModuleHashCracker:
		tui.startHCCommand(cmdKey)
	case ModulePipeline:
		tui.startPipelineCommand(cmdKey)
	case ModuleHuntr:
		tui.startHuntrCommand(cmdKey)
	case ModuleODINT:
		tui.startODINTCommand(cmdKey)
	case ModulePortScanner:
		tui.startPortCommand(cmdKey)
	case ModuleSilentEye:
		tui.startSilentCommand(cmdKey)
	case ModuleTerminator:
		tui.startTerminatorCommand(cmdKey)
	}
}

func (tui *TUI) startWPCommand(cmdKey string) {
	tui.currentCmd = cmdKey
	tui.collectedInputs = make(map[string]string)

	switch cmdKey {
	case "1":
		tui.inputFields = []string{"url"}
		tui.addOutput("[*] CHECK -- Enter a URL to test for WordPress")
		tui.inputPrompt = "Enter target URL"
	case "2":
		files, _ := filepath.Glob(filepath.Join(tui.listsDir, "*.txt"))
		if len(files) > 0 {
			tui.pickerFiles = nil
			for _, f := range files {
				tui.pickerFiles = append(tui.pickerFiles, f)
			}
			tui.pickerSelected = 0
			tui.commandState = StateFilePicker
			return
		}
		tui.addOutput("[*] No .txt files found in lists/ folder")
		tui.addOutput("[*] Place .txt files in: " + tui.listsDir)
		return
	case "3":
		domains := tui.db.GetRecheckDomains()
		if len(domains) == 0 {
			tui.addOutput("[*] No errors or non-WordPress sites in DB to recheck")
			return
		}
		tui.commandState = StateRunning
		tui.Render()
		ctx, cancel := context.WithCancel(context.Background())
		tui.cancelScan = cancel
		go func() {
			defer func() { tui.cancelScan = nil }()
			tui.runRecheck(ctx, domains)
			if ctx.Err() == nil {
				tui.commandState = StateComplete
			}
			tui.Render()
		}()
		return
	case "4":
		tui.clearOutput()
		tui.addOutput("[*] Output cleared")
		return
	case "Q", "q":
		tui.running = false
		return
	default:
		return
	}

	tui.currentField = 0
	tui.inputBuffer = ""
	tui.inputCursor = 0
	tui.commandState = StateInput
}

func (tui *TUI) startHHCommand(cmdKey string) {
	tui.currentCmd = cmdKey
	tui.collectedInputs = make(map[string]string)

	switch cmdKey {
	case "1": // Scan single target
		tui.inputFields = []string{"target"}
		tui.addOutput("[*] SCAN -- Enter a WordPress site to scan for hashes")
		tui.inputPrompt = "Enter target URL"
		tui.currentField = 0
		tui.inputBuffer = ""
		tui.inputCursor = 0
		tui.commandState = StateInput
		return
	case "2": // Bulk scan from file
		files, _ := filepath.Glob(filepath.Join(tui.targetsDir, "*.txt"))
		if len(files) > 0 {
			tui.pickerFiles = files
			tui.pickerSelected = 0
			tui.commandState = StateFilePicker
			return
		}
		tui.addOutput("[*] No .txt files found in targets/ folder")
		tui.addOutput("[*] Place .txt files in: " + tui.targetsDir)
		return
	case "3": // Rescan incomplete
		domains := tui.db.GetIncompleteDomains()
		if len(domains) == 0 {
			tui.addOutput("[*] No incomplete domains to rescan")
			return
		}
		tui.commandState = StateRunning
		tui.Render()
		ctx, cancel := context.WithCancel(context.Background())
		tui.cancelScan = cancel
		go func() {
			defer func() { tui.cancelScan = nil }()
			tui.runRescanIncomplete(ctx, domains)
			if ctx.Err() == nil {
				tui.commandState = StateComplete
			}
			tui.Render()
		}()
		return
	case "4": // Stats
		tui.showHHStats()
		return
	case "5": // Export
		tui.exportHashes()
		return
	case "6": // History
		tui.showHashHistory()
		return
	case "7": // New DB - not applicable in unified tool
		tui.addOutput("[*] Unified database in use - cannot create new DB")
		return
	case "8": // Merge
		tui.inputFields = []string{"dbpath"}
		tui.addOutput("[*] MERGE -- Enter path to database to merge")
		tui.inputPrompt = "Enter database path"
		tui.currentField = 0
		tui.inputBuffer = ""
		tui.inputCursor = 0
		tui.commandState = StateInput
		return
	case "9": // Clear
		tui.clearOutput()
		tui.addOutput("[*] Output cleared")
		return
	case "0": // Resume
		tui.addOutput("[*] Resume functionality -- checking for interrupted sessions...")
		return
	case "Q", "q":
		tui.running = false
		return
	}
}

func (tui *TUI) startHCCommand(cmdKey string) {
	tui.currentCmd = cmdKey
	tui.collectedInputs = make(map[string]string)

	switch cmdKey {
	case "1": // Load from external .db
		tui.inputFields = []string{"dbpath"}
		tui.addOutput("[*] LOAD -- Enter path to .db file with hashes")
		tui.inputPrompt = "Enter database path"
		tui.currentField = 0
		tui.inputBuffer = ""
		tui.inputCursor = 0
		tui.commandState = StateInput
		return
	case "2": // Load from internal hashes table
		tui.loadInternalHashes()
		return
	case "3": // Load wordlist
		tui.inputFields = []string{"wordlist"}
		tui.addOutput("[*] WORDLIST -- Enter path to custom wordlist")
		tui.inputPrompt = "Enter wordlist path"
		tui.currentField = 0
		tui.inputBuffer = ""
		tui.inputCursor = 0
		tui.commandState = StateInput
		return
	case "4": // Start cracking
		tui.startCracking()
		return
	case "5": // View cracked
		tui.showCracked()
		return
	case "6": // Export
		tui.exportCracked()
		return
	case "7": // Stats
		tui.showCrackStats()
		return
	case "8": // Clear
		tui.clearOutput()
		tui.addOutput("[*] Output cleared")
		return
	case "Q", "q":
		tui.running = false
		return
	}
}

func (tui *TUI) startPipelineCommand(cmdKey string) {
	tui.currentCmd = cmdKey
	tui.collectedInputs = make(map[string]string)

	switch cmdKey {
	case "1": // Full pipeline - needs a file of domains
		files, _ := filepath.Glob(filepath.Join(tui.listsDir, "*.txt"))
		if len(files) > 0 {
			tui.pickerFiles = files
			tui.pickerSelected = 0
			tui.commandState = StateFilePicker
			return
		}
		tui.addOutput("[*] No .txt files found in lists/ folder")
		tui.addOutput("[*] Place domain lists in: " + tui.listsDir)
		return
	case "2": // Detect -> Harvest
		files, _ := filepath.Glob(filepath.Join(tui.listsDir, "*.txt"))
		if len(files) > 0 {
			tui.pickerFiles = files
			tui.pickerSelected = 0
			tui.commandState = StateFilePicker
			return
		}
		tui.addOutput("[*] No .txt files found in lists/ folder")
		return
	case "3": // Harvest -> Crack (uses DB data, no file needed)
		tui.commandState = StateRunning
		tui.Render()
		ctx, cancel := context.WithCancel(context.Background())
		tui.cancelScan = cancel
		go func() {
			defer func() { tui.cancelScan = nil }()
			tui.runHarvestCrack(ctx)
			if ctx.Err() == nil {
				tui.commandState = StateComplete
			}
			tui.Render()
		}()
		return
	case "4": // View cross-module targets intelligence
		tui.showCrossModuleIntel()
		return
	case "5": // Unified export
		tui.exportAll()
		return
	case "6": // Clear
		tui.clearOutput()
		tui.addOutput("[*] Output cleared")
		return
	case "Q", "q":
		tui.running = false
		return
	}
}

func (tui *TUI) executeCommand() {
	if tui.currentCmd == "settings" {
		field := tui.inputFields[0]
		value := tui.collectedInputs[field]
		tui.applySettingValue(field, value)
		return
	}

	switch tui.activeModule {
	case ModuleWPDeterminer:
		tui.executeWPCommand()
	case ModuleHashHunter:
		tui.executeHHCommand()
	case ModuleHashCracker:
		tui.executeHCCommand()
	case ModulePipeline:
		tui.executePipelineCommand()
	case ModuleHuntr:
		tui.executeHuntrCommand()
	case ModuleODINT:
		tui.executeODINTCommand()
	case ModulePortScanner:
		tui.portHandleInput("")
	case ModuleSilentEye:
		tui.executeSilentCommand()
	case ModuleTerminator:
		// Terminator uses file picker directly, no text input commands
	}
}

func (tui *TUI) executeWPCommand() {
	tui.commandState = StateRunning
	tui.Render()

	ctx, cancel := context.WithCancel(context.Background())
	tui.cancelScan = cancel

	go func() {
		defer func() {
			tui.cancelScan = nil
		}()
		switch tui.currentCmd {
		case "1":
			tui.runSingleCheck(ctx, tui.collectedInputs["url"])
		case "2":
			tui.runBulkCheck(ctx, tui.collectedInputs["file"])
		}
		if ctx.Err() == nil {
			tui.commandState = StateComplete
		}
		tui.Render()
	}()
}

func (tui *TUI) executeHHCommand() {
	tui.commandState = StateRunning
	tui.Render()
	ctx, cancel := context.WithCancel(context.Background())
	tui.cancelScan = cancel
	go func() {
		defer func() { tui.cancelScan = nil }()
		switch tui.currentCmd {
		case "1":
			tui.runSingleScan(ctx, tui.collectedInputs["target"])
		case "2":
			tui.runBulkScan(ctx, tui.collectedInputs["file"])
		case "8":
			tui.runMergeDB(tui.collectedInputs["dbpath"])
		}
		if ctx.Err() == nil {
			tui.commandState = StateComplete
		}
		tui.Render()
	}()
}

func (tui *TUI) executeHCCommand() {
	switch tui.currentCmd {
	case "1":
		path := tui.collectedInputs["dbpath"]
		err := tui.loadExternalDB(path)
		if err != nil {
			tui.addOutput(fmt.Sprintf("[-] Error: %v", err))
		}
		tui.commandState = StateMenu
	case "3":
		path := tui.collectedInputs["wordlist"]
		err := tui.loadWordlist(path)
		if err != nil {
			tui.addOutput(fmt.Sprintf("[-] Error: %v", err))
		} else {
			tui.addOutput(fmt.Sprintf("[+] Loaded %d words", len(tui.hcCustomWords)))
		}
		tui.commandState = StateMenu
	}
}

func (tui *TUI) executePipelineCommand() {
	tui.commandState = StateRunning
	tui.Render()
	ctx, cancel := context.WithCancel(context.Background())
	tui.cancelScan = cancel
	go func() {
		defer func() { tui.cancelScan = nil }()
		switch tui.currentCmd {
		case "1":
			tui.runFullPipeline(ctx, tui.collectedInputs["file"])
		case "2":
			tui.runDetectHarvest(ctx, tui.collectedInputs["file"])
		}
		if ctx.Err() == nil {
			tui.commandState = StateComplete
		}
		tui.Render()
	}()
}

// ===========================================================================
// WP DETERMINER COMMANDS
// ===========================================================================

func (tui *TUI) runSingleCheck(ctx context.Context, target string) {
	tui.startLog("single")
	defer tui.closeLog()

	atomic.StoreInt64(&tui.wpTotal, 1)
	atomic.StoreInt64(&tui.wpChecked, 0)
	atomic.StoreInt64(&tui.wpWordpress, 0)
	atomic.StoreInt64(&tui.wpNotWP, 0)
	atomic.StoreInt64(&tui.wpErrors, 0)

	tui.addOutput(fmt.Sprintf("[*] Checking: %s", target))
	tui.addOutput("")
	tui.writeLog("TARGET: %s", target)

	result := tui.checker.Check(ctx, target)
	atomic.AddInt64(&tui.wpChecked, 1)

	if ctx.Err() != nil {
		tui.writeLog("CANCELLED by user")
		return
	}

	tui.db.UpsertSiteResult(result)

	if result.Error != nil {
		atomic.AddInt64(&tui.wpErrors, 1)
		tui.addOutput(fmt.Sprintf("[ERR] %s -- %v", result.Domain, result.Error))
		tui.writeLog("ERROR: %s -- %v", result.Domain, result.Error)
	} else if result.IsWordPress {
		atomic.AddInt64(&tui.wpWordpress, 1)
		tui.addOutput(fmt.Sprintf("[WP] %s -- WordPress CONFIRMED (method: %s)", result.Domain, result.Method))
		tui.writeLog("WORDPRESS: %s (method: %s)", result.Domain, result.Method)
	} else {
		atomic.AddInt64(&tui.wpNotWP, 1)
		tui.addOutput(fmt.Sprintf("[NOT] %s -- Not WordPress", result.Domain))
		tui.writeLog("NOT WP: %s", result.Domain)
	}
	tui.addOutput("")
}

func (tui *TUI) runBulkCheck(ctx context.Context, filePath string) {
	filePath = strings.Trim(filePath, "\"'")

	if !filepath.IsAbs(filePath) {
		filePath = filepath.Join(tui.listsDir, filePath)
	}
	absPath, _ := filepath.Abs(filePath)
	absLists, _ := filepath.Abs(tui.listsDir)
	if !strings.HasPrefix(strings.ToLower(absPath), strings.ToLower(absLists)) {
		tui.addOutput("[-] Files must be inside lists/ folder")
		return
	}

	tui.startLog("bulk")
	defer tui.closeLog()

	file, err := os.Open(filePath)
	if err != nil {
		tui.addOutput(fmt.Sprintf("[-] Error opening file: %v", err))
		tui.writeLog("ERROR: Failed to open file: %v", err)
		return
	}

	type lineEntry struct {
		raw       string
		trimmed   string
		commented bool
		blank     bool
	}

	var lines []lineEntry
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		raw := scanner.Text()
		trimmed := strings.TrimSpace(raw)
		entry := lineEntry{raw: raw, trimmed: trimmed}

		if trimmed == "" {
			entry.blank = true
		} else if strings.HasPrefix(trimmed, "#") {
			entry.commented = true
		}
		lines = append(lines, entry)
	}
	file.Close()

	type targetInfo struct {
		index  int
		target string
	}
	var targets []targetInfo
	for i, entry := range lines {
		if !entry.blank && !entry.commented {
			targets = append(targets, targetInfo{index: i, target: entry.trimmed})
		}
	}

	if len(targets) == 0 {
		tui.addOutput("[-] No targets found in file (all blank or commented)")
		tui.writeLog("ERROR: No targets found in file")
		return
	}

	alreadyCommented := 0
	for _, entry := range lines {
		if entry.commented {
			alreadyCommented++
		}
	}

	tui.writeLog("FILE: %s", filePath)
	tui.writeLog("TOTAL LINES: %d", len(lines))
	tui.writeLog("TARGETS TO CHECK: %d", len(targets))
	tui.writeLog("ALREADY COMMENTED: %d", alreadyCommented)
	tui.writeLog("")

	atomic.StoreInt64(&tui.wpTotal, int64(len(targets)))
	atomic.StoreInt64(&tui.wpChecked, 0)
	atomic.StoreInt64(&tui.wpWordpress, 0)
	atomic.StoreInt64(&tui.wpNotWP, 0)
	atomic.StoreInt64(&tui.wpErrors, 0)

	workers := tui.wpWorkers
	tui.addOutput(fmt.Sprintf("[*] BULK CHECK -- Non-WordPress sites will get # prepended"))
	tui.addOutput(fmt.Sprintf("[*] Loaded %d targets to check (%d already commented)", len(targets), alreadyCommented))
	tui.addOutput(fmt.Sprintf("[*] Workers: %d | Timeout: %ds", workers, int(tui.wpTimeout.Seconds())))
	tui.addOutput("[*] Press ESC to cancel")
	tui.addOutput("")

	var linesMu sync.Mutex
	wpCount := 0
	notWPCount := 0
	errCount := 0

	flushFile := func() {
		var out []string
		for _, entry := range lines {
			out = append(out, entry.raw)
		}
		os.WriteFile(filePath, []byte(strings.Join(out, "\n")+"\n"), 0644)
	}

	type workItem struct {
		index  int
		target string
	}

	workChan := make(chan workItem, len(targets))
	resultChan := make(chan struct {
		index  int
		result CheckResult
	}, workers*2)

	var wg sync.WaitGroup
	for i := 0; i < workers; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for item := range workChan {
				select {
				case <-ctx.Done():
					return
				default:
				}
				res := tui.checker.Check(ctx, item.target)
				resultChan <- struct {
					index  int
					result CheckResult
				}{item.index, res}
			}
		}()
	}

	go func() {
		for _, t := range targets {
			workChan <- workItem{index: t.index, target: t.target}
		}
		close(workChan)
		wg.Wait()
		close(resultChan)
	}()

	for r := range resultChan {
		if ctx.Err() != nil {
			tui.addOutput("[!] Bulk check cancelled by user")
			tui.writeLog("CANCELLED by user")
			linesMu.Lock()
			flushFile()
			linesMu.Unlock()
			return
		}

		atomic.AddInt64(&tui.wpChecked, 1)
		res := r.result
		tui.db.UpsertSiteResult(res)

		if res.Error != nil {
			atomic.AddInt64(&tui.wpErrors, 1)
			errCount++
			tui.addOutput(fmt.Sprintf("[ERR] %s -- %v", res.Domain, res.Error))
			tui.writeLog("ERROR: %s -- %v", res.Domain, res.Error)
			linesMu.Lock()
			lines[r.index].raw = "# " + lines[r.index].raw
			lines[r.index].commented = true
			linesMu.Unlock()
		} else if res.IsWordPress {
			atomic.AddInt64(&tui.wpWordpress, 1)
			wpCount++
			tui.addOutput(fmt.Sprintf("[WP]  %s  (%s)", res.Domain, res.Method))
			tui.writeLog("WORDPRESS: %s (method: %s)", res.Domain, res.Method)
		} else {
			atomic.AddInt64(&tui.wpNotWP, 1)
			notWPCount++
			tui.addOutput(fmt.Sprintf("[NOT] %s", res.Domain))
			tui.writeLog("NOT WP: %s", res.Domain)
			linesMu.Lock()
			lines[r.index].raw = "# " + lines[r.index].raw
			lines[r.index].commented = true
			linesMu.Unlock()
		}

		linesMu.Lock()
		flushFile()
		linesMu.Unlock()

		tui.Render()
	}

	tui.writeLog("")
	tui.writeLog("--- Summary ---")
	tui.writeLog("WordPress:     %d", wpCount)
	tui.writeLog("Not WordPress: %d (# prepended)", notWPCount)
	tui.writeLog("Errors:        %d (# prepended)", errCount)
	tui.writeLog("Already #:     %d (untouched)", alreadyCommented)
	tui.writeLog("File updated:  %s", filePath)

	tui.addOutput("")
	tui.addOutput("[*] === Bulk Check Complete ===")
	tui.addOutput(fmt.Sprintf("    WordPress:     %d sites", wpCount))
	tui.addOutput(fmt.Sprintf("    Not WordPress: %d sites (# prepended)", notWPCount))
	tui.addOutput(fmt.Sprintf("    Errors:        %d sites (# prepended)", errCount))
	tui.addOutput(fmt.Sprintf("    Already #:     %d lines (untouched)", alreadyCommented))
	tui.addOutput(fmt.Sprintf("    File updated:  %s", filePath))
}

func (tui *TUI) runRecheck(ctx context.Context, domains []string) {
	tui.startLog("recheck")
	defer tui.closeLog()

	atomic.StoreInt64(&tui.wpTotal, int64(len(domains)))
	atomic.StoreInt64(&tui.wpChecked, 0)
	atomic.StoreInt64(&tui.wpWordpress, 0)
	atomic.StoreInt64(&tui.wpNotWP, 0)
	atomic.StoreInt64(&tui.wpErrors, 0)

	workers := tui.wpWorkers
	tui.addOutput("[*] RECHECK -- Re-checking errors & non-WordPress sites from DB")
	tui.addOutput(fmt.Sprintf("[*] Found %d sites to recheck", len(domains)))
	tui.addOutput(fmt.Sprintf("[*] Workers: %d | Timeout: %ds", workers, int(tui.wpTimeout.Seconds())))
	tui.addOutput("[*] Press ESC to cancel")
	tui.addOutput("")
	tui.writeLog("RECHECK: %d domains", len(domains))
	tui.writeLog("Workers: %d | Timeout: %ds", workers, int(tui.wpTimeout.Seconds()))
	tui.writeLog("")

	workChan := make(chan string, len(domains))
	resultChan := make(chan CheckResult, workers*2)

	var wg sync.WaitGroup
	for i := 0; i < workers; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for domain := range workChan {
				select {
				case <-ctx.Done():
					return
				default:
				}
				resultChan <- tui.checker.Check(ctx, domain)
			}
		}()
	}

	go func() {
		for _, d := range domains {
			workChan <- d
		}
		close(workChan)
		wg.Wait()
		close(resultChan)
	}()

	newWP := 0
	stillNotWP := 0
	stillErr := 0
	uncommented := 0

	for res := range resultChan {
		if ctx.Err() != nil {
			tui.addOutput("[!] Recheck cancelled by user")
			tui.writeLog("CANCELLED by user")
			return
		}

		atomic.AddInt64(&tui.wpChecked, 1)
		tui.db.UpsertSiteResult(res)

		if res.Error != nil {
			atomic.AddInt64(&tui.wpErrors, 1)
			stillErr++
			tui.addOutput(fmt.Sprintf("[ERR] %s -- %v", res.Domain, res.Error))
			tui.writeLog("ERROR: %s -- %v", res.Domain, res.Error)
		} else if res.IsWordPress {
			atomic.AddInt64(&tui.wpWordpress, 1)
			newWP++
			tui.addOutput(fmt.Sprintf("[WP]  %s  (%s) <- NEW!", res.Domain, res.Method))
			tui.writeLog("WORDPRESS: %s (method: %s) -- NEWLY DETECTED", res.Domain, res.Method)

			uncommented += tui.uncommentDomain(res.Domain)
		} else {
			atomic.AddInt64(&tui.wpNotWP, 1)
			stillNotWP++
			tui.addOutput(fmt.Sprintf("[NOT] %s", res.Domain))
			tui.writeLog("NOT WP: %s", res.Domain)
		}
		tui.Render()
	}

	if ctx.Err() != nil {
		return
	}

	tui.writeLog("")
	tui.writeLog("--- Recheck Summary ---")
	tui.writeLog("Newly confirmed WordPress: %d", newWP)
	tui.writeLog("Still not WordPress:       %d", stillNotWP)
	tui.writeLog("Still errors:              %d", stillErr)
	if uncommented > 0 {
		tui.writeLog("Uncommented in files:      %d", uncommented)
	}

	tui.addOutput("")
	tui.addOutput("[*] === Recheck Complete ===")
	tui.addOutput(fmt.Sprintf("    Newly confirmed WP: %d sites", newWP))
	tui.addOutput(fmt.Sprintf("    Still not WP:       %d sites", stillNotWP))
	tui.addOutput(fmt.Sprintf("    Still errors:       %d sites", stillErr))
	if uncommented > 0 {
		tui.addOutput(fmt.Sprintf("    Uncommented in files: %d lines", uncommented))
	}
}

func (tui *TUI) uncommentDomain(domain string) int {
	domainLower := strings.ToLower(domain)
	count := 0

	files, _ := filepath.Glob(filepath.Join(tui.listsDir, "*.txt"))
	for _, fpath := range files {
		data, err := os.ReadFile(fpath)
		if err != nil {
			continue
		}
		lines := strings.Split(string(data), "\n")
		changed := false
		for i, line := range lines {
			trimmed := strings.TrimSpace(line)
			if !strings.HasPrefix(trimmed, "# ") {
				continue
			}
			d := strings.TrimSpace(strings.TrimPrefix(trimmed, "# "))
			d = strings.TrimPrefix(d, "https://")
			d = strings.TrimPrefix(d, "http://")
			d = strings.TrimSuffix(d, "/")
			d = strings.TrimPrefix(d, "www.")
			if strings.ToLower(d) == domainLower {
				lines[i] = d
				changed = true
				count++
				tui.writeLog("UNCOMMENTED: %s in %s", d, filepath.Base(fpath))
			}
		}
		if changed {
			os.WriteFile(fpath, []byte(strings.Join(lines, "\n")), 0644)
		}
	}
	return count
}

// ===========================================================================
// HASH HUNTER COMMANDS
// ===========================================================================

func (tui *TUI) runSingleScan(ctx context.Context, target string) {
	tui.startLog("hh-scan")
	defer tui.closeLog()

	tui.addOutput(fmt.Sprintf("[*] Scanning: %s", target))
	tui.writeLog("Single scan: %s", target)

	result := tui.hunter.Scan(ctx, target)

	if ctx.Err() != nil {
		return
	}

	newHashes := 0
	for _, user := range result.Users {
		isNew := tui.db.InsertHashIfNew(result.Domain, user.Hash, user.Username, "", user.AvatarURL, user.HashType)
		if isNew {
			newHashes++
			hashPreview := user.Hash
			if len(hashPreview) > 16 {
				hashPreview = hashPreview[:16] + "..."
			}
			tui.addOutput(fmt.Sprintf("[+] NEW: %s (%s) %s", hashPreview, user.Username, result.Domain))
		}
	}

	tui.db.UpdateDomain(result.Domain, result.Status, len(result.Users))

	tui.addOutput("")
	tui.addOutput(fmt.Sprintf("[*] === Scan Complete: %s ===", result.Domain))
	tui.addOutput(fmt.Sprintf("    Status: %s", result.Status))
	tui.addOutput(fmt.Sprintf("    Total hashes found: %d", len(result.Users)))
	tui.addOutput(fmt.Sprintf("    New hashes: %d", newHashes))
	if result.APITotal > 0 {
		tui.addOutput(fmt.Sprintf("    API reported: %d users, fetched: %d", result.APITotal, result.APIFetched))
	}

	tui.writeLog("Scan complete: %s, status=%s, hashes=%d, new=%d", result.Domain, result.Status, len(result.Users), newHashes)
}

func (tui *TUI) runBulkScan(ctx context.Context, filePath string) {
	tui.startLog("hh-bulk")
	defer tui.closeLog()

	data, err := os.ReadFile(filePath)
	if err != nil {
		tui.addOutput(fmt.Sprintf("[ERR] Cannot read file: %v", err))
		return
	}

	lines := strings.Split(string(data), "\n")
	var targets []string
	for _, line := range lines {
		line = strings.TrimSpace(line)
		if line != "" && !strings.HasPrefix(line, "#") {
			targets = append(targets, line)
		}
	}

	if len(targets) == 0 {
		tui.addOutput("[*] No targets found in file")
		return
	}

	tui.addOutput(fmt.Sprintf("[*] Bulk scan: %d targets from %s", len(targets), filepath.Base(filePath)))
	tui.writeLog("Bulk scan: %d targets from %s", len(targets), filePath)

	totalNew := 0
	for i, target := range targets {
		if ctx.Err() != nil {
			break
		}

		tui.addOutput(fmt.Sprintf("[*] [%d/%d] Scanning: %s", i+1, len(targets), target))

		result := tui.hunter.Scan(ctx, target)
		if ctx.Err() != nil {
			break
		}

		newHashes := 0
		for _, user := range result.Users {
			isNew := tui.db.InsertHashIfNew(result.Domain, user.Hash, user.Username, "", user.AvatarURL, user.HashType)
			if isNew {
				newHashes++
			}
		}
		totalNew += newHashes

		tui.db.UpdateDomain(result.Domain, result.Status, len(result.Users))

		if len(result.Users) > 0 {
			tui.addOutput(fmt.Sprintf("[+] %s: %d hashes (%d new)", result.Domain, len(result.Users), newHashes))
		} else {
			tui.addOutput(fmt.Sprintf("[-] %s: no hashes (%s)", result.Domain, result.Status))
		}
		tui.writeLog("[%d/%d] %s: status=%s, hashes=%d, new=%d", i+1, len(targets), result.Domain, result.Status, len(result.Users), newHashes)
		tui.Render()
	}

	tui.addOutput("")
	tui.addOutput("[*] === Bulk Scan Complete ===")
	tui.addOutput(fmt.Sprintf("    Targets: %d", len(targets)))
	tui.addOutput(fmt.Sprintf("    New hashes: %d", totalNew))
	tui.addOutput(fmt.Sprintf("    Total hashes in DB: %d", tui.db.GetTotalHashes()))
}

func (tui *TUI) runRescanIncomplete(ctx context.Context, domains []string) {
	tui.startLog("hh-rescan")
	defer tui.closeLog()

	tui.addOutput(fmt.Sprintf("[*] Rescanning %d incomplete domains", len(domains)))

	totalNew := 0
	for i, domain := range domains {
		if ctx.Err() != nil {
			break
		}

		tui.addOutput(fmt.Sprintf("[*] [%d/%d] Rescanning: %s", i+1, len(domains), domain))
		result := tui.hunter.Scan(ctx, domain)
		if ctx.Err() != nil {
			break
		}

		newHashes := 0
		for _, user := range result.Users {
			isNew := tui.db.InsertHashIfNew(result.Domain, user.Hash, user.Username, "", user.AvatarURL, user.HashType)
			if isNew {
				newHashes++
			}
		}
		totalNew += newHashes
		tui.db.UpdateDomain(result.Domain, result.Status, len(result.Users))

		if newHashes > 0 {
			tui.addOutput(fmt.Sprintf("[+] %s: %d new hashes", result.Domain, newHashes))
		}
		tui.Render()
	}

	tui.addOutput("")
	tui.addOutput("[*] === Rescan Complete ===")
	tui.addOutput(fmt.Sprintf("    New hashes found: %d", totalNew))
}

func (tui *TUI) showHHStats() {
	totalHashes := tui.db.GetTotalHashes()
	totalDomains := tui.db.GetTotalDomains()
	incomplete := tui.db.GetIncompleteCount()
	_, cracked := tui.db.GetCrackedStats()

	tui.addOutput("")
	tui.addOutput("[*] === Hash Hunter Statistics ===")
	tui.addOutput(fmt.Sprintf("    Total hashes: %d", totalHashes))
	tui.addOutput(fmt.Sprintf("    Total domains: %d", totalDomains))
	tui.addOutput(fmt.Sprintf("    Incomplete domains: %d", incomplete))
	tui.addOutput(fmt.Sprintf("    Cracked hashes: %d", cracked))
	tui.addOutput("")
}

func (tui *TUI) exportHashes() {
	totalHashes := tui.db.GetTotalHashes()
	if totalHashes == 0 {
		tui.addOutput("[-] No hashes to export")
		return
	}

	filename := filepath.Join(tui.logsDir, fmt.Sprintf("hashes_export_%s.csv", time.Now().Format("2006-01-02_150405")))
	file, err := os.Create(filename)
	if err != nil {
		tui.addOutput(fmt.Sprintf("[-] Error creating export: %v", err))
		return
	}
	defer file.Close()

	file.WriteString("domain,hash,username,hash_type,discovered\n")
	rows, err := tui.db.conn.Query("SELECT domain, hash, username, hash_type, discovered FROM hashes ORDER BY domain")
	if err != nil {
		tui.addOutput(fmt.Sprintf("[-] Error querying: %v", err))
		return
	}
	defer rows.Close()

	count := 0
	for rows.Next() {
		var domain, hash, username, hashType, discovered string
		rows.Scan(&domain, &hash, &username, &hashType, &discovered)
		file.WriteString(fmt.Sprintf("%s,%s,%s,%s,%s\n", domain, hash, username, hashType, discovered))
		count++
	}
	tui.addOutput(fmt.Sprintf("[+] Exported %d hashes to %s", count, filename))
}

func (tui *TUI) showHashHistory() {
	rows, err := tui.db.conn.Query("SELECT domain, hash, action, timestamp FROM hash_history ORDER BY timestamp DESC LIMIT 50")
	if err != nil {
		tui.addOutput("[-] No history available")
		return
	}
	defer rows.Close()

	tui.addOutput("")
	tui.addOutput("[*] === Recent Hash History (last 50) ===")
	count := 0
	for rows.Next() {
		var domain, hash, action, ts string
		rows.Scan(&domain, &hash, &action, &ts)
		hashPreview := hash
		if len(hash) > 16 {
			hashPreview = hash[:16] + "..."
		}
		tui.addOutput(fmt.Sprintf("    [%s] %s %s @ %s", action, hashPreview, domain, ts))
		count++
	}
	if count == 0 {
		tui.addOutput("    No history entries yet")
	}
	tui.addOutput("")
}

func (tui *TUI) runMergeDB(dbPath string) {
	tui.addOutput(fmt.Sprintf("[*] Merging from: %s", dbPath))

	srcDB, err := sql.Open("sqlite", dbPath)
	if err != nil {
		tui.addOutput(fmt.Sprintf("[-] Cannot open source database: %v", err))
		return
	}
	defer srcDB.Close()

	rows, err := srcDB.Query("SELECT domain, hash, username, hash_type FROM hashes WHERE hash != '' AND hash IS NOT NULL")
	if err != nil {
		tui.addOutput(fmt.Sprintf("[-] Cannot query source: %v", err))
		return
	}
	defer rows.Close()

	imported := 0
	for rows.Next() {
		var domain, hash, username, hashType string
		if rows.Scan(&domain, &hash, &username, &hashType) == nil {
			if tui.db.InsertHashIfNew(domain, hash, username, "", "", hashType) {
				imported++
			}
		}
	}

	tui.addOutput(fmt.Sprintf("[+] Merged %d new hashes from %s", imported, filepath.Base(dbPath)))
}

// ===========================================================================
// HASH CRACKER COMMANDS
// ===========================================================================

func (tui *TUI) loadInternalHashes() {
	rows, err := tui.db.conn.Query("SELECT domain, hash, username, hash_type FROM hashes WHERE hash != '' AND hash IS NOT NULL ORDER BY domain")
	if err != nil {
		tui.addOutput(fmt.Sprintf("[-] Error loading internal hashes: %v", err))
		return
	}
	defer rows.Close()

	tui.hcHashes = nil
	for rows.Next() {
		var r HashRecord
		if rows.Scan(&r.Domain, &r.Hash, &r.Username, &r.HashType) == nil {
			tui.hcHashes = append(tui.hcHashes, r)
		}
	}

	atomic.StoreInt64(&tui.hcTotalHashes, int64(len(tui.hcHashes)))
	tui.addOutput(fmt.Sprintf("[+] Loaded %d hashes from internal database", len(tui.hcHashes)))

	domains := make(map[string]int)
	for _, h := range tui.hcHashes {
		domains[h.Domain]++
	}
	tui.addOutput(fmt.Sprintf("    Domains: %d unique", len(domains)))
}

func (tui *TUI) loadExternalDB(path string) error {
	path = strings.TrimSpace(path)
	path = strings.Trim(path, "\"'")
	path = filepath.Clean(path)

	tui.addOutput(fmt.Sprintf("[*] Loading: %s", path))

	if _, err := os.Stat(path); os.IsNotExist(err) {
		return fmt.Errorf("file does not exist: %s", path)
	}

	db, err := sql.Open("sqlite", path)
	if err != nil {
		return err
	}
	defer db.Close()

	rows, err := db.Query("SELECT COALESCE(domain,''), hash, COALESCE(username,''), COALESCE(hash_type,'md5') FROM hashes WHERE hash != '' AND hash IS NOT NULL")
	if err != nil {
		return err
	}
	defer rows.Close()

	tui.hcHashes = nil
	for rows.Next() {
		var r HashRecord
		if rows.Scan(&r.Domain, &r.Hash, &r.Username, &r.HashType) == nil {
			tui.hcHashes = append(tui.hcHashes, r)
		}
	}

	atomic.StoreInt64(&tui.hcTotalHashes, int64(len(tui.hcHashes)))
	tui.addOutput(fmt.Sprintf("[+] Loaded %d hashes from %s", len(tui.hcHashes), filepath.Base(path)))
	return nil
}

func (tui *TUI) loadWordlist(path string) error {
	path = strings.TrimSpace(path)
	path = strings.Trim(path, "\"'")
	path = filepath.Clean(path)

	file, err := os.Open(path)
	if err != nil {
		return err
	}
	defer file.Close()

	tui.hcCustomWords = nil
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line != "" && !strings.HasPrefix(line, "#") {
			tui.hcCustomWords = append(tui.hcCustomWords, line)
		}
	}
	return nil
}

func (tui *TUI) startCracking() {
	if len(tui.hcHashes) == 0 {
		tui.addOutput("[-] No hashes loaded! Load from internal DB or external file first.")
		return
	}

	tui.startLog("crack")

	atomic.StoreInt64(&tui.hcTestedEmails, 0)
	atomic.StoreInt64(&tui.hcCrackedCount, 0)

	tui.hcCrackedMap = tui.db.LoadCrackedHashes()

	alreadyCracked := 0
	for _, record := range tui.hcHashes {
		if _, exists := tui.hcCrackedMap[record.Hash]; exists {
			alreadyCracked++
		}
	}
	remaining := len(tui.hcHashes) - alreadyCracked

	tui.commandState = StateRunning
	tui.addOutput("")
	tui.addOutput(fmt.Sprintf("[*] Starting crack of %d hashes (%d remaining)...", len(tui.hcHashes), remaining))
	tui.addOutput(fmt.Sprintf("[*] Using %d email providers", len(tui.hcProviders)))
	if len(tui.hcCustomWords) > 0 {
		tui.addOutput(fmt.Sprintf("[*] Using %d custom words", len(tui.hcCustomWords)))
	}
	tui.addOutput("")

	providers := []string{}
	seenDomains := make(map[string]bool)
	for _, h := range tui.hcHashes {
		if !seenDomains[h.Domain] && h.Domain != "" {
			seenDomains[h.Domain] = true
			providers = append(providers, h.Domain)
		}
	}
	for _, p := range tui.hcProviders {
		if !seenDomains[p] {
			seenDomains[p] = true
			providers = append(providers, p)
		}
	}
	for _, w := range tui.hcCustomWords {
		if strings.Contains(w, ".") {
			providers = append(providers, w)
		}
	}

	numWorkers := runtime.NumCPU()
	if numWorkers < 4 {
		numWorkers = 4
	}
	tui.addOutput(fmt.Sprintf("[*] Using %d crack workers", numWorkers))
	tui.addOutput("")

	go func() {
		defer tui.closeLog()

		// Filter to only uncracked hashes
		var work []HashRecord
		for _, record := range tui.hcHashes {
			if _, exists := tui.hcCrackedMap[record.Hash]; !exists {
				work = append(work, record)
			}
		}

		// Feed hashes to workers via channel
		hashChan := make(chan HashRecord, numWorkers*2)
		var wg sync.WaitGroup
		var crackedMu sync.Mutex

		for i := 0; i < numWorkers; i++ {
			wg.Add(1)
			go func() {
				defer wg.Done()
				for record := range hashChan {
					if tui.commandState != StateRunning {
						return
					}

					crackedMu.Lock()
					_, alreadyCracked := tui.hcCrackedMap[record.Hash]
					crackedMu.Unlock()
					if alreadyCracked {
						continue
					}

					targetHash := strings.ToLower(record.Hash)
					emailPatterns := generateEmailPatterns(record.Username, record.DisplayName, record.Domain, tui.hcFirstNames)
					for _, w := range tui.hcCustomWords {
						if !strings.Contains(w, ".") && !strings.Contains(w, "@") {
							emailPatterns = append(emailPatterns, strings.ToLower(w))
						}
					}

					found := false
					for _, pattern := range emailPatterns {
						if found || tui.commandState != StateRunning {
							break
						}
						for _, provider := range providers {
							email := pattern + "@" + provider
							tested := atomic.AddInt64(&tui.hcTestedEmails, 1)
							if tested%500000 == 0 {
								tui.Render()
							}

							md5Hash := hashEmail(email, "md5")
							if md5Hash == targetHash {
								atomic.AddInt64(&tui.hcCrackedCount, 1)
								crackedMu.Lock()
								tui.hcCrackedMap[record.Hash] = email
								crackedMu.Unlock()

								tui.addOutput(fmt.Sprintf("[CRACKED] %s", email))
								tui.addOutput(fmt.Sprintf("          Hash: %s (md5)", record.Hash))
								tui.addOutput(fmt.Sprintf("          User: %s @ %s", record.Username, record.Domain))
								tui.addOutput(fmt.Sprintf("          Method: pattern=%q + provider=%q", pattern, provider))
								tui.addOutput("")
								tui.db.InsertCracked(record.Hash, email, record.Domain, record.Username, "md5")
								tui.writeLog("CRACKED: %s -> %s (pattern=%s, provider=%s)", record.Hash, email, pattern, provider)
								tui.Render()
								found = true
								break
							}

							sha256Hash := hashEmail(email, "sha256")
							if sha256Hash == targetHash {
								atomic.AddInt64(&tui.hcCrackedCount, 1)
								crackedMu.Lock()
								tui.hcCrackedMap[record.Hash] = email
								crackedMu.Unlock()

								tui.addOutput(fmt.Sprintf("[CRACKED] %s", email))
								tui.addOutput(fmt.Sprintf("          Hash: %s (sha256)", record.Hash))
								tui.addOutput(fmt.Sprintf("          User: %s @ %s", record.Username, record.Domain))
								tui.addOutput(fmt.Sprintf("          Method: pattern=%q + provider=%q", pattern, provider))
								tui.addOutput("")
								tui.db.InsertCracked(record.Hash, email, record.Domain, record.Username, "sha256")
								tui.writeLog("CRACKED: %s -> %s (pattern=%s, provider=%s)", record.Hash, email, pattern, provider)
								tui.Render()
								found = true
								break
							}
						}
					}

					// Only cracked results are shown in output
				}
			}()
		}

		// Feed work
		for _, record := range work {
			if tui.commandState != StateRunning {
				break
			}
			hashChan <- record
		}
		close(hashChan)
		wg.Wait()

		crackedCount := atomic.LoadInt64(&tui.hcCrackedCount)
		testedCount := atomic.LoadInt64(&tui.hcTestedEmails)
		tui.hcCurrentHash = ""
		tui.commandState = StateComplete
		tui.addOutput("")
		tui.addOutput("[*] === Cracking Complete ===")
		tui.addOutput(fmt.Sprintf("    Tested: %d email combinations", testedCount))
		tui.addOutput(fmt.Sprintf("    Cracked: %d hashes", crackedCount))
		tui.addOutput(fmt.Sprintf("    Workers: %d", numWorkers))
		tui.addOutput("")
		tui.Render()
	}()
}

func (tui *TUI) showCracked() {
	tui.addOutput("")
	tui.addOutput("[*] === Cracked Hashes ===")
	crackedMap := tui.db.LoadCrackedHashes()
	if len(crackedMap) == 0 {
		tui.addOutput("    No hashes cracked yet")
	} else {
		for hash, email := range crackedMap {
			hashPreview := hash
			if len(hash) > 16 {
				hashPreview = hash[:16] + "..."
			}
			tui.addOutput(fmt.Sprintf("    %s -> %s", hashPreview, email))
		}
	}
	tui.addOutput("")
}

func (tui *TUI) exportCracked() {
	_, cracked := tui.db.GetCrackedStats()
	if cracked == 0 {
		tui.addOutput("[-] No cracked hashes to export")
		return
	}

	filename := filepath.Join(tui.logsDir, fmt.Sprintf("cracked_export_%s.csv", time.Now().Format("2006-01-02_150405")))
	file, err := os.Create(filename)
	if err != nil {
		tui.addOutput(fmt.Sprintf("[-] Error: %v", err))
		return
	}
	defer file.Close()

	file.WriteString("email,hash,domain,username,method,cracked_at\n")
	rows, err := tui.db.conn.Query("SELECT email, hash, domain, username, method, cracked_at FROM cracked ORDER BY cracked_at DESC")
	if err != nil {
		tui.addOutput(fmt.Sprintf("[-] Error: %v", err))
		return
	}
	defer rows.Close()

	count := 0
	for rows.Next() {
		var email, hash, domain, username, method, crackedAt string
		rows.Scan(&email, &hash, &domain, &username, &method, &crackedAt)
		file.WriteString(fmt.Sprintf("%s,%s,%s,%s,%s,%s\n", email, hash, domain, username, method, crackedAt))
		count++
	}
	tui.addOutput(fmt.Sprintf("[+] Exported %d cracked hashes to %s", count, filename))
}

func (tui *TUI) showCrackStats() {
	totalH, cracked := tui.db.GetCrackedStats()
	tui.addOutput("")
	tui.addOutput("[*] === Hash Cracker Statistics ===")
	tui.addOutput(fmt.Sprintf("    Total hashes in DB: %d", totalH))
	tui.addOutput(fmt.Sprintf("    Cracked: %d", cracked))
	tui.addOutput(fmt.Sprintf("    Loaded for cracking: %d", len(tui.hcHashes)))
	tui.addOutput(fmt.Sprintf("    Email providers: %d", len(tui.hcProviders)))
	tui.addOutput("")
}

// ===========================================================================
// PIPELINE COMMANDS
// ===========================================================================

func (tui *TUI) runFullPipeline(ctx context.Context, filePath string) {
	tui.startLog("pipeline-full")
	defer tui.closeLog()

	tui.pipelinePhase = "detect"
	tui.addOutput("[PIPELINE] ========================================")
	tui.addOutput("[PIPELINE] FULL PIPELINE: Detect -> Harvest -> Crack")
	tui.addOutput("[PIPELINE] ========================================")
	tui.addOutput("")

	// Phase 1: WordPress Detection
	tui.addOutput("[PHASE 1] WordPress Detection")
	tui.addOutput(fmt.Sprintf("[PHASE 1] Loading targets from: %s", filepath.Base(filePath)))
	tui.writeLog("Pipeline Phase 1: WordPress Detection from %s", filePath)
	tui.Render()

	data, err := os.ReadFile(filePath)
	if err != nil {
		tui.addOutput(fmt.Sprintf("[-] Cannot read file: %v", err))
		return
	}

	rawLines := strings.Split(string(data), "\n")
	var targets []string
	for _, line := range rawLines {
		line = strings.TrimSpace(line)
		if line != "" && !strings.HasPrefix(line, "#") {
			targets = append(targets, line)
		}
	}

	if len(targets) == 0 {
		tui.addOutput("[-] No targets found in file")
		return
	}

	tui.addOutput(fmt.Sprintf("[PHASE 1] Checking %d domains for WordPress...", len(targets)))
	tui.Render()

	var wpSites []string
	for i, target := range targets {
		if ctx.Err() != nil {
			return
		}

		result := tui.checker.Check(ctx, target)
		if ctx.Err() != nil {
			return
		}

		tui.db.UpsertSiteResult(result)

		if result.IsWordPress {
			wpSites = append(wpSites, result.Domain)
			tui.addOutput(fmt.Sprintf("[WP] [%d/%d] %s (method: %s)", i+1, len(targets), result.Domain, result.Method))
		} else if result.Error != nil {
			tui.addOutput(fmt.Sprintf("[ERR] [%d/%d] %s: %v", i+1, len(targets), result.Domain, result.Error))
		} else {
			tui.addOutput(fmt.Sprintf("[NOT] [%d/%d] %s", i+1, len(targets), result.Domain))
		}
		tui.writeLog("[Phase1] [%d/%d] %s: wp=%v", i+1, len(targets), result.Domain, result.IsWordPress)
		tui.Render()
	}

	tui.addOutput("")
	tui.addOutput(fmt.Sprintf("[PHASE 1] Complete: %d WordPress sites found out of %d", len(wpSites), len(targets)))
	tui.addOutput("")

	if len(wpSites) == 0 {
		tui.addOutput("[PIPELINE] No WordPress sites found — pipeline complete")
		tui.pipelinePhase = "complete"
		return
	}

	// Phase 2: Hash Harvest
	if ctx.Err() != nil {
		return
	}
	tui.pipelinePhase = "harvest"
	tui.addOutput("[PHASE 2] Hash Harvesting")
	tui.addOutput(fmt.Sprintf("[PHASE 2] Scanning %d WordPress sites for hashes...", len(wpSites)))
	tui.writeLog("Pipeline Phase 2: Hash Harvesting %d WP sites", len(wpSites))
	tui.Render()

	totalNewHashes := 0
	for i, site := range wpSites {
		if ctx.Err() != nil {
			return
		}

		tui.addOutput(fmt.Sprintf("[*] [%d/%d] Harvesting: %s", i+1, len(wpSites), site))
		result := tui.hunter.Scan(ctx, site)
		if ctx.Err() != nil {
			return
		}

		newHashes := 0
		for _, user := range result.Users {
			if tui.db.InsertHashIfNew(result.Domain, user.Hash, user.Username, "", user.AvatarURL, user.HashType) {
				newHashes++
			}
		}
		totalNewHashes += newHashes
		tui.db.UpdateDomain(result.Domain, result.Status, len(result.Users))

		if len(result.Users) > 0 {
			tui.addOutput(fmt.Sprintf("[+] %s: %d hashes (%d new)", result.Domain, len(result.Users), newHashes))
		} else {
			tui.addOutput(fmt.Sprintf("[-] %s: no hashes (%s)", result.Domain, result.Status))
		}
		tui.writeLog("[Phase2] [%d/%d] %s: hashes=%d, new=%d", i+1, len(wpSites), result.Domain, len(result.Users), newHashes)
		tui.Render()
	}

	tui.addOutput("")
	tui.addOutput(fmt.Sprintf("[PHASE 2] Complete: %d new hashes harvested", totalNewHashes))
	tui.addOutput("")

	if totalNewHashes == 0 {
		tui.addOutput("[PIPELINE] No new hashes found — pipeline complete")
		tui.pipelinePhase = "complete"
		return
	}

	// Phase 3: Crack
	if ctx.Err() != nil {
		return
	}
	tui.pipelinePhase = "crack"
	tui.addOutput("[PHASE 3] Hash Cracking")
	tui.writeLog("Pipeline Phase 3: Hash Cracking")

	uncracked := tui.db.GetUncracked()
	if len(uncracked) == 0 {
		tui.addOutput("[*] All hashes already cracked!")
		tui.pipelinePhase = "complete"
		return
	}

	tui.addOutput(fmt.Sprintf("[PHASE 3] Cracking %d uncracked hashes...", len(uncracked)))
	tui.Render()

	crackedMap := tui.db.LoadCrackedHashes()
	providers := make([]string, 0, len(tui.hcProviders))
	seenDomains := make(map[string]bool)
	for _, h := range uncracked {
		if !seenDomains[h.Domain] && h.Domain != "" {
			seenDomains[h.Domain] = true
			providers = append(providers, h.Domain)
		}
	}
	for _, p := range tui.hcProviders {
		if !seenDomains[p] {
			seenDomains[p] = true
			providers = append(providers, p)
		}
	}

	crackedCount := 0
	testedCount := int64(0)
	for _, record := range uncracked {
		if ctx.Err() != nil {
			return
		}
		if _, exists := crackedMap[record.Hash]; exists {
			continue
		}

		emailPatterns := generateEmailPatterns(record.Username, record.DisplayName, record.Domain, tui.hcFirstNames)
		found := false
		for _, pattern := range emailPatterns {
			if found || ctx.Err() != nil {
				break
			}
			for _, provider := range providers {
				email := pattern + "@" + provider
				testedCount++

				md5Hash := hashEmail(email, "md5")
				sha256Hash := hashEmail(email, "sha256")
				targetHash := strings.ToLower(record.Hash)

				if md5Hash == targetHash || sha256Hash == targetHash {
					crackedCount++
					crackedMap[record.Hash] = email
					crackedHashType := "md5"
					if sha256Hash == targetHash {
						crackedHashType = "sha256"
					}
					tui.addOutput(fmt.Sprintf("[CRACKED] %s -> %s (%s)", record.Hash[:16]+"...", email, crackedHashType))
					tui.db.InsertCracked(record.Hash, email, record.Domain, record.Username, crackedHashType)
					tui.writeLog("CRACKED: %s -> %s", record.Hash, email)
					tui.Render()
					found = true
					break
				}
			}
		}
	}

	tui.addOutput("")
	tui.addOutput(fmt.Sprintf("[PHASE 3] Complete: %d hashes cracked (%d tested)", crackedCount, testedCount))
	tui.addOutput("")

	// Summary
	tui.pipelinePhase = "complete"
	tui.addOutput("[PIPELINE] ========================================")
	tui.addOutput("[PIPELINE] PIPELINE COMPLETE — SUMMARY")
	tui.addOutput("[PIPELINE] ========================================")
	tui.addOutput(fmt.Sprintf("    Domains checked: %d", len(targets)))
	tui.addOutput(fmt.Sprintf("    WordPress found: %d", len(wpSites)))
	tui.addOutput(fmt.Sprintf("    New hashes harvested: %d", totalNewHashes))
	tui.addOutput(fmt.Sprintf("    Hashes cracked: %d", crackedCount))
	tui.addOutput(fmt.Sprintf("    Emails tested: %d", testedCount))
	tui.writeLog("Pipeline complete: checked=%d, wp=%d, hashes=%d, cracked=%d", len(targets), len(wpSites), totalNewHashes, crackedCount)
}

func (tui *TUI) runDetectHarvest(ctx context.Context, filePath string) {
	tui.startLog("pipeline-detect-harvest")
	defer tui.closeLog()

	tui.pipelinePhase = "detect"
	tui.addOutput("[PIPELINE] Detect WordPress -> Harvest Hashes")
	tui.addOutput("")

	data, err := os.ReadFile(filePath)
	if err != nil {
		tui.addOutput(fmt.Sprintf("[-] Cannot read file: %v", err))
		return
	}

	rawLines := strings.Split(string(data), "\n")
	var targets []string
	for _, line := range rawLines {
		line = strings.TrimSpace(line)
		if line != "" && !strings.HasPrefix(line, "#") {
			targets = append(targets, line)
		}
	}

	if len(targets) == 0 {
		tui.addOutput("[-] No targets found")
		return
	}

	tui.addOutput(fmt.Sprintf("[PHASE 1] Checking %d domains for WordPress...", len(targets)))
	tui.Render()

	var wpSites []string
	for i, target := range targets {
		if ctx.Err() != nil {
			return
		}
		result := tui.checker.Check(ctx, target)
		if ctx.Err() != nil {
			return
		}
		tui.db.UpsertSiteResult(result)
		if result.IsWordPress {
			wpSites = append(wpSites, result.Domain)
			tui.addOutput(fmt.Sprintf("[WP] [%d/%d] %s", i+1, len(targets), result.Domain))
		}
		tui.Render()
	}

	tui.addOutput(fmt.Sprintf("[PHASE 1] %d WordPress sites found", len(wpSites)))
	tui.addOutput("")

	if len(wpSites) == 0 {
		tui.pipelinePhase = "complete"
		return
	}

	tui.pipelinePhase = "harvest"
	tui.addOutput(fmt.Sprintf("[PHASE 2] Harvesting hashes from %d sites...", len(wpSites)))
	tui.Render()

	totalNew := 0
	for i, site := range wpSites {
		if ctx.Err() != nil {
			return
		}
		tui.addOutput(fmt.Sprintf("[*] [%d/%d] Scanning: %s", i+1, len(wpSites), site))
		result := tui.hunter.Scan(ctx, site)
		if ctx.Err() != nil {
			return
		}
		newHashes := 0
		for _, user := range result.Users {
			if tui.db.InsertHashIfNew(result.Domain, user.Hash, user.Username, "", user.AvatarURL, user.HashType) {
				newHashes++
			}
		}
		totalNew += newHashes
		tui.db.UpdateDomain(result.Domain, result.Status, len(result.Users))
		if len(result.Users) > 0 {
			tui.addOutput(fmt.Sprintf("[+] %s: %d hashes (%d new)", result.Domain, len(result.Users), newHashes))
		}
		tui.Render()
	}

	tui.pipelinePhase = "complete"
	tui.addOutput("")
	tui.addOutput(fmt.Sprintf("[PIPELINE] Done: %d WP sites, %d new hashes", len(wpSites), totalNew))
}

func (tui *TUI) runHarvestCrack(ctx context.Context) {
	tui.startLog("pipeline-harvest-crack")
	defer tui.closeLog()

	// Phase 1: Harvest from confirmed WP sites not yet scanned
	tui.pipelinePhase = "harvest"
	wpSites := tui.db.GetWPSitesNotScanned()

	if len(wpSites) > 0 {
		tui.addOutput(fmt.Sprintf("[PHASE 1] Harvesting hashes from %d unscanned WP sites...", len(wpSites)))
		tui.Render()

		for i, site := range wpSites {
			if ctx.Err() != nil {
				return
			}
			tui.addOutput(fmt.Sprintf("[*] [%d/%d] Scanning: %s", i+1, len(wpSites), site))
			result := tui.hunter.Scan(ctx, site)
			if ctx.Err() != nil {
				return
			}
			newHashes := 0
			for _, user := range result.Users {
				if tui.db.InsertHashIfNew(result.Domain, user.Hash, user.Username, "", user.AvatarURL, user.HashType) {
					newHashes++
				}
			}
			tui.db.UpdateDomain(result.Domain, result.Status, len(result.Users))
			if newHashes > 0 {
				tui.addOutput(fmt.Sprintf("[+] %s: %d new hashes", result.Domain, newHashes))
			}
			tui.Render()
		}
		tui.addOutput("")
	} else {
		tui.addOutput("[*] No unscanned WP sites — skipping harvest phase")
	}

	// Phase 2: Crack uncracked hashes
	tui.pipelinePhase = "crack"
	uncracked := tui.db.GetUncracked()

	if len(uncracked) == 0 {
		tui.addOutput("[*] No uncracked hashes — nothing to crack")
		tui.pipelinePhase = "complete"
		return
	}

	tui.addOutput(fmt.Sprintf("[PHASE 2] Cracking %d uncracked hashes...", len(uncracked)))
	tui.Render()

	crackedMap := tui.db.LoadCrackedHashes()
	providers := make([]string, 0, len(tui.hcProviders))
	seenDomains := make(map[string]bool)
	for _, h := range uncracked {
		if !seenDomains[h.Domain] && h.Domain != "" {
			seenDomains[h.Domain] = true
			providers = append(providers, h.Domain)
		}
	}
	for _, p := range tui.hcProviders {
		if !seenDomains[p] {
			seenDomains[p] = true
			providers = append(providers, p)
		}
	}

	crackedCount := 0
	for _, record := range uncracked {
		if ctx.Err() != nil {
			return
		}
		if _, exists := crackedMap[record.Hash]; exists {
			continue
		}

		emailPatterns := generateEmailPatterns(record.Username, record.DisplayName, record.Domain, tui.hcFirstNames)
		found := false
		for _, pattern := range emailPatterns {
			if found || ctx.Err() != nil {
				break
			}
			for _, provider := range providers {
				email := pattern + "@" + provider
				md5Hash := hashEmail(email, "md5")
				sha256Hash := hashEmail(email, "sha256")
				targetHash := strings.ToLower(record.Hash)
				if md5Hash == targetHash || sha256Hash == targetHash {
					crackedCount++
					crackedMap[record.Hash] = email
					crackedHashType := "md5"
					if sha256Hash == targetHash {
						crackedHashType = "sha256"
					}
					tui.addOutput(fmt.Sprintf("[CRACKED] %s -> %s", record.Hash[:16]+"...", email))
					tui.db.InsertCracked(record.Hash, email, record.Domain, record.Username, crackedHashType)
					tui.writeLog("CRACKED: %s -> %s", record.Hash, email)
					tui.Render()
					found = true
					break
				}
			}
		}
	}

	tui.pipelinePhase = "complete"
	tui.addOutput("")
	tui.addOutput(fmt.Sprintf("[PIPELINE] Done: %d hashes cracked", crackedCount))
}

func (tui *TUI) showCrossModuleIntel() {
	tui.addOutput("")
	tui.addOutput("[*] ========================================")
	tui.addOutput("[*] CROSS-MODULE INTELLIGENCE REPORT")
	tui.addOutput("[*] ========================================")
	tui.addOutput("")

	// WP Determiner stats
	totalSites, wpSites, notWP, siteErrors := tui.db.GetSiteStats()
	tui.addOutput("[*] --- WordPress Determiner ---")
	tui.addOutput(fmt.Sprintf("    Total sites checked: %d", totalSites))
	tui.addOutput(fmt.Sprintf("    WordPress confirmed: %d", wpSites))
	tui.addOutput(fmt.Sprintf("    Not WordPress: %d", notWP))
	tui.addOutput(fmt.Sprintf("    Errors: %d", siteErrors))
	tui.addOutput("")

	// Hash Hunter stats
	totalHashes := tui.db.GetTotalHashes()
	totalDomains := tui.db.GetTotalDomains()
	incomplete := tui.db.GetIncompleteCount()
	tui.addOutput("[*] --- Hash Hunter ---")
	tui.addOutput(fmt.Sprintf("    Domains scanned: %d", totalDomains))
	tui.addOutput(fmt.Sprintf("    Total hashes: %d", totalHashes))
	tui.addOutput(fmt.Sprintf("    Incomplete domains: %d", incomplete))
	tui.addOutput("")

	// Hash Cracker stats
	_, crackedTotal := tui.db.GetCrackedStats()
	tui.addOutput("[*] --- Hash Cracker ---")
	tui.addOutput(fmt.Sprintf("    Cracked hashes: %d", crackedTotal))
	tui.addOutput("")

	// Cross-module opportunities
	wpNotScanned := tui.db.GetWPSitesNotScanned()
	uncracked := tui.db.GetUncracked()

	tui.addOutput("[*] --- Actionable Intelligence ---")
	if len(wpNotScanned) > 0 {
		tui.addOutput(fmt.Sprintf("[!] %d WordPress sites not yet scanned by Hash Hunter:", len(wpNotScanned)))
		limit := len(wpNotScanned)
		if limit > 10 {
			limit = 10
		}
		for i := 0; i < limit; i++ {
			tui.addOutput(fmt.Sprintf("    -> %s", wpNotScanned[i]))
		}
		if len(wpNotScanned) > 10 {
			tui.addOutput(fmt.Sprintf("    ... and %d more", len(wpNotScanned)-10))
		}
		tui.addOutput("    TIP: Use 'detect-harvest' or 'harvest-crack' pipeline")
	} else {
		tui.addOutput("[+] All WordPress sites have been scanned by Hash Hunter")
	}
	tui.addOutput("")

	if len(uncracked) > 0 {
		tui.addOutput(fmt.Sprintf("[!] %d hashes remain uncracked", len(uncracked)))
		tui.addOutput("    TIP: Use 'harvest-crack' pipeline or switch to Hash Cracker module")
	} else if totalHashes > 0 {
		tui.addOutput("[+] All hashes have been cracked!")
	} else {
		tui.addOutput("[*] No hashes in database yet")
	}
	tui.addOutput("")
}

func (tui *TUI) exportAll() {
	filename := filepath.Join(tui.logsDir, fmt.Sprintf("terminator_export_%s.csv", time.Now().Format("2006-01-02_150405")))
	file, err := os.Create(filename)
	if err != nil {
		tui.addOutput(fmt.Sprintf("[-] Error: %v", err))
		return
	}
	defer file.Close()

	// Section 1: Sites
	file.WriteString("=== SITES ===\n")
	file.WriteString("domain,is_wordpress,method,status_code,error,last_checked\n")
	rows, err := tui.db.conn.Query("SELECT domain, is_wordpress, method, status_code, error, last_checked FROM sites ORDER BY domain")
	if err == nil {
		for rows.Next() {
			var domain, method, errStr, lastChecked string
			var isWP, statusCode int
			rows.Scan(&domain, &isWP, &method, &statusCode, &errStr, &lastChecked)
			file.WriteString(fmt.Sprintf("%s,%d,%s,%d,%s,%s\n", domain, isWP, method, statusCode, errStr, lastChecked))
		}
		rows.Close()
	}

	// Section 2: Hashes
	file.WriteString("\n=== HASHES ===\n")
	file.WriteString("domain,hash,username,hash_type,discovered\n")
	rows2, err := tui.db.conn.Query("SELECT domain, hash, username, hash_type, discovered FROM hashes ORDER BY domain")
	if err == nil {
		for rows2.Next() {
			var domain, hash, username, hashType, discovered string
			rows2.Scan(&domain, &hash, &username, &hashType, &discovered)
			file.WriteString(fmt.Sprintf("%s,%s,%s,%s,%s\n", domain, hash, username, hashType, discovered))
		}
		rows2.Close()
	}

	// Section 3: Cracked
	file.WriteString("\n=== CRACKED ===\n")
	file.WriteString("email,hash,domain,username,method,cracked_at\n")
	rows3, err := tui.db.conn.Query("SELECT email, hash, domain, username, method, cracked_at FROM cracked ORDER BY cracked_at DESC")
	if err == nil {
		for rows3.Next() {
			var email, hash, domain, username, method, crackedAt string
			rows3.Scan(&email, &hash, &domain, &username, &method, &crackedAt)
			file.WriteString(fmt.Sprintf("%s,%s,%s,%s,%s,%s\n", email, hash, domain, username, method, crackedAt))
		}
		rows3.Close()
	}

	// Count totals
	totalSites, _, _, _ := tui.db.GetSiteStats()
	totalHashes := tui.db.GetTotalHashes()
	_, totalCracked := tui.db.GetCrackedStats()
	tui.addOutput(fmt.Sprintf("[+] Exported to %s", filename))
	tui.addOutput(fmt.Sprintf("    Sites: %d | Hashes: %d | Cracked: %d", totalSites, totalHashes, totalCracked))
}

// ===========================================================================
// INPUT HANDLING
// ===========================================================================

func (tui *TUI) handleModuleSelectInput(ev *tcell.EventKey) bool {
	switch ev.Key() {
	case tcell.KeyEscape, tcell.KeyTab:
		tui.showModuleSelect = false
		return true
	case tcell.KeyUp:
		if tui.moduleSelected > 0 {
			tui.moduleSelected--
		}
		return true
	case tcell.KeyDown:
		if tui.moduleSelected < 8 {
			tui.moduleSelected++
		}
		return true
	case tcell.KeyEnter:
		tui.switchToModule(ActiveModule(tui.moduleSelected))
		return true
	}

	switch ev.Rune() {
	case '1':
		tui.switchToModule(ModuleWPDeterminer)
		return true
	case '2':
		tui.switchToModule(ModuleHashHunter)
		return true
	case '3':
		tui.switchToModule(ModuleHashCracker)
		return true
	case '4':
		tui.switchToModule(ModulePipeline)
		return true
	case '5':
		tui.switchToModule(ModuleHuntr)
		return true
	case '6':
		tui.switchToModule(ModuleODINT)
		return true
	case '7':
		tui.switchToModule(ModulePortScanner)
		return true
	case '8':
		tui.switchToModule(ModuleSilentEye)
		return true
	case '9':
		tui.switchToModule(ModuleTerminator)
		return true
	case 'q', 'Q':
		tui.showModuleSelect = false
		return true
	}

	return true
}

func (tui *TUI) getMaxMenuKey() rune {
	items := tui.menuItems
	maxKey := '0'
	for _, item := range items {
		if len(item.Key) == 1 {
			r := rune(item.Key[0])
			if r > maxKey && r != 'Q' && r != 'q' {
				maxKey = r
			}
		}
	}
	return maxKey
}

func (tui *TUI) isValidMenuKey(r rune) bool {
	for _, item := range tui.menuItems {
		if len(item.Key) == 1 && rune(item.Key[0]) == r {
			return true
		}
	}
	return false
}

func (tui *TUI) handleMenuInput(ev *tcell.EventKey) bool {
	// Tab opens module selector
	if ev.Key() == tcell.KeyTab {
		tui.showModuleSelect = true
		tui.moduleSelected = int(tui.activeModule)
		return true
	}

	// Right arrow moves focus to output panel
	if ev.Key() == tcell.KeyRight && !tui.focusOutput {
		tui.focusOutput = true
		return true
	}

	// Left arrow returns focus to menu
	if ev.Key() == tcell.KeyLeft && tui.focusOutput {
		tui.focusOutput = false
		return true
	}
	if ev.Key() == tcell.KeyEscape {
		if tui.focusOutput {
			tui.focusOutput = false
		}
		return true
	}

	if tui.focusOutput {
		switch ev.Key() {
		case tcell.KeyUp:
			tui.scrollOutput(-1)
		case tcell.KeyDown:
			tui.scrollOutput(1)
		case tcell.KeyPgUp:
			tui.scrollOutput(-10)
		case tcell.KeyPgDn:
			tui.scrollOutput(10)
		case tcell.KeyHome:
			tui.scrollOutputTo("top")
		case tcell.KeyEnd:
			tui.scrollOutputTo("bottom")
		}
		switch ev.Rune() {
		case 'q', 'Q':
			return false
		default:
			if tui.isValidMenuKey(ev.Rune()) {
				tui.focusOutput = false
				tui.startCommand(string(ev.Rune()))
			}
		}
		return true
	}

	// Menu panel focused
	switch ev.Key() {
	case tcell.KeyF1:
		tui.openSettingsMenu()
		return true
	case tcell.KeyUp:
		if tui.selectedItem > 0 {
			tui.selectedItem--
		}
	case tcell.KeyDown:
		if tui.selectedItem < len(tui.menuItems)-1 {
			tui.selectedItem++
		}
	case tcell.KeyPgUp:
		tui.scrollOutput(-5)
	case tcell.KeyPgDn:
		tui.scrollOutput(5)
	case tcell.KeyEnter:
		tui.startCommand(tui.menuItems[tui.selectedItem].Key)
	}

	switch ev.Rune() {
	case 'q', 'Q':
		return false
	default:
		if tui.isValidMenuKey(ev.Rune()) {
			tui.startCommand(string(ev.Rune()))
		}
	}

	return true
}

func (tui *TUI) handleTextInput(ev *tcell.EventKey) bool {
	switch ev.Key() {
	case tcell.KeyEscape:
		tui.commandState = StateMenu
		tui.inputBuffer = ""
		tui.currentCmd = ""
		return true
	case tcell.KeyEnter:
		if len(tui.inputFields) > 0 && tui.currentField < len(tui.inputFields) && len(tui.inputBuffer) > 0 {
			tui.collectedInputs[tui.inputFields[tui.currentField]] = tui.inputBuffer
			tui.addOutput(fmt.Sprintf("  > %s: %s", tui.inputFields[tui.currentField], tui.inputBuffer))
			tui.currentField++
			tui.inputBuffer = ""
			tui.inputCursor = 0

			if tui.currentField >= len(tui.inputFields) {
				tui.executeCommand()
			}
		}
		return true
	case tcell.KeyBackspace, tcell.KeyBackspace2:
		if len(tui.inputBuffer) > 0 && tui.inputCursor > 0 {
			tui.inputBuffer = tui.inputBuffer[:tui.inputCursor-1] + tui.inputBuffer[tui.inputCursor:]
			tui.inputCursor--
		}
		return true
	case tcell.KeyDelete:
		if tui.inputCursor < len(tui.inputBuffer) {
			tui.inputBuffer = tui.inputBuffer[:tui.inputCursor] + tui.inputBuffer[tui.inputCursor+1:]
		}
		return true
	case tcell.KeyLeft:
		if tui.inputCursor > 0 {
			tui.inputCursor--
		}
		return true
	case tcell.KeyRight:
		if tui.inputCursor < len(tui.inputBuffer) {
			tui.inputCursor++
		}
		return true
	case tcell.KeyHome:
		tui.inputCursor = 0
		return true
	case tcell.KeyEnd:
		tui.inputCursor = len(tui.inputBuffer)
		return true
	case tcell.KeyCtrlV:
		clipboard := getClipboard()
		if clipboard != "" {
			tui.inputBuffer = tui.inputBuffer[:tui.inputCursor] + clipboard + tui.inputBuffer[tui.inputCursor:]
			tui.inputCursor += len(clipboard)
		}
		return true
	case tcell.KeyCtrlU:
		tui.inputBuffer = ""
		tui.inputCursor = 0
		return true
	case tcell.KeyRune:
		tui.inputBuffer = tui.inputBuffer[:tui.inputCursor] + string(ev.Rune()) + tui.inputBuffer[tui.inputCursor:]
		tui.inputCursor++
		return true
	}

	return true
}

func (tui *TUI) handleSettingsInput(ev *tcell.EventKey) {
	switch ev.Key() {
	case tcell.KeyEscape:
		tui.commandState = StateMenu
	case tcell.KeyUp:
		if tui.selectedSetting > 0 {
			tui.selectedSetting--
		}
	case tcell.KeyDown:
		if tui.selectedSetting < 1 {
			tui.selectedSetting++
		}
	case tcell.KeyEnter:
		tui.editSelectedSetting()
	}
}

func (tui *TUI) handleFilePickerInput(ev *tcell.EventKey) {
	switch ev.Key() {
	case tcell.KeyEscape:
		tui.commandState = StateMenu
	case tcell.KeyUp:
		if tui.pickerSelected > 0 {
			tui.pickerSelected--
		}
	case tcell.KeyDown:
		if tui.pickerSelected < len(tui.pickerFiles)-1 {
			tui.pickerSelected++
		}
	case tcell.KeyEnter:
		if len(tui.pickerFiles) > 0 {
			selected := tui.pickerFiles[tui.pickerSelected]
			tui.collectedInputs = make(map[string]string)
			tui.collectedInputs["file"] = selected
			tui.addOutput(fmt.Sprintf("  > file: %s", filepath.Base(selected)))

			if tui.activeModule == ModulePipeline {
				tui.collectedInputs["file"] = selected
				tui.executePipelineCommand()
				return
			}

			if tui.activeModule == ModuleTerminator {
				tui.currentCmd = "1"
				tui.commandState = StateRunning
				tui.Render()
				ctx, cancel := context.WithCancel(context.Background())
				tui.cancelScan = cancel
				go func() {
					defer func() { tui.cancelScan = nil }()
					tui.runTerminatorMode(ctx, selected)
					if ctx.Err() == nil {
						tui.commandState = StateComplete
					}
					tui.Render()
				}()
				return
			}

			tui.currentCmd = "2"
			tui.commandState = StateRunning
			tui.Render()

			ctx, cancel := context.WithCancel(context.Background())
			tui.cancelScan = cancel
			go func() {
				defer func() { tui.cancelScan = nil }()
				switch tui.activeModule {
				case ModuleHashHunter:
					tui.runBulkScan(ctx, selected)
				case ModuleHuntr:
					tui.runHuntrBulkFromFile(ctx, selected)
				case ModuleODINT:
					tui.runOdintBulkFromFile(ctx, selected)
				case ModulePortScanner:
					tui.portBulkFromFile(ctx, selected)
				case ModuleSilentEye:
					tui.runSilentBulkFromFile(ctx, selected)
				default:
					tui.runBulkCheck(ctx, selected)
				}
				if ctx.Err() == nil {
					tui.commandState = StateComplete
				}
				tui.Render()
			}()
		}
	}
}

// ===========================================================================
// TERMINATOR MODE — ALL MODULES SEQUENTIAL
// ===========================================================================

func (tui *TUI) startTerminatorCommand(cmdKey string) {
	switch cmdKey {
	case "1": // Full Terminator run
		// Gather files from both lists/ and targets/ directories
		var allFiles []string
		seen := make(map[string]bool)

		for _, dir := range []string{tui.listsDir, tui.targetsDir} {
			for _, ext := range []string{"*.txt", "*.csv", "*.list"} {
				matches, _ := filepath.Glob(filepath.Join(dir, ext))
				for _, f := range matches {
					if !seen[f] {
						seen[f] = true
						allFiles = append(allFiles, f)
					}
				}
			}
		}

		if len(allFiles) > 0 {
			tui.pickerFiles = allFiles
			tui.pickerSelected = 0
			tui.commandState = StateFilePicker
			return
		}
		tui.addOutput("[-] No target files found (.txt/.csv/.list)")
		tui.addOutput("[*] Place files in: " + tui.listsDir)
		tui.addOutput("[*]             or: " + tui.targetsDir)
		return

	case "2": // Combined stats
		tui.showTerminatorStats()
		return

	case "3": // Clear
		tui.clearOutput()
		tui.addOutput("[*] Output cleared")
		return

	case "Q", "q":
		tui.running = false
		return
	}
}

func (tui *TUI) showTerminatorStats() {
	tui.addOutput("")
	tui.addOutput("[TERMINATOR] ══════════════════════════════════════")
	tui.addOutput("[TERMINATOR]        COMBINED STATISTICS")
	tui.addOutput("[TERMINATOR] ══════════════════════════════════════")
	tui.addOutput("")

	// ODINT stats
	var odintScans, odintTechs, odintFindings int
	tui.db.conn.QueryRow("SELECT COUNT(*) FROM odint_scans").Scan(&odintScans)
	tui.db.conn.QueryRow("SELECT COUNT(*) FROM odint_technologies").Scan(&odintTechs)
	tui.db.conn.QueryRow("SELECT COUNT(*) FROM odint_findings").Scan(&odintFindings)
	tui.addOutput(fmt.Sprintf("  [ODINT]        %d scans | %d techs | %d findings", odintScans, odintTechs, odintFindings))

	// Huntr stats
	var huntrFindings int
	tui.db.conn.QueryRow("SELECT COUNT(*) FROM huntr_findings").Scan(&huntrFindings)
	tui.addOutput(fmt.Sprintf("  [HUNTR]        %d credential findings", huntrFindings))

	// Port Scanner stats
	var portTargets int
	tui.db.conn.QueryRow("SELECT COUNT(*) FROM port_targets").Scan(&portTargets)
	var openPorts int
	tui.db.conn.QueryRow("SELECT COUNT(*) FROM port_results WHERE state='open'").Scan(&openPorts)
	tui.addOutput(fmt.Sprintf("  [PORT SCANNER] %d targets | %d open ports", portTargets, openPorts))

	// Silent Eye stats
	var silentTargets int
	tui.db.conn.QueryRow("SELECT COUNT(*) FROM silent_targets").Scan(&silentTargets)
	var silentSubs int
	tui.db.conn.QueryRow("SELECT COUNT(*) FROM silent_subdomains").Scan(&silentSubs)
	tui.addOutput(fmt.Sprintf("  [SILENT EYE]   %d targets | %d subdomains", silentTargets, silentSubs))

	// Pipeline / WP stats
	totalSites, wpSites, _, _ := tui.db.GetSiteStats()
	totalHashes := tui.db.GetTotalHashes()
	_, cracked := tui.db.GetCrackedStats()
	tui.addOutput(fmt.Sprintf("  [PIPELINE]     %d sites | %d WP | %d hashes | %d cracked", totalSites, wpSites, totalHashes, cracked))

	tui.addOutput("")
	tui.addOutput("[TERMINATOR] ══════════════════════════════════════")
}

func (tui *TUI) renderTerminatorDashboard() {
	modColor := moduleColors[ModuleTerminator]
	borderStyle := tcell.StyleDefault.Foreground(modColor)
	titleStyle := tcell.StyleDefault.Foreground(ColorPrimary).Bold(true)
	textStyle := tcell.StyleDefault.Foreground(ColorText)
	highlightStyle := tcell.StyleDefault.Foreground(modColor)
	successStyle := tcell.StyleDefault.Foreground(ColorSuccess)
	dimStyle := tcell.StyleDefault.Foreground(ColorDim)
	infoStyle := tcell.StyleDefault.Foreground(ColorInfo)
	warnStyle := tcell.StyleDefault.Foreground(ColorWarning)

	menuWidth := 52
	outputX := menuWidth + 1
	outputWidth := tui.width - menuWidth - 2
	inputHeight := 3
	outputHeight := tui.height - inputHeight - 2

	// Header
	header := fmt.Sprintf(" MOTHERFUCKIN TERMINATOR BOT 9000 v%s | TERMINATOR MODE ", Version)
	tui.drawString(1, 0, header, titleStyle)

	// Combined stats in header
	totalSites, wpSites, _, _ := tui.db.GetSiteStats()
	totalHashes := tui.db.GetTotalHashes()
	_, totalCracked := tui.db.GetCrackedStats()
	statsStr := fmt.Sprintf("Sites: %d | WP: %d | Hashes: %d | Cracked: %d ", totalSites, wpSites, totalHashes, totalCracked)
	tui.drawString(tui.width-len(statsStr)-1, 0, statsStr, dimStyle)

	// Menu box
	tui.drawBox(0, 1, menuWidth, tui.height-inputHeight-1, "TERMINATOR COMMANDS", borderStyle)

	menuY := 3
	items := tui.menuItems
	for i, item := range items {
		y := menuY + i
		if y >= tui.height-inputHeight-2 {
			break
		}
		keyStyle := highlightStyle
		nameStyle := textStyle
		if i == tui.selectedItem && !tui.focusOutput {
			tui.fillRect(1, y, menuWidth-2, 1, ' ', tcell.StyleDefault.Background(ColorBorder))
			keyStyle = successStyle.Bold(true).Background(ColorBorder)
			nameStyle = tcell.StyleDefault.Foreground(ColorText).Bold(true).Background(ColorBorder)
		}
		tui.drawString(2, y, fmt.Sprintf("[%s]", item.Key), keyStyle)
		tui.drawStringClipped(6, y, menuWidth-8, item.Name+" - "+item.Desc, nameStyle)
	}

	// Execution order panel
	phaseY := menuY + len(items) + 1
	tui.drawString(2, phaseY, "--- Execution Order ---", dimStyle)

	type phaseInfo struct {
		num   string
		name  string
		desc  string
		color tcell.Color
	}
	phases := []phaseInfo{
		{"1", "ODINT", "Domain recon (DNS, SSL, techs, CVEs)", moduleColors[ModuleODINT]},
		{"2", "Huntr", "Credential file hunting", moduleColors[ModuleHuntr]},
		{"3", "Port Scanner", "Network port scanning", moduleColors[ModulePortScanner]},
		{"4", "Silent Eye", "Passive OSINT (crt.sh, Shodan, VT)", moduleColors[ModuleSilentEye]},
		{"5", "Pipeline", "WP detect + hash harvest + crack", moduleColors[ModulePipeline]},
	}

	for i, p := range phases {
		py := phaseY + 1 + i
		if py >= tui.height-inputHeight-2 {
			break
		}

		// Determine phase status indicator
		indicator := "  "
		indicatorStyle := dimStyle
		currentPhaseNum := ""
		if tui.terminatorPhase != "" {
			// Extract phase number from terminatorPhase like "1/5 ODINT"
			if len(tui.terminatorPhase) > 0 {
				currentPhaseNum = string(tui.terminatorPhase[0])
			}
		}

		if tui.terminatorPhase == "COMPLETE" {
			indicator = "[DONE]"
			indicatorStyle = tcell.StyleDefault.Foreground(ColorSuccess)
		} else if tui.terminatorPhase == "CANCELLED" {
			if p.num <= currentPhaseNum {
				indicator = "[STOP]"
				indicatorStyle = tcell.StyleDefault.Foreground(ColorDanger)
			} else {
				indicator = "[SKIP]"
				indicatorStyle = tcell.StyleDefault.Foreground(ColorDim)
			}
		} else if currentPhaseNum == p.num {
			indicator = "[>>]"
			indicatorStyle = tcell.StyleDefault.Foreground(modColor).Bold(true)
		} else if currentPhaseNum != "" && p.num < currentPhaseNum {
			indicator = "[OK]"
			indicatorStyle = tcell.StyleDefault.Foreground(ColorSuccess)
		}

		tui.drawString(2, py, indicator, indicatorStyle)
		tui.drawString(9, py, fmt.Sprintf("%s.", p.num), tcell.StyleDefault.Foreground(p.color))
		tui.drawString(12, py, p.name, tcell.StyleDefault.Foreground(p.color).Bold(true))
		tui.drawStringClipped(26, py, menuWidth-28, p.desc, dimStyle)
	}

	// Module stats panel
	statsY := phaseY + len(phases) + 2
	if statsY < tui.height-inputHeight-2 {
		tui.drawString(2, statsY, "--- Module Stats ---", dimStyle)
		var odintScans, huntrFindings, portTargets, silentTargets int
		tui.db.conn.QueryRow("SELECT COUNT(*) FROM odint_scans").Scan(&odintScans)
		tui.db.conn.QueryRow("SELECT COUNT(*) FROM huntr_findings").Scan(&huntrFindings)
		tui.db.conn.QueryRow("SELECT COUNT(*) FROM port_targets").Scan(&portTargets)
		tui.db.conn.QueryRow("SELECT COUNT(*) FROM silent_targets").Scan(&silentTargets)

		if statsY+1 < tui.height-inputHeight-2 {
			tui.drawString(2, statsY+1, fmt.Sprintf("ODINT: %d scans", odintScans), infoStyle)
		}
		if statsY+2 < tui.height-inputHeight-2 {
			tui.drawString(2, statsY+2, fmt.Sprintf("Huntr: %d findings", huntrFindings), infoStyle)
		}
		if statsY+3 < tui.height-inputHeight-2 {
			tui.drawString(2, statsY+3, fmt.Sprintf("Ports: %d targets", portTargets), infoStyle)
		}
		if statsY+4 < tui.height-inputHeight-2 {
			tui.drawString(2, statsY+4, fmt.Sprintf("Silent Eye: %d targets", silentTargets), infoStyle)
		}
	}

	// Shortcuts
	shortcutY := statsY + 6
	if shortcutY < tui.height-inputHeight-2 {
		tui.drawString(2, shortcutY, "--- Shortcuts ---", dimStyle)
		if shortcutY+1 < tui.height-inputHeight-2 {
			tui.drawString(2, shortcutY+1, "F1: Settings  Tab: Module Switch", infoStyle)
		}
		if shortcutY+2 < tui.height-inputHeight-2 {
			tui.drawString(2, shortcutY+2, "ESC: Cancel / Back", infoStyle)
		}
		if shortcutY+3 < tui.height-inputHeight-2 {
			tui.drawString(2, shortcutY+3, "PgUp/PgDn: Scroll", infoStyle)
		}
	}

	// Output box
	outputTitle := "OUTPUT"
	outputBorder := borderStyle
	if tui.focusOutput {
		outputTitle = "OUTPUT [FOCUSED]"
		outputBorder = tcell.StyleDefault.Foreground(ColorAccent)
	}
	tui.drawBox(outputX, 1, outputWidth, outputHeight, outputTitle, outputBorder)

	tui.outputMutex.Lock()
	maxLines := outputHeight - 2
	if len(tui.outputLines) == 0 {
		tui.drawStringClipped(outputX+2, 3, outputWidth-4, "[*] TERMINATOR MODE ready — select [1] to load targets", infoStyle)
	} else {
		startLine := tui.outputScroll
		for i := 0; i < maxLines && startLine+i < len(tui.outputLines); i++ {
			line := tui.outputLines[startLine+i]
			y := 2 + i
			lineStyle := tcell.StyleDefault.Foreground(tcell.ColorWhite)
			if strings.HasPrefix(line, "[+]") || strings.HasPrefix(line, "[WP]") || strings.HasPrefix(line, "[CRACKED]") {
				lineStyle = tcell.StyleDefault.Foreground(ColorSuccess)
			} else if strings.HasPrefix(line, "[-]") || strings.HasPrefix(line, "[NOT]") {
				lineStyle = tcell.StyleDefault.Foreground(ColorDanger)
			} else if strings.HasPrefix(line, "[ERR]") {
				lineStyle = warnStyle
			} else if strings.HasPrefix(line, "[*]") {
				lineStyle = infoStyle
			} else if strings.HasPrefix(line, "[!]") || strings.HasPrefix(line, "[TERMINATOR]") {
				lineStyle = tcell.StyleDefault.Foreground(modColor).Bold(true)
			}
			tui.drawStringClipped(outputX+1, y, outputWidth-2, line, lineStyle)
		}
		if len(tui.outputLines) > maxLines {
			scrollInfo := fmt.Sprintf(" %d/%d ", tui.outputScroll+maxLines, len(tui.outputLines))
			tui.drawString(outputX+outputWidth-len(scrollInfo)-1, 1, scrollInfo, dimStyle)
		}
	}
	tui.outputMutex.Unlock()

	// Input box
	inputY := tui.height - inputHeight - 1
	tui.drawBox(0, inputY, tui.width, inputHeight+1, "INPUT", borderStyle)
	if tui.commandState == StateRunning {
		phase := tui.terminatorPhase
		if phase == "" {
			phase = "Initializing"
		}
		tui.drawString(2, inputY+1, fmt.Sprintf("[%s] Running... press ESC to cancel", phase), warnStyle)
	} else if tui.commandState == StateComplete {
		tui.drawString(2, inputY+1, "TERMINATOR complete! All 5 phases finished. Press any key.", successStyle)
	} else if tui.commandState == StateFilePicker {
		tui.drawString(2, inputY+1, "Select target file with arrows, Enter to confirm", warnStyle)
	} else {
		hint := "Tab: Module | Arrows: Navigate | Enter: Select | Q: Quit"
		tui.drawStringClipped(2, inputY+1, tui.width-4, hint, dimStyle)
	}

	// Status bar
	statusY := tui.height - 1
	statusStyle := tcell.StyleDefault.Foreground(tcell.ColorBlack).Background(modColor)
	tui.fillRect(0, statusY, tui.width, 1, ' ', statusStyle)
	phase := tui.terminatorPhase
	if phase == "" {
		phase = "Ready"
	}
	status := fmt.Sprintf(" TERMINATOR | Phase: %s | Sites: %d | WP: %d | Hashes: %d | Cracked: %d ",
		phase, totalSites, wpSites, totalHashes, totalCracked)
	tui.drawString(0, statusY, status, statusStyle)
}

func (tui *TUI) runTerminatorMode(ctx context.Context, filePath string) {
	tui.startLog("terminator")
	defer tui.closeLog()

	tui.addOutput("")
	tui.addOutput("[TERMINATOR] ╔══════════════════════════════════════════╗")
	tui.addOutput("[TERMINATOR] ║     TERMINATOR MODE — FULL ASSAULT      ║")
	tui.addOutput("[TERMINATOR] ║   ALL 5 MODULES ARMED AND SEQUENCING    ║")
	tui.addOutput("[TERMINATOR] ╚══════════════════════════════════════════╝")
	tui.addOutput(fmt.Sprintf("[TERMINATOR] Target file: %s", filepath.Base(filePath)))
	tui.addOutput("")
	tui.Render()

	// Phase 1: ODINT
	if ctx.Err() != nil {
		return
	}
	tui.terminatorPhase = "1/5 ODINT"
	tui.addOutput("[TERMINATOR] ┌──────────────────────────────────────┐")
	tui.addOutput("[TERMINATOR] │  PHASE 1/5: ODINT — Domain Recon    │")
	tui.addOutput("[TERMINATOR] └──────────────────────────────────────┘")
	tui.Render()
	tui.runOdintBulkFromFile(ctx, filePath)
	if ctx.Err() != nil {
		tui.terminatorPhase = "CANCELLED"
		tui.addOutput("[TERMINATOR] Cancelled during ODINT phase")
		return
	}
	tui.addOutput("[TERMINATOR] Phase 1 complete: ODINT finished")
	tui.addOutput("")

	// Phase 2: Port Scanner
	if ctx.Err() != nil {
		return
	}
	tui.terminatorPhase = "2/5 Ports"
	tui.addOutput("[TERMINATOR] ┌──────────────────────────────────────┐")
	tui.addOutput("[TERMINATOR] │  PHASE 2/5: PORT SCANNER — Scanning │")
	tui.addOutput("[TERMINATOR] └──────────────────────────────────────┘")
	tui.Render()
	tui.portBulkFromFile(ctx, filePath)
	if ctx.Err() != nil {
		tui.terminatorPhase = "CANCELLED"
		tui.addOutput("[TERMINATOR] Cancelled during Port Scanner phase")
		return
	}
	tui.addOutput("[TERMINATOR] Phase 2 complete: Port Scanner finished")
	tui.addOutput("")

	// Phase 3: Silent Eye
	if ctx.Err() != nil {
		return
	}
	tui.terminatorPhase = "3/5 Silent"
	tui.addOutput("[TERMINATOR] ┌──────────────────────────────────────┐")
	tui.addOutput("[TERMINATOR] │  PHASE 3/5: SILENT EYE — Passive    │")
	tui.addOutput("[TERMINATOR] └──────────────────────────────────────┘")
	tui.Render()
	tui.runSilentBulkFromFile(ctx, filePath)
	if ctx.Err() != nil {
		tui.terminatorPhase = "CANCELLED"
		tui.addOutput("[TERMINATOR] Cancelled during Silent Eye phase")
		return
	}
	tui.addOutput("[TERMINATOR] Phase 3 complete: Silent Eye finished")
	tui.addOutput("")

	// Phase 4: Pipeline
	if ctx.Err() != nil {
		return
	}
	tui.terminatorPhase = "4/5 Pipeline"
	tui.addOutput("[TERMINATOR] ┌──────────────────────────────────────┐")
	tui.addOutput("[TERMINATOR] │  PHASE 4/5: PIPELINE — WP+Hash+Crack│")
	tui.addOutput("[TERMINATOR] └──────────────────────────────────────┘")
	tui.Render()
	tui.runFullPipeline(ctx, filePath)
	if ctx.Err() != nil {
		tui.terminatorPhase = "CANCELLED"
		tui.addOutput("[TERMINATOR] Cancelled during Pipeline phase")
		return
	}
	tui.addOutput("[TERMINATOR] Phase 4 complete: Pipeline finished")
	tui.addOutput("")

	// Phase 5: Huntr
	if ctx.Err() != nil {
		return
	}
	tui.terminatorPhase = "5/5 Huntr"
	tui.addOutput("[TERMINATOR] ┌──────────────────────────────────────┐")
	tui.addOutput("[TERMINATOR] │  PHASE 5/5: HUNTR — Credential Hunt │")
	tui.addOutput("[TERMINATOR] └──────────────────────────────────────┘")
	tui.Render()
	tui.runHuntrBulkFromFile(ctx, filePath)
	if ctx.Err() != nil {
		tui.terminatorPhase = "CANCELLED"
		tui.addOutput("[TERMINATOR] Cancelled during Huntr phase")
		return
	}
	tui.addOutput("[TERMINATOR] Phase 5 complete: Huntr finished")
	tui.addOutput("")

	// Final summary
	tui.terminatorPhase = "COMPLETE"
	tui.addOutput("[TERMINATOR] ╔══════════════════════════════════════════╗")
	tui.addOutput("[TERMINATOR] ║      ALL 5 PHASES COMPLETE              ║")
	tui.addOutput("[TERMINATOR] ╚══════════════════════════════════════════╝")
	tui.addOutput("")
	tui.showTerminatorStats()
}

func (tui *TUI) isDuplicateEvent(ev *tcell.EventKey) bool {
	tui.skipNextEvent = !tui.skipNextEvent
	return !tui.skipNextEvent
}

// ===========================================================================
// EVENT LOOP
// ===========================================================================

func (tui *TUI) HandleInput() {
	for tui.running {
		ev := tui.screen.PollEvent()
		switch ev := ev.(type) {
		case *tcell.EventKey:
			if tui.isDuplicateEvent(ev) {
				continue
			}

			if tui.showSplash {
				if ev.Key() == tcell.KeyRune && ev.Rune() == ' ' {
					tui.showSplash = false
					tui.Render()
				}
				continue
			}

			// Module selector takes priority
			if tui.showModuleSelect {
				tui.handleModuleSelectInput(ev)
				tui.Render()
				continue
			}

			switch tui.commandState {
			case StateMenu:
				if !tui.handleMenuInput(ev) {
					tui.running = false
					return
				}
			case StateInput:
				if !tui.handleTextInput(ev) {
					tui.running = false
					return
				}
			case StateComplete:
				switch ev.Key() {
				case tcell.KeyRight:
					tui.focusOutput = true
				case tcell.KeyLeft:
					tui.focusOutput = false
				case tcell.KeyUp:
					if tui.focusOutput {
						tui.scrollOutput(-1)
					}
				case tcell.KeyDown:
					if tui.focusOutput {
						tui.scrollOutput(1)
					}
				case tcell.KeyPgUp:
					tui.scrollOutput(-10)
				case tcell.KeyPgDn:
					tui.scrollOutput(10)
				case tcell.KeyHome:
					tui.scrollOutputTo("top")
				case tcell.KeyEnd:
					tui.scrollOutputTo("bottom")
				case tcell.KeyTab:
					tui.showModuleSelect = true
					tui.moduleSelected = int(tui.activeModule)
				case tcell.KeyEnter, tcell.KeyEscape:
					tui.commandState = StateMenu
					tui.currentCmd = ""
					tui.focusOutput = false
				}
				switch ev.Rune() {
				case 'q', 'Q':
					tui.running = false
					return
				default:
					if tui.isValidMenuKey(ev.Rune()) {
						tui.commandState = StateMenu
						tui.currentCmd = ""
						tui.focusOutput = false
						tui.startCommand(string(ev.Rune()))
					}
				}
			case StateSettings:
				tui.handleSettingsInput(ev)
			case StateFilePicker:
				tui.handleFilePickerInput(ev)
			case StateRunning:
				switch ev.Key() {
				case tcell.KeyEscape:
					if tui.cancelScan != nil {
						tui.cancelScan()
					}
					tui.addOutput("[!] Scan cancelled by user")
					tui.commandState = StateMenu
					tui.currentCmd = ""
				case tcell.KeyUp:
					tui.scrollOutput(-1)
				case tcell.KeyDown:
					tui.scrollOutput(1)
				case tcell.KeyPgUp:
					tui.scrollOutput(-10)
				case tcell.KeyPgDn:
					tui.scrollOutput(10)
				case tcell.KeyHome:
					tui.scrollOutputTo("top")
				case tcell.KeyEnd:
					tui.scrollOutputTo("bottom")
				}
			}

		case *tcell.EventResize:
			tui.width, tui.height = tui.screen.Size()
			tui.screen.Sync()
		}

		tui.Render()
	}
}

// ===========================================================================
// RUN LOOP
// ===========================================================================

func (tui *TUI) Run() {
	tui.screen.Clear()
	tui.Render()

	go tui.HandleInput()

	ticker := time.NewTicker(50 * time.Millisecond)
	defer ticker.Stop()

	for tui.running {
		<-ticker.C

		if tui.showSplash && tui.splashFrame >= 2.5 {
			tui.showSplash = false
		}

		tui.Render()
	}
}

// ===========================================================================
// DB PATH RESOLUTION
// ===========================================================================

func resolveDBPath() string {
	// Step 1: Check for platform subfolder next to executable
	exePath, _ := os.Executable()
	baseDir := filepath.Dir(exePath)

	var platformDir string
	if runtime.GOOS == "windows" {
		platformDir = filepath.Join(baseDir, "Windows")
	} else {
		platformDir = filepath.Join(baseDir, "Linux")
	}

	if info, err := os.Stat(platformDir); err == nil && info.IsDir() {
		return filepath.Join(platformDir, "terminator.db")
	}

	// Step 2: Check for recon-suite parent
	reconDB := filepath.Join(baseDir, "..", "terminator.db")
	if _, err := os.Stat(filepath.Dir(reconDB)); err == nil {
		absRecon, _ := filepath.Abs(reconDB)
		_ = absRecon
	}

	// Step 3: Default — next to executable
	return filepath.Join(baseDir, "terminator.db")
}

// ===========================================================================
// MAIN
// ===========================================================================

func main() {
	log.SetOutput(io.Discard)

	if os.Getenv("TERM") == "" {
		os.Setenv("TERM", "xterm-256color")
	}

	dbPath := resolveDBPath()
	workers := 100
	timeout := 10 * time.Second

	// Parse CLI args
	startModule := ""
	for i, arg := range os.Args {
		if arg == "-db" && i+1 < len(os.Args) {
			dbPath = os.Args[i+1]
		}
		if arg == "-w" && i+1 < len(os.Args) {
			if v, err := strconv.Atoi(os.Args[i+1]); err == nil && v > 0 {
				workers = v
			}
		}
		if arg == "-t" && i+1 < len(os.Args) {
			if v, err := strconv.Atoi(os.Args[i+1]); err == nil && v > 0 {
				timeout = time.Duration(v) * time.Second
			}
		}
		if (arg == "-m" || arg == "--module") && i+1 < len(os.Args) {
			startModule = strings.ToLower(os.Args[i+1])
		}
	}

	tui, err := NewTUI(dbPath, workers, timeout)
	if err != nil {
		fmt.Printf("Failed to initialize TUI: %v\n", err)
		os.Exit(1)
	}
	defer tui.Close()

	// Skip splash and jump to specific module if requested
	if startModule != "" {
		tui.showSplash = false
		switch startModule {
		case "wp", "wordpress", "1":
			tui.switchToModule(ModuleWPDeterminer)
		case "hh", "hunter", "hash-hunter", "2":
			tui.switchToModule(ModuleHashHunter)
		case "hc", "cracker", "hash-cracker", "3":
			tui.switchToModule(ModuleHashCracker)
		case "pipeline", "pipe", "4":
			tui.switchToModule(ModulePipeline)
		case "huntr", "5":
			tui.switchToModule(ModuleHuntr)
		case "odint", "toolkit", "6":
			tui.switchToModule(ModuleODINT)
		case "port", "port-scanner", "7":
			tui.switchToModule(ModulePortScanner)
		case "silent", "silent-eye", "8":
			tui.switchToModule(ModuleSilentEye)
		default:
			fmt.Printf("Unknown module: %s\n", startModule)
			fmt.Println("Valid: wp, hunter, cracker, pipeline, huntr, odint, port, silent (or 1-8)")
			tui.Close()
			os.Exit(1)
		}
	}

	// Start embedded DB viewer server on port 8089 and auto-open browser
	go tui.startDBViewerServer(dbPath)
	go func() {
		time.Sleep(500 * time.Millisecond)
		exec.Command("cmd", "/c", "start", "", "http://localhost:8089/db-viewer.html").Start()
	}()

	tui.Run()
}

// ===========================================================================
// EMBEDDED DB VIEWER SERVER
// ===========================================================================

func (tui *TUI) startDBViewerServer(dbPath string) {
	baseDir := filepath.Dir(dbPath)

	mux := http.NewServeMux()

	// Serve terminator.db with WAL checkpoint
	mux.HandleFunc("/terminator.db", func(w http.ResponseWriter, r *http.Request) {
		// Checkpoint WAL to flush pending writes into main DB file
		tui.db.conn.Exec("PRAGMA wal_checkpoint(PASSIVE)")

		w.Header().Set("Content-Type", "application/octet-stream")
		w.Header().Set("Cache-Control", "no-store")
		w.Header().Set("Access-Control-Allow-Origin", "*")
		http.ServeFile(w, r, dbPath)
	})

	// Serve db-viewer.html and other static files from project root
	// Project root is one level up from the DB location, or same dir
	projectRoot := baseDir
	viewerPath := filepath.Join(projectRoot, "db-viewer.html")
	if _, err := os.Stat(viewerPath); os.IsNotExist(err) {
		// Try parent directory (DB might be in a subfolder)
		projectRoot = filepath.Dir(baseDir)
	}

	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Access-Control-Allow-Origin", "*")
		http.FileServer(http.Dir(projectRoot)).ServeHTTP(w, r)
	})

	// Silently listen — don't crash the TUI if port is busy
	http.ListenAndServe("127.0.0.1:8089", mux)
}

// ===========================================================================
// COMPILE GUARDS — ensure all imported packages are used
// ===========================================================================

var _ = bufio.NewScanner
var _ = context.Background
var _ = md5.New
var _ = sha256.New
var _ = tls.Config{}
var _ = sql.Open
var _ = hex.EncodeToString
var _ = json.Unmarshal
var _ = fmt.Sprintf
var _ = io.ReadAll
var _ = log.SetOutput
var _ = net.Listen
var _ = http.Get
var _ = url.Parse
var _ = os.Exit
var _ = exec.Command
var _ = filepath.Join
var _ = regexp.MustCompile
var _ = runtime.GOOS
var _ = strconv.Atoi
var _ = strings.TrimSpace
var _ sync.Mutex
var _ = atomic.LoadInt64
var _ = time.Now
var _ = tcell.NewScreen
