import argparse
import json
import os
import re
import secrets
import string
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Iterator

import shutil

import requests
from playwright.sync_api import TimeoutError as PwTimeout, sync_playwright

BASE_DIR = Path(__file__).resolve().parent
ACCOUNTS_FILE = BASE_DIR / "akun_xai.txt"
SESSIONS_DIR = BASE_DIR / "sessions"
MAIL_API = "https://mail.falcinelli.my.id"
SIGNUP_URL = "https://accounts.x.ai/sign-up"
BUCKET_URL = "https://9test.kcodex.my.id" #ganti ini jadi url 9router kalian
BUCKET_PASSWORD = "123456"
BUCKET_GROK_CLI_URL = "https://9test.kcodex.my.id/dashboard/providers/grok-cli" ##ini juga ganti ip:port / url kalian
DEVICE_DONE_URL = "accounts.x.ai/oauth2/device/done"
REQUEST_TIMEOUT = 20
POLL_INTERVAL = 5
INBOX_WAIT = 300
DEVICE_DONE_WAIT = 5
LOOP_WAIT_AFTER_SESSION_SAVE = 5

# Cookie names penting untuk session x.ai / Grok
IMPORTANT_COOKIE_NAMES = {
    "__cf_bm",
    "__cuid",
    "_gcl_au",
    "_twpid",
    "cf_clearance",
    "last-logged-in-with",
    "OptanonAlertBoxClosed",
    "OptanonConsent",
    "sso",
    "sso-rw",
}

# -- UI: table / border log (CLI) --------------------------------------------
# dipakai batch + single-loop agar log rapi kotak/table tanpa file ekstra.

def _term_width(default: int = 80) -> int:
    try:
        w = shutil.get_terminal_size((default, 20)).columns
        return max(46, min(int(w), 160))
    except Exception:
        return default

def _box(text: str, width: int | None = None, pad: int = 1) -> None:
    w = width or _term_width()
    inner = w - 2
    lines = str(text).splitlines() or [""]
    print("+" + "-" * (w - 2) + "+")
    for ln in lines:
        for chunk in [ln[i:i + inner - pad * 2] for i in range(0, max(1, len(ln)), inner - pad * 2)] or [""]:
            c = (" " * pad) + chunk.ljust(inner - pad * 2) + (" " * pad)
            print("|" + c + "|")
    print("+" + "-" * (w - 2) + "+")

def _table(rows: list[list[Any]], headers: list[str] | None = None, title: str | None = None) -> None:
    if not rows and not headers:
        return
    w = _term_width()
    # auto column widths muat terminal, padding 1 kiri kanan per kolom
    data = ([headers] if headers else []) + rows
    if not data:
        return
    ncols = max(len(r) for r in data)
    # normalize rows length
    norm: list[list[str]] = []
    for r in data:
        rr = [str(c) for c in list(r) + [""] * (ncols - len(r))]
        norm.append(rr)
    # hitung lebar naturale (max len + 2 pad)
    col_w = [0] * ncols
    for r in norm:
        for i, c in enumerate(r):
            col_w[i] = max(col_w[i], len(c))
    for i in range(ncols):
        col_w[i] = min(col_w[i] + 2, 40)  # cap 40 biar tidak meledak
        col_w[i] = max(col_w[i], 6)
    # scale jika melebihi terminal: potong proporsional
    total = sum(col_w) + ncols + 1
    if total > w:
        over = total - w
        # kurangi dari kolom paling lebar dulu
        for _ in range(over):
            idx = max(range(ncols), key=lambda k: col_w[k])
            if col_w[idx] > 8:
                col_w[idx] -= 1
            else:
                break
    def sep(char: str = "-"):
        return "+" + "+".join(char * cw for cw in col_w) + "+"
    def row_str(cells: list[str]):
        parts = []
        for i, c in enumerate(cells):
            cw = col_w[i]
            # trunc jika kepanjangan, pad 1 kiri kanan
            inner = cw - 2
            disp = c if len(c) <= inner else c[: max(0, inner - 3)] + "..."
            parts.append(" " + disp.ljust(inner) + " ")
        return "|" + "|".join(parts) + "|"
    if title:
        print(f"\n{title}")
    print(sep("-"))
    if headers:
        print(row_str([str(h) for h in headers]))
        print(sep("="))
        body = norm[1:]
    else:
        body = norm
    for r in body:
        print(row_str(r))
    print(sep("-"))

def log_step(tag: str, msg: str) -> None:
    ts = datetime.now().strftime("%H:%M:%S")
    print(f"[{ts}] [{tag}] {msg}")

def log_ok(msg: str) -> None:
    print(f"  [?] {msg}")

def log_warn(msg: str) -> None:
    print(f"  [!] {msg}")

def log_err(msg: str) -> None:
    print(f"  [x] {msg}", file=sys.stderr)

CHROME_CANDIDATES = [
    Path(r"C:\Program Files\Google\Chrome\Application\chrome.exe"),
    Path(r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"),
    Path(os.environ.get("LOCALAPPDATA", "")) / "Google/Chrome/Application/chrome.exe",
]


# -- helpers: credentials ------------------------------------------------

def save_credentials(email: str, password: str, path: Path = ACCOUNTS_FILE) -> None:
    """Simpan credential format email|password saja (append ke file)."""
    line = f"{email}|{password}\n"
    with path.open("a", encoding="utf-8") as f:
        f.write(line)
    print(f"  [+] Credential disimpan ke {path.name}: {email}|{password}")


# -- helpers: cookie / session -------------------------------------------

def _is_xai_domain(domain: str) -> bool:
    d = (domain or "").lower().lstrip(".")
    return d == "x.ai" or d.endswith(".x.ai") or "x.ai" in d


def _is_important_cookie(name: str) -> bool:
    n = name or ""
    if n in IMPORTANT_COOKIE_NAMES:
        return True
    # mixpanel cookie: mp_<hash>_mixpanel
    if n.startswith("mp_") and n.endswith("_mixpanel"):
        return True
    return False


def _cookie_to_export(cookie: dict[str, Any]) -> dict[str, Any]:
    """Normalisasi cookie Playwright ke format simpan yang mudah dibaca ulang."""
    expires = cookie.get("expires", -1)
    expires_iso = None
    if isinstance(expires, (int, float)) and expires > 0:
        try:
            expires_iso = datetime.fromtimestamp(expires, tz=timezone.utc).isoformat().replace("+00:00", "Z")
        except (OSError, OverflowError, ValueError):
            expires_iso = None

    return {
        "name": cookie.get("name"),
        "value": cookie.get("value"),
        "domain": cookie.get("domain"),
        "path": cookie.get("path", "/"),
        "expires": expires,
        "expires_iso": expires_iso,
        "httpOnly": bool(cookie.get("httpOnly", False)),
        "secure": bool(cookie.get("secure", False)),
        "sameSite": cookie.get("sameSite") or "None",
    }


def collect_xai_cookies(context) -> list[dict[str, Any]]:
    """Ambil cookie session x.ai dari browser context."""
    try:
        all_cookies = context.cookies()
    except Exception as exc:
        print(f"  [!] Gagal ambil cookies: {exc}")
        return []

    xai_cookies: list[dict[str, Any]] = []
    for c in all_cookies:
        domain = c.get("domain") or ""
        name = c.get("name") or ""
        if not _is_xai_domain(domain):
            continue
        # Simpan semua cookie domain x.ai (penting + pendukung session)
        xai_cookies.append(_cookie_to_export(c))

    # Prioritaskan cookie penting di urutan depan
    xai_cookies.sort(
        key=lambda c: (0 if _is_important_cookie(c.get("name") or "") else 1, c.get("name") or "", c.get("domain") or "")
    )
    return xai_cookies


def cookies_to_header(cookies: list[dict[str, Any]]) -> str:
    """Buat string Cookie header dari list cookie (name=value; ...)."""
    parts: list[str] = []
    seen: set[str] = set()
    for c in cookies:
        name = c.get("name")
        value = c.get("value")
        if not name or value is None:
            continue
        # Prefer cookie domain .x.ai / x.ai untuk header sederhana
        key = name
        if key in seen:
            continue
        seen.add(key)
        parts.append(f"{name}={value}")
    return "; ".join(parts)


def save_session(
    context,
    email: str,
    password: str,
    sessions_dir: Path = SESSIONS_DIR,
) -> Path | None:
    sessions_dir.mkdir(parents=True, exist_ok=True)

    cookies = collect_xai_cookies(context)
    if not cookies:
        print("  [!] Tidak ada cookie x.ai yang ditemukan untuk disimpan.")
        return None

    # Nama file aman dari email
    safe_email = re.sub(r"[^\w.\-@]+", "_", email.strip())
    safe_email = safe_email.replace("@", "_")
    stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
    out_path = sessions_dir / f"{safe_email}_{stamp}.json"
    meta_path = sessions_dir / f"{safe_email}_{stamp}.meta.json"

    # Index by name untuk akses cepat (sso, sso-rw, cf_clearance, dll)
    by_name: dict[str, list[dict[str, Any]]] = {}
    for c in cookies:
        by_name.setdefault(c["name"], []).append(c)

    important_present = sorted(
        {c["name"] for c in cookies if _is_important_cookie(c.get("name") or "")}
    )
    missing_important = sorted(IMPORTANT_COOKIE_NAMES - set(important_present))

    payload = {
        "email": email,
        "password": password,
        "saved_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
        "domains": sorted({(c.get("domain") or "") for c in cookies}),
        "cookie_count": len(cookies),
        "important_cookies_present": important_present,
        "important_cookies_missing": missing_important,
        "cookie_header": cookies_to_header(cookies),
        "cookies": cookies,
        "cookies_by_name": by_name,
        # Playwright storage_state-compatible partial (cookies only)
        "storage_state": {
            "cookies": [
                {
                    "name": c["name"],
                    "value": c["value"],
                    "domain": c["domain"],
                    "path": c.get("path") or "/",
                    "expires": c.get("expires", -1),
                    "httpOnly": c.get("httpOnly", False),
                    "secure": c.get("secure", False),
                    "sameSite": c.get("sameSite") or "None",
                }
                for c in cookies
            ],
            "origins": [],
        },
    }

    meta = {
        "email": email,
        "saved_at": payload["saved_at"],
        "session_file": out_path.name,
        "cookie_count": len(cookies),
        "has_sso": "sso" in by_name,
        "has_sso_rw": "sso-rw" in by_name,
        "has_cf_clearance": "cf_clearance" in by_name,
        "important_cookies_present": important_present,
        "important_cookies_missing": missing_important,
    }

    try:
        out_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
        meta_path.write_text(json.dumps(meta, indent=2, ensure_ascii=False), encoding="utf-8")
        print(f"  [+] Session disimpan: {out_path.name} ({len(cookies)} cookies)")
        print(f"  [+] Meta disimpan   : {meta_path.name}")
        if missing_important:
            print(f"  [!] Cookie penting belum ada: {', '.join(missing_important)}")
        else:
            print("  [+] Semua cookie penting session terdeteksi.")
        # Ringkas sso bila ada
        if "sso" in by_name:
            sso_val = (by_name["sso"][0].get("value") or "")[:48]
            print(f"  [+] sso: {sso_val}...")
        return out_path
    except Exception as exc:
        print(f"  [!] Gagal simpan session: {exc}")
        return None


# -- helpers: email API --------------------------------------------------

def find_chrome() -> Path:
    for c in CHROME_CANDIDATES:
        if c.is_file():
            return c
    raise FileNotFoundError("Chrome tidak ditemukan. Instal Google Chrome.")


def api(method: str, path: str, **kw: Any) -> Any:
    r = requests.request(method, f"{MAIL_API}{path}", timeout=REQUEST_TIMEOUT, **kw)
    r.raise_for_status()
    try:
        return r.json()
    except ValueError as e:
        raise RuntimeError(f"Respons API bukan JSON: {r.text[:300]}") from e


def generate_email() -> str:
    p = api("POST", "/api/generate", json={})
    if not isinstance(p, dict):
        raise RuntimeError(f"Format /api/generate tidak dikenal: {p!r}")
    email = p.get("email") or p.get("address")
    if not isinstance(email, str) or "@" not in email:
        raise RuntimeError(f"Email tidak ditemukan: {json.dumps(p, indent=2)}")
    return email


def random_name(length: int = 8) -> str:
    return secrets.choice(string.ascii_uppercase) + "".join(
        secrets.choice(string.ascii_lowercase) for _ in range(length - 1)
    )


def random_last(length: int = 8) -> str:
    return "acc-" + "".join(
        secrets.choice(string.digits)
        for _ in range(length)
    )

def extract_code_from_text(text: str) -> str | None:
    text_clean = text.replace("\n", " ").replace("\r", " ")
    patterns = [
        r"confirmation code\s*:?\s*([A-Z0-9]+(?:-[A-Z0-9]+)+)",
        r"verification code\s*:?\s*([A-Z0-9]+(?:-[A-Z0-9]+)+)",
        r"one.time.code\s*:?\s*([A-Z0-9]+(?:-[A-Z0-9]+)+)",
        r"\b([A-Z0-9]{5,7}-[A-Z0-9]{2,4})\b",
        r"\b([A-Z0-9]{6})\b",
    ]
    for pat in patterns:
        m = re.search(pat, text_clean, re.IGNORECASE)
        if m:
            return m.group(1).upper()
    return None


def normalize_otp_code(code: str) -> str:
    return re.sub(r"[^A-Za-z0-9]", "", code).upper()


def iter_messages(payload: Any) -> Iterator[dict[str, Any]]:
    if isinstance(payload, list):
        yield from (i for i in payload if isinstance(i, dict))
        return
    if isinstance(payload, dict):
        for key in ("messages", "data", "inbox", "items"):
            v = payload.get(key)
            if isinstance(v, list):
                yield from (i for i in v if isinstance(i, dict))
                return
        if any(k in payload for k in ("id", "_id", "messageId")):
            yield payload


def get_code_from_inbox(email: str) -> str | None:
    inbox = api("GET", f"/api/inbox/{email}")
    for rec in iter_messages(inbox):
        code = extract_code_from_text(json.dumps(rec))
        if code:
            return code
        mid = rec.get("id") or rec.get("_id") or rec.get("messageId")
        if mid:
            msg = api("GET", f"/api/message/{mid}")
            code = extract_code_from_text(json.dumps(msg))
            if code:
                return code
    return None


def poll_code(email: str, timeout: int) -> str | None:
    deadline = time.monotonic() + timeout
    attempt = 0
    while time.monotonic() < deadline:
        attempt += 1
        try:
            code = get_code_from_inbox(email)
            if code:
                return code
        except Exception as exc:
            print(f"  [!] Cek inbox gagal: {exc}")
        sisa = int(deadline - time.monotonic())
        print(f"  Menunggu kode verifikasi... ({sisa}s tersisa, cek ke-{attempt})")
        time.sleep(POLL_INTERVAL)
    return None


# -- helpers: Playwright -------------------------------------------------

def try_click(page, *selectors: str, timeout: int = 10_000) -> bool:
    for sel in selectors:
        try:
            page.locator(sel).first.click(timeout=timeout)
            return True
        except PwTimeout:
            continue
    return False


def fill_react_input(page, selector: str, value: str, timeout: int = 8000) -> bool:
    try:
        loc = page.locator(selector).first
        loc.wait_for(state="visible", timeout=timeout)
        loc.click(timeout=timeout)
        loc.fill("")
        loc.fill(value)
        page.evaluate(
            """([sel, val]) => {
                const el = document.querySelector(sel);
                if (!el) return false;
                const proto = window.HTMLInputElement.prototype;
                const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
                if (setter) setter.call(el, val);
                else el.value = val;
                el.dispatchEvent(new Event('input', { bubbles: true }));
                el.dispatchEvent(new Event('change', { bubbles: true }));
                return true;
            }""",
            [selector, value],
        )
        return True
    except Exception:
        return False


def fill_otp_code(page, code: str) -> bool:
    otp_code = normalize_otp_code(code)
    print(f"    Kode OTP (tanpa dash): {otp_code}")
    selectors = [
        'input[data-input-otp="true"]',
        'input[name="code"]',
        'input[autocomplete="one-time-code"]',
        'input[pattern*="a-zA-Z0-9"]',
    ]
    for sel in selectors:
        try:
            loc = page.locator(sel).first
            loc.wait_for(state="visible", timeout=8000)
            loc.click(timeout=3000)
            page.keyboard.press("Control+A")
            page.keyboard.press("Backspace")
            page.wait_for_timeout(150)
            for ch in otp_code:
                page.keyboard.type(ch, delay=80)
            page.evaluate(
                """([sel, val]) => {
                    const el = document.querySelector(sel);
                    if (!el) return;
                    const proto = window.HTMLInputElement.prototype;
                    const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
                    if (setter) setter.call(el, val);
                    else el.value = val;
                    el.dispatchEvent(new Event('input', { bubbles: true }));
                    el.dispatchEvent(new Event('change', { bubbles: true }));
                    el.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true }));
                }""",
                [sel, otp_code],
            )
            current = loc.input_value(timeout=2000)
            if normalize_otp_code(current) == otp_code:
                print(f"    ? OTP terisi via selector: {sel}")
                return True
            if fill_react_input(page, sel, otp_code):
                current = loc.input_value(timeout=2000)
                if normalize_otp_code(current) == otp_code:
                    print(f"    ? OTP terisi via React fill: {sel}")
                    return True
        except Exception as exc:
            print(f"    [!] Gagal selector {sel}: {exc}")
            continue
    return False


def wait_for_complete_form(page, timeout_ms: int = 30_000) -> bool:
    try:
        page.wait_for_selector(
            'input[data-testid="givenName"], input[name="givenName"]',
            timeout=timeout_ms,
        )
        return True
    except PwTimeout:
        return False


def fill_complete_form(page, first_name: str, last_name: str, password: str) -> bool:
    pairs = [
        ("givenName", first_name, [
            'input[data-testid="givenName"]',
            'input[name="givenName"]',
            'input[autocomplete="given-name"]',
        ]),
        ("familyName", last_name, [
            'input[data-testid="familyName"]',
            'input[name="familyName"]',
            'input[autocomplete="family-name"]',
        ]),
        ("password", password, [
            'input[data-testid="password"]',
            'input[name="password"]',
            'input[type="password"]',
        ]),
    ]
    ok_all = True
    for label, value, selectors in pairs:
        filled = False
        for sel in selectors:
            if fill_react_input(page, sel, value):
                try:
                    got = page.locator(sel).first.input_value(timeout=2000)
                    if got == value:
                        print(f"    ? {label}: terisi")
                        filled = True
                        break
                except Exception:
                    print(f"    ? {label}: terisi (tidak diverifikasi)")
                    filled = True
                    break
        if not filled:
            print(f"    [!] Gagal isi {label}")
            ok_all = False
    return ok_all


# -- helpers: bucket & Add & device consent -----------------------------

def open_bucket_and_login(context, page) -> Any:
    """Buka langsung ke target, cegat form login jika belum login, lalu pastikan ada di target."""
    print(f"\n[11] Membuka tab ke bucket target: {BUCKET_GROK_CLI_URL}")
    try:
        bucket_page = context.new_page()
        bucket_page.goto(BUCKET_GROK_CLI_URL, wait_until="domcontentloaded", timeout=45_000)
    except Exception as exc:
        print(f"  [!] Gagal buka tab baru ({exc}). Navigasi tab aktif.")
        try:
            page.goto(BUCKET_GROK_CLI_URL, wait_until="domcontentloaded", timeout=45_000)
            bucket_page = page
        except Exception as exc2:
            print(f"  [!] Gagal navigasi: {exc2}")
            return None
    
    # Tunggu sebentar untuk memberikan waktu React merender atau mengarahkan ke halaman login
    bucket_page.wait_for_timeout(3000)
    
    print("  [*] Mengecek form login 9router...")
    login_selectors = [
        'input[type="password"]',
        'input[name="password"]',
        'input[placeholder*="password" i]',
        'input[autocomplete="current-password"]'
    ]
    
    login_found = False
    for sel in login_selectors:
        try:
            loc = bucket_page.locator(sel).first
            # Menggunakan wait_for agar benar-benar menunggu form-nya muncul (maksimal 5 detik tiap selector)
            loc.wait_for(state="visible", timeout=5000)
            login_found = True
            loc.fill(BUCKET_PASSWORD)
            print(f"  [+] Password bucket diisi via: {sel}")
            bucket_page.wait_for_timeout(500)
            bucket_page.keyboard.press("Enter")
            
            # Beri waktu buat auth process dan navigasi kembali
            bucket_page.wait_for_timeout(5000)
            break
        except Exception:
            continue
            
    if not login_found:
        print("  [?] Form password tidak ditemukan (Kemungkinan sudah login sebelumnya).")
        
    # Pastikan URL kita berada di grok-cli, karena mungkin setelah login dilempar ke /dashboard biasa
    current_url = bucket_page.url
    if "grok-cli" not in current_url:
        print(f"  [*] Mengarahkan ulang ke target yang benar: {BUCKET_GROK_CLI_URL}")
        try:
            bucket_page.goto(BUCKET_GROK_CLI_URL, wait_until="domcontentloaded", timeout=30_000)
            bucket_page.wait_for_timeout(3000)
        except Exception:
            pass
            
    return bucket_page


def click_add_button(page) -> bool:
    """Klik tombol Add di halaman providers bucket."""
    print("  [*] Mencari tombol 'Add'...")
    add_selectors = [
        'button:has-text("Add")',
        'span:has-text("add")',
        'div[role="button"]:has-text("add")',
        "text=Add",
        'button:has-text("add")',
        'button:has-text("ADD")',
    ]
    
    # Melakukan percobaan beberapa kali karena UI mungkin lambat di-render
    for attempt in range(4):
        for sel in add_selectors:
            try:
                locator = page.locator(sel)
                count = locator.count()

                if count > 0:
                    # Jika ada 2 atau lebih, pilih tombol kedua
                    loc = locator.nth(1) if count >= 2 else locator.first

                    if loc.is_visible():
                        loc.click(timeout=5000)
                        print(f"  [+] Klik Add via: {sel}")
                        page.wait_for_timeout(3000)
                        return True

            except Exception:
                continue
        # Tunggu 2 detik sebelum percobaan berikutnya
        page.wait_for_timeout(2000)
        
    print("  [!] Tombol Add tidak ditemukan.")
    return False


def click_named_button(page, names, timeout_ms: int = 4000) -> bool:
    for name in names:
        try:
            btn = page.get_by_role("button", name=re.compile(rf"^{re.escape(name)}$", re.I)).first
            if btn.count() > 0 and btn.is_visible():
                btn.click(timeout=timeout_ms)
                print(f"  [+] Klik tombol via role: {name}")
                return True
        except Exception:
            pass
        try:
            btn = page.locator(f'button:has-text("{name}")').first
            if btn.count() > 0 and btn.is_visible():
                btn.click(timeout=timeout_ms)
                print(f"  [+] Klik tombol via text: {name}")
                return True
        except Exception:
            pass
        try:
            btn = page.locator(f'div[role="button"]:has-text("{name}")').first
            if btn.count() > 0 and btn.is_visible():
                btn.click(timeout=timeout_ms)
                print(f"  [+] Klik tombol via role=button: {name}")
                return True
        except Exception:
            pass
    return False


def is_device_done(context) -> bool:
    try:
        pages = list(context.pages)
    except Exception:
        return False
    for pg in pages:
        try:
            if pg.is_closed():
                continue
            if DEVICE_DONE_URL in pg.url.lower():
                return True
        except Exception:
            continue
    return False


def handle_device_consent(context, timeout_ms: int = 180_000) -> bool:
    """Handle OAuth device consent: Continue -> Allow, polling sampai done."""
    print("[12] Memantau OAuth device consent x.ai (Continue -> Allow)...")
    start = time.monotonic()
    continue_clicked = False
    allow_clicked = False

    while (time.monotonic() - start) * 1000 < timeout_ms:
        try:
            pages = list(context.pages)
        except Exception:
            print("[!] Browser context hilang.")
            return False
        if not pages:
            print("[!] Semua tab ditutup.")
            return False

        for pg in pages:
            try:
                if pg.is_closed():
                    continue
                url = pg.url.lower()
            except Exception:
                continue

            if "accounts.x.ai/oauth2/device" in url or "user_code=" in url:
                print(f"  [*] Device consent: {pg.url}")
                try:
                    pg.bring_to_front()
                except Exception:
                    pass

                if not continue_clicked:
                    if click_named_button(pg, ("Continue", "Lanjut", "Next", "Berikutnya", "Confirm")):
                        continue_clicked = True
                        print("[?] Continue (device consent) diklik.")
                        pg.wait_for_timeout(1200)

                if "oauth2/device/done" in url:
                    print("[?] device/done terdeteksi.")
                    return True

                if continue_clicked and not allow_clicked:
                    if click_named_button(pg, ("Allow", "Izinkan", "Approve", "Authorize", "Setuju", "Yes")):
                        allow_clicked = True
                        print("[?] Allow (device consent) diklik.")
                        pg.wait_for_timeout(6000)
                        try:
                            if "oauth2/device/done" in pg.url.lower() or is_device_done(context):
                                print("[?] device/done setelah Allow.")
                                return True
                        except Exception:
                            pass
                        return True

                for sel in (
                    'button:has-text("Continue")',
                    'button:has-text("Allow")',
                    'button[type="submit"]',
                    'div[role="button"]:has-text("Continue")',
                    'div[role="button"]:has-text("Allow")',
                ):
                    try:
                        el = pg.locator(sel).first
                        if el.count() > 0 and el.is_visible():
                            txt = (el.inner_text() or "").strip()
                            print(f"  [+] Klik device consent via: {sel} ({txt!r})")
                            el.click(timeout=3000)
                            pg.wait_for_timeout(1200)
                            if "allow" in txt.lower() or "izin" in txt.lower():
                                return True
                            if "continue" in txt.lower() or "lanjut" in txt.lower():
                                continue_clicked = True
                    except Exception:
                        continue

            if allow_clicked or (
                "oauth2/device" not in url
                and any(x in url for x in ("success", "dashboard", "providers", "bucket.det.my.id"))
            ):
                if continue_clicked:
                    return True

        time.sleep(0.5)

    print("[!] Timeout device consent.")
    return continue_clicked and allow_clicked


def wait_for_device_done(context, timeout_sec: int) -> bool:
    print(f"[13] Polling sampai {DEVICE_DONE_URL} muncul (max {timeout_sec}s)...")
    deadline = time.monotonic() + timeout_sec
    attempt = 0
    while time.monotonic() < deadline:
        attempt += 1
        if is_device_done(context):
            print(f"    ? device/done terdeteksi (cek ke-{attempt})")
            return True
        time.sleep(2)
    return False


# -- main ----------------------------------------------------------------

def run_once(profile_name: str = "tempxgrok", headless: bool = False) -> int:
    print("[1] Cek mail API & generate email...")
    try:
        health = api("GET", "/health")
        print(f"    Mail API: {health}")
        email = generate_email()
        chrome_path = find_chrome()
    except (FileNotFoundError, requests.RequestException, RuntimeError) as exc:
        print(f"GAGAL: {exc}", file=sys.stderr)
        return 1

    # Profil Chrome mengikuti email yang di-generate: setiap email baru
    # mendapat folder profile sendiri di profiles/<email> agar terisolasi
    # dan bisa dipakai ulang untuk email yang sama.
    profile_name = re.sub(r"[^\w.\-@]+", "_", email.strip()).replace("@", "_at_")
    if not profile_name:
        profile_name = "tempxgrok"

    first_name = random_name()
    last_name = random_last()
    password = "@Akun123##$$"

    print(f"\n{'='*50}")
    print(f"Profil Chrome : {profile_name}")
    print(f"Headless      : {headless}")
    print(f"Email         : {email}")
    print(f"Nama depan    : {first_name}")
    print(f"Nama belakang : {last_name}")
    print(f"Password      : {password}")
    print(f"{'='*50}\n")

    # Nama folder profil di profiles/<email> - bedakan per email agar
    # setiap akun punya session Chrome terpisah dan tidak bentrok.
    safe = "".join(c if c.isalnum() or c in "-_." else "_" for c in profile_name)
    if not safe:
        safe = "tempxgrok"
    profile_dir = BASE_DIR / "profiles" / safe
    profile_dir.mkdir(parents=True, exist_ok=True)

    print(f"[2] Membuka Chrome (profil: {profile_dir})...")
    with sync_playwright() as pw:
        ctx = pw.chromium.launch_persistent_context(
            user_data_dir=str(profile_dir),
            executable_path=str(chrome_path),
            headless=headless,
            args=["--disable-blink-features=AutomationControlled"],
        )
        page = ctx.pages[0] if ctx.pages else ctx.new_page()
        page.goto(SIGNUP_URL, wait_until="domcontentloaded")
        page.wait_for_timeout(3000)

        # -- langkah 1: klik "Sign Up With Email" ---------------------
        print("[3] Mencari tombol 'Sign Up With Email'...")
        signup_btn_selectors = [
            "text=Sign Up With Email",
            "button:has-text('Sign Up')",
            'button:has-text("Sign up with email")',
            "a:has-text('Sign Up')",
            '[data-testid="signup-with-email"]',
        ]
        if not try_click(page, *signup_btn_selectors, timeout=8000):
            print("  [!] Tombol tidak ditemukan, klik manual jika perlu.")
        page.wait_for_timeout(2000)

        # -- langkah 2: isi email --------------------------------------
        print(f"[4] Isi email: {email}")
        email_selectors = [
            'input[type="email"]', 'input[name="email"]',
            'input[autocomplete="email"]', 'input[placeholder*="email" i]',
            "input#email",
        ]
        email_ok = False
        for sel in email_selectors:
            if fill_react_input(page, sel, email):
                email_ok = True
                break
        if not email_ok:
            print("  [!] Gagal isi email otomatis. Isi manual.")
            input("    Tekan Enter setelah mengisi email...")
        page.wait_for_timeout(1000)

        # -- langkah 3: user selesaikan Turnstile #1 -------------------
        print("[5] >> Selesaikan Turnstile (langkah pertama) di browser.")
        print("    Skrip akan otomatis Enter + tunggu 5 detik lalu lanjut.")
        page.keyboard.press("Enter")
        page.wait_for_timeout(4000)

        # -- langkah 4: tunggu kode verifikasi -------------------------
        print(f"[6] Pantau inbox {MAIL_API}/api/inbox/{email}")
        code = poll_code(email, INBOX_WAIT)

        if not code:
            print(f"\n[!] Kode tidak ditemukan dalam {INBOX_WAIT}s.", file=sys.stderr)
            code = input("    Masukkan kode verifikasi manual: ").strip()
            if not code:
                ctx.close()
                return 1
        print(f"    ? Kode ditemukan: {code}")

        # -- langkah 5: isi kode OTP otomatis --------------------------
        print("[7] Isi kode verifikasi otomatis...")
        try:
            page.wait_for_selector(
                'input[data-input-otp="true"], input[name="code"], input[autocomplete="one-time-code"]',
                timeout=20_000,
            )
        except PwTimeout:
            print("  [!] Field OTP belum muncul.")

        if not fill_otp_code(page, code):
            print(f"  [!] Gagal isi kode otomatis. Isi manual: {normalize_otp_code(code)}")
        page.wait_for_timeout(1500)

        # -- langkah 6: tunggu form Complete sign up -------------------
        print("[8] Menunggu halaman 'Complete your sign up'...")
        if not wait_for_complete_form(page, timeout_ms=45_000):
            print("  [!] Form complete belum muncul.")
        print(f"    Isi: {first_name} / {last_name} / {password}")
        if not fill_complete_form(page, first_name, last_name, password):
            print("  [!] Sebagian field gagal. Coba isi ulang...")
            page.wait_for_timeout(1000)
            fill_complete_form(page, first_name, last_name, password)

        # -- langkah 7: CEGAT CLOUDFLARE CAPTCHA -----------------------
        print("[9] Menangani Cloudflare CAPTCHA (jika ada)...")
        page.wait_for_timeout(3000)
        
        try:
            # Mencari iframe Turnstile dan mencoba melakukan auto-click dasar
            turnstile_iframe = page.locator('iframe[src*="turnstile"]')
            if turnstile_iframe.is_visible(timeout=2000):
                print("  [*] Turnstile CAPTCHA terdeteksi. Mencoba auto-click...")
                # Klik sembarang di dalam iframe biasanya bisa men-trigger CAPTCHA
                turnstile_iframe.click(position={"x": 30, "y": 30}, timeout=3000) 
        except Exception:
            pass
            
        print("  >> JEDA 15 DETIK. JIKA CAPTCHA BELUM TERCENTANG, SILAKAN KLIK SECARA MANUAL DI BROWSER!")
        page.wait_for_timeout(15000)

        # Setelah melewati nama/password/captcha: tekan Enter
        print("[10] Menekan Enter (Menyelesaikan Sign Up)...")
        page.keyboard.press("Enter")
        page.wait_for_timeout(5000)

        # -- langkah 8: buka bucket & login ----------------------------
        bucket_page = open_bucket_and_login(ctx, page)

        # -- langkah 9: klik Add ---------------------------------------
        if bucket_page:
            bucket_page.wait_for_timeout(2000) # Tunggu UI render
            
        if not click_add_button(bucket_page or page):
            print("  [!] Klik Add gagal. Lanjutkan manual jika perlu.")
            input("    Tekan Enter setelah selesai klik Add...")

        # -- langkah 10: handle device consent --------------------------
        page.wait_for_timeout(3000)
        handle_device_consent(ctx, timeout_ms=180_000)

        # -- langkah 11: tunggu device/done ----------------------------
        if not wait_for_device_done(ctx, DEVICE_DONE_WAIT):
            log_warn("device/done timeout - kembali ke awal.")
            try:
                ctx.close()
            except Exception:
                pass
            return 2

        # -- langkah 12: simpan credential email|password --------------
        print("[14] Menyimpan credential (email|password)...")
        try:
            save_credentials(email, password)
        except Exception as exc:
            print(f"    [!] Gagal simpan credential: {exc}")

        # -- langkah 13: simpan cookie / session (tanpa logout) --------
        print("[15] Menyimpan cookie / session x.ai...")
        page.wait_for_timeout(2000)
        try:
            # Kunjungi accounts.x.ai agar cookie domain ter-refresh
            try:
                page.goto("https://accounts.x.ai/", wait_until="domcontentloaded", timeout=30_000)
                page.wait_for_timeout(1500)
            except Exception:
                pass
            save_session(ctx, email, password)
        except Exception as exc:
            print(f"    [!] Gagal simpan session: {exc}")

        print(f"\n>> Siklus selesai (session disimpan, tanpa logout). Menunggu {LOOP_WAIT_AFTER_SESSION_SAVE} detik sebelum mengulang...")
        ctx.close()

    return 0


def parse_args() -> argparse.Namespace:
    p = argparse.ArgumentParser(
        description="Auto sign-up x.ai + bucket grok-cli + simpan cookie/session (tanpa logout).",
        formatter_class=argparse.RawTextHelpFormatter,
        epilog=(
            "Contoh:\n"
            "  python tempxgrok.py                          # loop tak terbatas (1 akun per siklus)\n"
            "  python tempxgrok.py --batch 10               # batch 10 akun, tabel ringkasan\n"
            "  python tempxgrok.py --batch 5 --headless     # batch headless\n"
            "  python tempxgrok.py --batch 20 --delay 3     # jeda 3 detik antar siklus"
        ),
    )
    p.add_argument(
        "--profile",
        type=str,
        default="",
        help=(
            "Nama profil Chrome fallback, hanya dipakai jika generate email "
            "menghasilkan string kosong. Normalnya nama folder profil OTOMATIS "
            "mengikuti email yang di-generate (profiles/<email>)."
        ),
    )
    p.add_argument(
        "--headless",
        action="store_true",
        help="Jalankan Chrome tanpa UI (headless).",
    )
    p.add_argument(
        "--batch",
        type=int,
        default=0,
        help=(
            "Jumlah akun yang dibuat dalam mode batch (default 0 = loop tak terbatas).\n"
            "Contoh: --batch 10  -> buat 10 akun berurutan, log tabel + border."
        ),
    )
    p.add_argument(
        "--delay",
        type=float,
        default=float(LOOP_WAIT_AFTER_SESSION_SAVE),
        help=f"Jeda detik antar siklus/batch (default {LOOP_WAIT_AFTER_SESSION_SAVE}).",
    )
    p.add_argument(
        "--device-wait",
        type=int,
        default=int(DEVICE_DONE_WAIT),
        help=f"Timeout tunggu device/done detik (default {DEVICE_DONE_WAIT}). Override DEVICE_DONE_WAIT.",
    )
    return p.parse_args()


def _last_saved_email(limit: int = 32) -> str:
    try:
        if ACCOUNTS_FILE.is_file():
            lines = [ln.strip() for ln in ACCOUNTS_FILE.read_text(encoding="utf-8").splitlines() if ln.strip()]
            if lines:
                return lines[-1].split("|", 1)[0].strip()[:limit]
    except Exception:
        pass
    return "-"

def _status_for(code: int) -> tuple[str, str]:
    if code == 0:
        return "OK", "session tersimpan"
    if code == 2:
        return "SKIP", "device/done timeout -> auto next"
    return "GAGAL", f"exit {code}"

def _append_capped(buf: list[Any], item: Any, cap: int = 20) -> None:
    buf.append(item)
    if len(buf) > cap:
        del buf[: len(buf) - cap]

def _batch_summary_table(rows: list[list[Any]], title: str = "Ringkasan Batch") -> None:
    if not rows:
        return
    _table(rows, headers=["#", "Email", "Status", "Durasi", "Keterangan"], title=title)


def main() -> int:
    args = parse_args()
    # Fallback profile hanya dipakai jika email kosong (sangat jarang).
    # Normalnya folder profil Chrome diturunkan dari email tiap siklus.
    profile_name = args.profile.strip() or "tempxgrok"
    headless = bool(args.headless)
    batch_total: int = int(args.batch or 0)
    loop_delay: float = float(args.delay)
    # override global DEVICE_DONE_WAIT jika user set --device-wait
    global DEVICE_DONE_WAIT
    try:
        DEVICE_DONE_WAIT = int(args.device_wait)
    except Exception:
        pass

    term_w = _term_width()
    _box(
        f"GROKFARM  |  Profil: auto(email) fallback={profile_name}  |  headless={headless}  |  device_wait={DEVICE_DONE_WAIT}s",
        width=term_w,
    )
    if batch_total > 0:
        _box(f"MODE BATCH  |  target={batch_total} akun  |  delay={loop_delay}s  |  log: border + table", width=term_w)
        rows: list[list[Any]] = []
        sukses = skip = gagal = 0
        t_batch_start = time.monotonic()
        for idx in range(1, batch_total + 1):
            _box(f" BATCH {idx}/{batch_total}  -  memulai siklus ", width=term_w)
            print(f"  Profil Chrome : auto(email)  (fallback: profiles/{profile_name})")
            print(f"  Headless      : {headless}")
            t0 = time.monotonic()
            result = run_once(profile_name=profile_name, headless=headless)
            dt_s = f"{time.monotonic() - t0:.1f}s"
            last_email = _last_saved_email()
            status, ket = _status_for(result)
            if result == 0:
                sukses += 1
            elif result == 2:
                skip += 1
            else:
                gagal += 1
            rows.append([str(idx), last_email, status, dt_s, ket])
            if idx % 5 == 0 or idx == batch_total:
                _batch_summary_table(rows, title=f"Progress {idx}/{batch_total}")
            if idx < batch_total:
                log_step("BATCH", f"Jeda {loop_delay}s sebelum batch berikutnya... (sukses {sukses}, skip {skip}, gagal {gagal})")
                time.sleep(max(0, loop_delay))
        total_dt = time.monotonic() - t_batch_start
        _box(
            f" BATCH SELESAI  |  total {batch_total}  |  OK {sukses}  |  SKIP {skip}  |  GAGAL {gagal}  |  durasi {total_dt/60:.1f} menit ",
            width=term_w,
        )
        _batch_summary_table(rows, title="RINGKASAN AKHIR BATCH")
        try:
            if ACCOUNTS_FILE.is_file():
                n = len([ln for ln in ACCOUNTS_FILE.read_text(encoding="utf-8").splitlines() if ln.strip()])
                print(f"\n  File akun: {ACCOUNTS_FILE.name}  ({n} baris)")
        except Exception:
            pass
        return 0 if gagal == 0 else 1

    # -- mode lama: loop tak terbatas ---------------------------------
    print(f"[*] Mode LOOP tak terbatas - delay {loop_delay}s antar siklus")
    print(f"[*] Tip: pakai --batch N untuk mode batch + tabel berborder\n")
    cycle = 0
    loop_rows: list[list[Any]] = []
    LOOP_HEADERS = ["Siklus", "Status", "Durasi", "Keterangan"]
    while True:
        cycle += 1
        _box(f" SIKLUS KE-{cycle}  |  profil=auto(email)  |  headless={headless}  |  delay={loop_delay}s ", width=term_w)
        t0 = time.monotonic()
        result = run_once(profile_name=profile_name, headless=headless)
        dt_s = f"{time.monotonic() - t0:.1f}s"
        if result == 2:
            log_warn(f"Siklus {cycle} SKIP (device/done timeout) -> auto balik ke awal, lanjut siklus {cycle+1} tanpa henti.")
            _append_capped(loop_rows, [str(cycle), "SKIP", dt_s, "device/done timeout -> next"])
            _table(loop_rows, headers=LOOP_HEADERS, title=f"Ringkasan 20 siklus terakhir (total {cycle})")
            time.sleep(max(0, loop_delay))
            continue
        if result != 0:
            log_err(f"Siklus {cycle} GAGAL exit={result} -> henti (pakai --batch agar auto lanjut walau gagal).")
            _append_capped(loop_rows, [str(cycle), f"GAGAL {result}", dt_s, "henti"])
            _table(loop_rows, headers=LOOP_HEADERS, title="Ringkasan loop")
            return result
        _append_capped(loop_rows, [str(cycle), "OK", dt_s, "session tersimpan"])
        if cycle % 5 == 0:
            _table(loop_rows, headers=LOOP_HEADERS, title=f"Ringkasan 20 siklus terakhir (total {cycle})")
        log_step("LOOP", f"Siklus {cycle} OK - jeda {loop_delay}s sebelum siklus berikutnya...")
        time.sleep(max(0, loop_delay))


if __name__ == "__main__":
    raise SystemExit(main())
