"""
Smile.One Auto Order - Google Login + Background order
Sign in with Google ဖြင့် login လုပ်ပြီး session သိမ်းကာ order တင်သည်
"""

import asyncio
import json
import re
from pathlib import Path
import os
import subprocess
import sys
import threading

# Windows consoles often use cp1252; package names/prices include ₱ and break print().
if hasattr(sys.stdout, "reconfigure"):
    try:
        sys.stdout.reconfigure(encoding="utf-8", errors="replace")
        sys.stderr.reconfigure(encoding="utf-8", errors="replace")
    except Exception:
        pass

try:
    from playwright.async_api import async_playwright, TimeoutError as PlaywrightTimeout
except ImportError:
    async_playwright = None

BASE_URL = "https://www.smile.one/br"
LOGIN_URL = f"{BASE_URL}/customer/account/login"
AUTH_FILE = Path(__file__).parent / "smile_auth.json"


def _google_login_entry_url() -> str:
    """
    Page opened for Admin "Login with Google" (do_google_login).
    Default: Brazil login (historical). Set SMILE_GOOGLE_LOGIN_ENTRY=ph so OAuth starts on the PH
    storefront — often better cookie coverage for /ph/ merchant orders.
    """
    mode = (os.getenv("SMILE_GOOGLE_LOGIN_ENTRY") or "br").strip().lower()
    if mode in ("ph", "phl", "philippines", "rp"):
        return "https://www.smile.one/ph/customer/account/login"
    return LOGIN_URL

_DEFAULT_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36"
_NAV_TIMEOUT_MS = 120_000
_PROFILE_DIR = Path(__file__).parent / ".pw_profile"


def _auth_file_has_host(host: str) -> bool:
    """True if smile_auth.json already carries a cookie for *host* (skip priming visits)."""
    try:
        if not AUTH_FILE.exists():
            return False
        data = json.loads(AUTH_FILE.read_text(encoding="utf-8"))
        cookies = data.get("cookies") if isinstance(data, dict) else None
        if not isinstance(cookies, list):
            return False
        target = (host or "").strip().lower()
        if not target:
            return False
        for c in cookies:
            dom = str((c or {}).get("domain") or "").lower()
            if not dom:
                continue
            if dom == target or dom == "." + target:
                return True
            stripped = dom.lstrip(".")
            if stripped and target.endswith(stripped):
                return True
        return False
    except Exception:
        return False

from order_automation_ph import (
    PH_CLICK_PAYMENT_JS,
    PH_EXTRA_BUY_TEXTS,
    PH_PACKAGE_LI_LOCATOR,
    PH_SCROLL_PRIMING_JS,
    SELECT_PACKAGE_PH_JS,
    ph_payment_targets,
)


async def _wait_ph_buy_button_ready(page, sel_ctx, timeout_ms: int = 25_000) -> bool:
    """PH checkout enables the main buy control only after Smile Coin + price settle."""
    import time as _time

    deadline = _time.monotonic() + timeout_ms / 1000.0
    while _time.monotonic() < deadline:
        for tgt in ph_payment_targets(page, sel_ctx):
            try:
                loc = tgt.locator(".payment-button-container .Nav-btn").first
                if await loc.count() == 0:
                    continue
                if await loc.is_visible() and not await loc.is_disabled():
                    return True
            except Exception:
                continue
        await asyncio.sleep(0.35)
    return False

try:
    from config import (
        CHECKOUT_REQ_SLEEP_SHORT_SEC,
        ORDER_NODE_SUBPROCESS_TIMEOUT_SEC,
        ORDER_SLEEP_AFTER_CLICK_SEC,
        ORDER_SLEEP_AFTER_FILL_SEC,
        ORDER_SLEEP_AFTER_GOTO_SEC,
        playwright_effective_channel,
    )
except Exception:
    ORDER_SLEEP_AFTER_GOTO_SEC = float(os.getenv("ORDER_SLEEP_AFTER_GOTO_SEC", "0.5"))
    ORDER_SLEEP_AFTER_FILL_SEC = float(os.getenv("ORDER_SLEEP_AFTER_FILL_SEC", "0.45"))
    ORDER_SLEEP_AFTER_CLICK_SEC = float(os.getenv("ORDER_SLEEP_AFTER_CLICK_SEC", "0.55"))
    CHECKOUT_REQ_SLEEP_SHORT_SEC = float(os.getenv("CHECKOUT_REQ_SLEEP_SHORT_SEC", "0.6"))
    ORDER_NODE_SUBPROCESS_TIMEOUT_SEC = 420

    def playwright_effective_channel():
        ch = (os.getenv("PLAYWRIGHT_CHANNEL") or "").strip()
        return ch or None

# Shared with scrape_checkout_requirements (Playwright Python: evaluate() accepts ONE extra arg only).
# Skips header/nav/footer/cookie/promo bars so we capture game-ID fields, not newsletter email.
_FORM_FIELDS_JS = """
() => {
  const isVisible = (el) => {
    if (!el) return false;
    const rect = el.getBoundingClientRect();
    if (rect.width === 0 || rect.height === 0) return false;
    const style = window.getComputedStyle(el);
    if (style.visibility === 'hidden' || style.display === 'none') return false;
    return true;
  };

  const inSiteChrome = (el) => {
    let p = el;
    for (let n = 0; n < 16 && p; n++) {
      const t = (p.tagName || '').toLowerCase();
      const cls = (typeof p.className === 'string' ? p.className : String(p.className || '')).toLowerCase();
      const id = ((p.id || '') + '').toLowerCase();
      const role = ((p.getAttribute && p.getAttribute('role')) || '').toLowerCase();
      if (t === 'header' || t === 'footer') return true;
      if (role === 'banner' || role === 'navigation' || role === 'contentinfo') return true;
      if (/header|navbar|nav-bar|toolbar|footer|subscription|newsletter|cookie|gdpr|top-bar|topbar|promo-banner|search-bar/i.test(cls + ' ' + id)) return true;
      p = p.parentElement;
    }
    return false;
  };

  const getLabel = (el) => {
    const id = el.getAttribute('id');
    if (id) {
      try {
        const lab = document.querySelector(`label[for="${id.replace(/"/g, '\\"')}"]`);
        if (lab && lab.innerText) return lab.innerText.trim();
      } catch (e) {}
    }
    const parentLabel = el.closest && el.closest('label');
    if (parentLabel && parentLabel.innerText) return parentLabel.innerText.trim();
    const aria = el.getAttribute('aria-label');
    if (aria) return aria.trim();
    if (el.placeholder) return el.placeholder.trim();
    return '';
  };

  const allowedTypes = new Set(['text','email','number','tel','url','password','textarea']);
  const out = [];
  const els = Array.from(document.querySelectorAll('input, textarea, select'));
  for (const el of els) {
    if (!isVisible(el)) continue;
    if (el.disabled) continue;
    if (inSiteChrome(el)) continue;

    const tag = el.tagName.toLowerCase();
    const type = (el.getAttribute('type') || '').toLowerCase();
    if (tag === 'select') {
      // ok
    } else {
      if (type === 'hidden' || type === 'submit' || type === 'button' || type === 'checkbox' || type === 'radio') continue;
      if (type === 'search') continue;
      if (tag === 'input' && type && !allowedTypes.has(type)) continue;
    }

    const key = el.getAttribute('name') || el.getAttribute('id');
    if (!key) continue;

    const phLow = (el.getAttribute('placeholder') || '').toLowerCase();
    if (/search|buscar|pesquisar/.test(phLow)) continue;

    const labelOut = getLabel(el);
    const placeholderOut = el.getAttribute('placeholder') || '';
    const required = !!el.required;
    let options = null;
    if (tag === 'select') {
      options = Array.from(el.options || []).map(o => ({ value: o.value, text: o.textContent }));
    }

    out.push({
      key,
      label: labelOut || key,
      type: tag === 'select' ? 'select' : (tag === 'textarea' ? 'textarea' : (type || 'text')),
      placeholder: placeholderOut,
      required,
      options
    });
  }
  return out;
}
"""


def _clean_fields(fields: list) -> list:
    cleaned = []
    for f in fields:
        if not f.get("key"):
            continue
        cleaned.append(f)
    return cleaned


_FILL_FORM_JS = """
(items) => {
  const setBySelector = (sel, v) => {
    const el = document.querySelector(sel);
    if (!el) return;
    if (el.tagName === 'SELECT') {
      const opts = Array.from(el.options || []);
      const found = opts.find(o => String(o.value) === String(v) || String(o.text) === String(v));
      if (found) el.value = found.value;
      return;
    }
    el.value = v;
    const ev = new Event('input', { bubbles: true });
    el.dispatchEvent(ev);
    const ev2 = new Event('change', { bubbles: true });
    el.dispatchEvent(ev2);
  };

  for (const [key, value] of Object.entries(items)) {
    if (value === null || value === undefined) continue;
    const v = String(value);

    setBySelector(`input[name="${key}"]`, v);
    setBySelector(`textarea[name="${key}"]`, v);
    setBySelector(`select[name="${key}"]`, v);
    setBySelector(`input#${key}`, v);
    setBySelector(`textarea#${key}`, v);
    setBySelector(`select#${key}`, v);
  }
}
"""


async def _new_context_with_saved_session(browser):
    """
    Load Playwright storage_state (cookies + localStorage/sessionStorage).
    Falls back to cookies-only dict if an old smile_auth.json format is present.
    """
    if not AUTH_FILE.exists():
        return await browser.new_context(user_agent=_DEFAULT_UA)

    try:
        return await browser.new_context(storage_state=str(AUTH_FILE), user_agent=_DEFAULT_UA)
    except Exception:
        pass

    import json

    try:
        data = json.loads(AUTH_FILE.read_text())
        cookies = data.get("cookies") if isinstance(data, dict) else None
        if cookies:
            return await browser.new_context(storage_state={"cookies": cookies}, user_agent=_DEFAULT_UA)
    except Exception:
        pass

    return await browser.new_context(user_agent=_DEFAULT_UA)


async def do_google_login(headed: bool = True) -> dict:
    """
    Browser ဖွင့်ပြီး Google ဖြင့် login လုပ်သည်။
    User က Google popup မှာ login ဝင်ပြီးရင် session သိမ်းသည်。
    """
    if not async_playwright:
        return {"ok": False, "message": "Playwright not installed. Run: pip install playwright && python -m playwright install chromium"}
    
    async with async_playwright() as p:
        # Google often blocks Playwright's bundled Chromium as "not secure".
        # Prefer system Chrome when set (e.g. Windows: PLAYWRIGHT_CHANNEL=chrome).
        # Linux without /opt/google/chrome/chrome falls back to bundled Chromium (see config).
        channel = playwright_effective_channel()

        if headed:
            # Use a persistent profile to reduce repeated re-auth prompts.
            _pc: dict = {
                "user_data_dir": str(_PROFILE_DIR),
                "headless": False,
                "args": [
                    "--disable-dev-shm-usage",
                    "--no-first-run",
                    "--no-default-browser-check",
                ],
                "user_agent": _DEFAULT_UA,
                "viewport": {"width": 1280, "height": 800},
            }
            if channel:
                _pc["channel"] = channel
            context = await p.chromium.launch_persistent_context(**_pc)
            context.set_default_navigation_timeout(_NAV_TIMEOUT_MS)
            context.set_default_timeout(_NAV_TIMEOUT_MS)
            page = context.pages[0] if context.pages else await context.new_page()
            page.set_default_navigation_timeout(_NAV_TIMEOUT_MS)
            page.set_default_timeout(_NAV_TIMEOUT_MS)
        else:
            _lw: dict = {"headless": True, "args": ["--disable-dev-shm-usage"]}
            if channel:
                _lw["channel"] = channel
            browser = await p.chromium.launch(**_lw)
            context = await browser.new_context(
                user_agent=_DEFAULT_UA,
                viewport={"width": 1280, "height": 800},
            )
            context.set_default_navigation_timeout(_NAV_TIMEOUT_MS)
            context.set_default_timeout(_NAV_TIMEOUT_MS)
            page = await context.new_page()
            page.set_default_navigation_timeout(_NAV_TIMEOUT_MS)
            page.set_default_timeout(_NAV_TIMEOUT_MS)

        try:
            # domcontentloaded — NOT networkidle (often never settles on smile.one).
            entry = _google_login_entry_url()
            try:
                await page.goto(
                    entry,
                    wait_until="domcontentloaded",
                    timeout=_NAV_TIMEOUT_MS,
                )
            except Exception:
                await page.goto(entry, wait_until="load", timeout=_NAV_TIMEOUT_MS)
            await asyncio.sleep(2)
            try:
                await page.wait_for_selector(
                    'a:has-text("Google"), button:has-text("Google")',
                    timeout=_NAV_TIMEOUT_MS,
                )
            except Exception:
                pass

            google_btn = page.locator('text=Google, button:has-text("Google"), a:has-text("Google")').first
            if await google_btn.count() == 0:
                return {"ok": False, "message": "Google login button not found"}
            
            try:
                async with page.expect_popup(timeout=5000) as popup_info:
                    await google_btn.click()
                popup = await popup_info.value
                for _ in range(90):
                    await asyncio.sleep(1)
                    try:
                        if await popup.is_closed():
                            break
                        if "smile.one" in popup.url:
                            break
                    except Exception:
                        break
            except Exception:
                await google_btn.click()
                for _ in range(60):
                    await asyncio.sleep(1)
                    if "smile.one" in page.url and "customer" not in page.url:
                        break
            
            await asyncio.sleep(3)

            # Touch PH storefront + account area so storage_state includes PH region cookies.
            for ph_url in (
                "https://www.smile.one/ph/",
                "https://www.smile.one/ph/customer/account/",
            ):
                try:
                    await page.goto(ph_url, wait_until="domcontentloaded", timeout=_NAV_TIMEOUT_MS)
                    await asyncio.sleep(2)
                except Exception as e:
                    print(f"[do_google_login] PH visit {ph_url} (optional): {e}")

            # Full storage state (cookies + origins/localStorage) — lasts longer than cookies-only.
            await context.storage_state(path=str(AUTH_FILE))

            return {"ok": True, "message": "Login successful! Session saved. You can close the browser."}
            
        except Exception as e:
            return {"ok": False, "message": str(e)[:150]}
        finally:
            try:
                await context.close()
            except Exception:
                pass
            try:
                if not headed:
                    await browser.close()
            except Exception:
                pass


def _cdp_debug_port_open(cdp_url: str) -> bool:
    """Return True if Chrome DevTools answers /json/version (clearer than raw ECONNREFUSED)."""
    import urllib.request

    base = (cdp_url or "").strip().rstrip("/")
    if not base.startswith("http"):
        base = "http://" + base
    for path in ("/json/version", "/json"):
        try:
            with urllib.request.urlopen(base + path, timeout=2.5) as r:
                if getattr(r, "status", None) == 200 or r.getcode() == 200:
                    return True
        except Exception:
            continue
    return False


async def save_session_via_cdp() -> dict:
    """
    Google blocks Playwright-launched Chrome for sign-in. Workaround: start normal Chrome with
    --remote-debugging-port, log in to smile.one manually, then this saves storage_state to smile_auth.json.

    After connecting, we visit Brazil + Philippines URLs so one saved session covers both storefronts.
    """
    if not async_playwright:
        return {"ok": False, "message": "Playwright not installed."}

    cdp_url = os.getenv("PLAYWRIGHT_CDP_URL", "http://127.0.0.1:9222").strip()
    if not cdp_url:
        return {"ok": False, "message": "PLAYWRIGHT_CDP_URL is empty."}

    if not _cdp_debug_port_open(cdp_url):
        return {
            "ok": False,
            "message": (
                f"Nothing is listening on {cdp_url} (Chrome remote debugging is off). "
                "On this PC: close every Chrome window, double-click start_chrome_cdp.bat, wait until Chrome opens, "
                "sign in at smile.one in THAT window only, then click Save session again. "
                "If port 9222 is in use by another app, set PLAYWRIGHT_CDP_URL in .env to another URL."
            ),
        }

    async with async_playwright() as p:
        try:
            browser = await p.chromium.connect_over_cdp(cdp_url, timeout=60000)
        except Exception as e:
            return {
                "ok": False,
                "message": (
                    f"Cannot connect to {cdp_url} even though the port responded — try closing all Chrome windows "
                    "and run start_chrome_cdp.bat again. "
                    f"({str(e)[:120]})"
                ),
            }

        try:
            contexts = browser.contexts
            if not contexts:
                return {"ok": False, "message": "No browser contexts — open Chrome with CDP first."}
            context = contexts[0]
            page = context.pages[0] if context.pages else await context.new_page()

            touch_urls = (
                "https://www.smile.one/br/customer/account/",
                "https://www.smile.one/ph/",
                "https://www.smile.one/ph/customer/account/",
            )
            for u in touch_urls:
                try:
                    await page.goto(u, wait_until="domcontentloaded", timeout=min(_NAV_TIMEOUT_MS, 90_000))
                    await asyncio.sleep(1.8)
                    print(f"[cdp_save] touched {u}")
                except Exception as ex:
                    print(f"[cdp_save] goto {u} (continuing): {ex}")

            await context.storage_state(path=str(AUTH_FILE))
            return {
                "ok": True,
                "message": (
                    "Session saved from Chrome to smile_auth.json (Brazil + Philippines pages were opened once). "
                    "You can close the CDP Chrome window if you want."
                ),
            }
        except Exception as e:
            return {"ok": False, "message": str(e)[:150]}
        finally:
            try:
                await browser.close()
            except Exception:
                pass


def run_save_session_from_cdp() -> dict:
    try:
        from dotenv import load_dotenv

        load_dotenv(Path(__file__).parent / ".env")
    except ImportError:
        pass
    return asyncio.run(save_session_via_cdp())


_SELECT_PACKAGE_JS = """
([targetBrlCents, targetIndex, targetName, targetLiId, targetPhpCents]) => {
  const allBrlCents = (txt) => {
    const all = [...(txt || '').matchAll(/[Rr]\\$\\s*([0-9.,]+)/g)];
    return all.map(m => {
      const n = Number(String(m[1]).replace(/\\./g, '').replace(',', '.'));
      return Number.isNaN(n) ? null : Math.round(n * 100);
    }).filter(v => v !== null);
  };

  const allPhpCents = (txt) => {
    const out = [];
    const re = /(?:\\u20b1|₱)\\s*([0-9][0-9.,]*(?:\\.[0-9]{1,2})?)|PHP\\s*([0-9][0-9.,]*(?:\\.[0-9]{1,2})?)/gi;
    let m;
    const s = txt || '';
    while ((m = re.exec(s)) !== null) {
      const raw = String(m[1] || m[2] || '').replace(/,/g, '');
      const n = Number(raw);
      if (!Number.isNaN(n) && n >= 0) out.push(Math.round(n * 100));
    }
    return out;
  };

  const normalize = (s) => (s || '').replace(/\\u00d7/g, 'x').replace(/[^a-z0-9x+]/gi, '').toLowerCase();

  const normContainsSku = (norm, sku) => {
    if (!sku) return true;
    let pos = norm.indexOf(sku);
    while (pos !== -1) {
      const next = norm.charAt(pos + sku.length);
      if (!(next >= '0' && next <= '9')) return true;
      pos = norm.indexOf(sku, pos + 1);
    }
    return false;
  };

  const rawIsWeeklyDiamondPassLike = (txt) => {
    const u = String(txt || '').toLowerCase();
    if (/weekly\\s*diamond\\s*pass/.test(u)) return true;
    if (/weekly/.test(u) && /pass/.test(u) && (/diamond|gem|bonus/).test(u)) return true;
    if (/passe\\s+semanal\\s+de\\s+diamante/.test(u)) return true;
    if (/passe\\s+semanal/.test(u) && /diamante/.test(u)) return true;
    if (/semanal\\s+de\\s+diamante/.test(u)) return true;
    return false;
  };

  const targetWantsWeeklyDiamondPass = (txt) => {
    const x = String(txt || '').toLowerCase();
    if (/weekly\\s*diamond\\s*pass/.test(x)) return true;
    if (/weekly/.test(x) && /pass/.test(x) && /diamond/.test(x)) return true;
    if (/passe\\s+semanal\\s+de\\s+diamante/.test(x)) return true;
    if (/passe\\s+semanal/.test(x) && /diamante/.test(x)) return true;
    return false;
  };

  const rawIsBareDiamondTopUp = (txt) => {
    if (rawIsWeeklyDiamondPassLike(txt)) return false;
    const u = String(txt || '').replace(/\\u00d7/g, 'x');
    return /diamond\\s*[x×]\\s*\\d{1,7}|diamondx\\d{1,7}|\\d{1,7}\\s+diamonds?\\b/i.test(u);
  };

  const pkgSkuMatches = (raw, norm, targetName) => {
    const t = String(targetName || '').trim();
    if (!t) return true;
    const r = (raw || '').replace(/\\s+/g, ' ').trim();
    if (targetWantsWeeklyDiamondPass(t) && rawIsBareDiamondTopUp(r) && !rawIsWeeklyDiamondPassLike(r)) return false;
    const dB = t.match(/Diamondx(\\d{1,7})/i);
    const dA = t.match(/Diamond\\s*[x×\\u00d7]\\s*(\\d{1,7})/i);
    const dn = dB ? dB[1] : (dA ? dA[1] : null);
    if (dn) {
      const nb = String(parseInt(dn, 10));
      const r1 = new RegExp('Diamond\\\\s*[\\\\u00d7x]\\\\s*' + nb + '(?!\\\\d)', 'i');
      const r2 = new RegExp('Diamondx' + nb + '(?!\\\\d)', 'i');
      const flat = r.replace(/\\u00d7/g, 'x');
      const hasD = r1.test(r) || r2.test(flat);
      if (!hasD) return false;
      if (!targetWantsWeeklyDiamondPass(t) && rawIsWeeklyDiamondPassLike(r)) return false;
      return true;
    }
    if (/twilight/i.test(t)) return /twilight/i.test(r);
    if (targetWantsWeeklyDiamondPass(t)) return rawIsWeeklyDiamondPassLike(r);
    const nw = /weekly/i.test(t);
    const ne = /elite/i.test(t);
    const nm = /monthly/i.test(t);
    const nxp = /epic/i.test(t);
    if (nw && ne) return /weekly/i.test(r) && /elite/i.test(r);
    if (nm && nxp) return /monthly/i.test(r) && /epic/i.test(r);
    if (nw || ne || nm || nxp) {
      let ok = true;
      if (nw) ok = ok && (/weekly/i.test(r) || rawIsWeeklyDiamondPassLike(r));
      if (ne) ok = ok && /elite/i.test(r);
      if (nm) ok = ok && /monthly/i.test(r);
      if (nxp) ok = ok && /epic/i.test(r);
      return ok;
    }
    return normContainsSku(norm, normalize(t));
  };

  const looksLikePackageRow = (txt) => {
    const s = String(txt || '');
    if (s.length > 520) return false;
    return (
      /[Rr]\\$\\s*[\\d]/.test(s)
      || /\\u20b1\\s*[\\d]/.test(s)
      || /PHP\\s*[\\d]/i.test(s)
    );
  };

  const seen = new Set();
  const lis = [];

  const pushIfPackage = (el) => {
    const raw = (el.innerText || '').replace(/\\s+/g, ' ').trim();
    if (!raw) return;
    if (!looksLikePackageRow(raw)) return;
    if (!seen.has(el)) { seen.add(el); lis.push(el); }
  };

  const collectFromSelector = (sel) => {
    try {
      for (const el of document.querySelectorAll(sel)) pushIfPackage(el);
    } catch (e) {}
  };

  const ulRoots = document.querySelectorAll(
    'ul.PcDiamant-ul, ul.commonDiamant-ul, .product-list-container ul'
  );
  for (const ul of ulRoots) {
    try {
      for (const li of ul.querySelectorAll(':scope > li')) pushIfPackage(li);
    } catch (e) {}
  }

  for (const sel of ['ul.PcDiamant-ul > li', 'ul.commonDiamant-ul > li', '.product-list-container ul > li']) {
    collectFromSelector(sel);
  }

  if (!lis.length) {
    const fallbackSels = [
      'ul[class*="PcDiamant"] > li', 'ul[class*="pc-diamant" i] > li',
      'ul[class*="commonDiamant"] > li', 'ul[class*="CommonDiamant"] > li',
      '[class*="product-list-container"] ul > li', '[class*="product-list"] ul > li',
      '[class*="goods-list"] li[class*="item" i]', '[class*="recharge"] ul > li',
      "[class*='PcDiamant'] li", "[class*='diamant'] li", "[class*='Diamant'] li",
    ];
    for (const sel of fallbackSels) collectFromSelector(sel);
  }

  if (!lis.length) {
    const root = document.querySelector('main') || document.querySelector('[class*="merchant"]') || document.body;
    try {
      root.querySelectorAll('li[id]').forEach((li) => pushIfPackage(li));
    } catch (e) {}
  }

  if (!lis.length) {
    const root = document.querySelector('main') || document.querySelector('[class*="merchant"]') || document.body;
    try {
      root.querySelectorAll('[id]').forEach((el) => {
        const tag = (el.tagName || '').toLowerCase();
        if (tag === 'script' || tag === 'style' || tag === 'noscript' || tag === 'link' || tag === 'meta') return;
        pushIfPackage(el);
      });
    } catch (e) {}
  }

  if (!lis.length) return { ok: false, reason: 'no_package_lis' };

  const pkgs = lis.map((li, i) => {
    const raw = (li.innerText || '').replace(/\\s+/g, ' ').trim();
    const prices = allBrlCents(raw);
    const phpPrices = allPhpCents(raw);
    return { li, raw, norm: normalize(raw), prices, phpPrices, idx: i, liId: li.id || '' };
  });

  const tn = String(targetName || '').trim();
  const hasBrlCents = targetBrlCents !== null && targetBrlCents !== undefined;
  const hasPhpCents = targetPhpCents !== null && targetPhpCents !== undefined;
  const hasAnyPrice = hasBrlCents || hasPhpCents;

  const rowMatchesTargetPrice = (p) => {
    if (hasBrlCents && p.prices && p.prices.includes(targetBrlCents)) return true;
    if (hasPhpCents && p.phpPrices && p.phpPrices.includes(targetPhpCents)) return true;
    return false;
  };

  // Strict: when targetLiId points to a known DOM li, verify BOTH price AND name match.
  if (targetLiId) {
    const byId = document.getElementById(String(targetLiId));
    if (!byId) {
      return { ok: false, reason: 'catalog_li_id_not_found', count: lis.length };
    }
    const ix = lis.indexOf(byId);
    if (ix < 0) {
      return { ok: false, reason: 'catalog_li_id_outside_packages', count: lis.length };
    }
    const rec = pkgs[ix];
    if (hasAnyPrice && !rowMatchesTargetPrice(rec)) {
      return { ok: false, reason: 'catalog_li_id_price_mismatch', count: lis.length };
    }
    if (tn && !pkgSkuMatches(rec.raw, rec.norm, targetName)) {
      return { ok: false, reason: 'catalog_li_id_name_mismatch', count: lis.length };
    }
    return {
      ok: true,
      liId: byId.id,
      text: rec.raw.slice(0, 150),
      matchedPrices: rec.prices,
      index: ix,
      matchStep: 'li_id_strict',
    };
  }

  let chosen = null;
  let matchStep = '';

  if (tn && hasPhpCents) {
    const matches = pkgs.filter((p) => p.phpPrices.includes(targetPhpCents) && pkgSkuMatches(p.raw, p.norm, targetName));
    if (matches.length === 1) { chosen = matches[0]; matchStep = 'php_price+name'; }
  }
  if (!chosen && tn && hasBrlCents) {
    const matches = pkgs.filter((p) => p.prices.includes(targetBrlCents) && pkgSkuMatches(p.raw, p.norm, targetName));
    if (matches.length === 1) {
      chosen = matches[0]; matchStep = 'brl_price+name';
    } else if (matches.length > 1 && targetIndex !== null && targetIndex !== undefined && !Number.isNaN(Number(targetIndex))) {
      const ti = Math.max(0, Math.min(pkgs.length - 1, Number(targetIndex)));
      chosen = matches.find((p) => p.idx === ti);
      if (chosen) matchStep = 'brl_price+name+index';
    }
  }

  if (!chosen && hasPhpCents) {
    const byPrice = pkgs.filter((p) => p.phpPrices.includes(targetPhpCents));
    if (byPrice.length === 1) { chosen = byPrice[0]; matchStep = 'unique_php_price'; }
  }
  if (!chosen && hasBrlCents) {
    const byPrice = pkgs.filter((p) => p.prices.includes(targetBrlCents));
    if (byPrice.length === 1) { chosen = byPrice[0]; matchStep = 'unique_brl_price'; }
  }

  if (!chosen && !hasAnyPrice && tn) {
    const matches = pkgs.filter((p) => pkgSkuMatches(p.raw, p.norm, targetName));
    if (matches.length === 1) {
      chosen = matches[0]; matchStep = 'name_only_unique';
    } else if (matches.length > 1 && targetIndex !== null && targetIndex !== undefined && !Number.isNaN(Number(targetIndex))) {
      const ti = Math.max(0, Math.min(pkgs.length - 1, Number(targetIndex)));
      chosen = matches.find((p) => p.idx === ti);
      if (chosen) matchStep = 'name_only+index';
    }
  }

  if (!chosen) {
    let reason = 'no_match';
    if (hasAnyPrice) {
      const n = pkgs.filter((p) => rowMatchesTargetPrice(p)).length;
      if (n > 1) reason = 'ambiguous_price';
      else if (n === 0) reason = 'price_not_found';
    }
    return { ok: false, reason, count: lis.length };
  }

  return {
    ok: true,
    liId: chosen.liId,
    text: chosen.raw.slice(0, 150),
    matchedPrices: chosen.prices,
    index: chosen.idx,
    matchStep
  };
}
"""

_CLICK_PAYMENT_JS = """
() => {
  const cards = Array.from(document.querySelectorAll('[class*="sectionNav-cartao"], [class*="sectionnav-cartao"]'));
  if (!cards.length) return { ok: false, reason: 'no_payment_methods' };

  const low = (s) => (s || '').toLowerCase();
  let sc = cards.find(c => low(c.className).includes('smilecoin'));
  if (!sc) {
    for (const c of cards) {
      const inner = c.querySelector('[class*="smilecoin"], [class*="SmileCoin"]');
      if (inner) { sc = inner; break; }
    }
  }
  if (!sc) {
    sc = cards.find(c => /smile\\s*coin/i.test((c.innerText || '')));
  }
  if (sc) { sc.click(); return { ok: true, method: 'Smile Coin' }; }

  return { ok: false, reason: 'no_smile_coin_option' };
}
"""

_CHECK_LOGIN_JS = """
() => {
  // Check for user-name / account-name elements FIRST — these prove login even
  // when .login-btn still shows "Entrar" (smile.one keeps that element visible).
  const userEl = document.querySelector('[class*="user-name"], [class*="userName"], [class*="nickname"], [class*="account-name"]');
  if (userEl && (userEl.innerText || '').trim().length > 0) return true;
  const norm = (s) => (s || '').trim().toLowerCase().replace(/\\s+/g, ' ');
  const isLoginCTA = (t) => t === 'entrar' || t === 'login' || t === 'log in' || t === 'sign in';
  const headerLogin = document.querySelector('.login-text');
  if (headerLogin) {
    const t = norm(headerLogin.innerText);
    if (t.length > 0 && !isLoginCTA(t) && !t.includes('entrar') && !t.includes('login')) return true;
  }
  // PH storefront often uses "Log in" (two words) on .login-btn — normalize whitespace.
  const loginBtn = document.querySelector('.login-btn');
  if (loginBtn) {
    const t = norm(loginBtn.innerText);
    if (isLoginCTA(t)) return false;
  }
  if (headerLogin) {
    const t = norm(headerLogin.innerText);
    if (t.includes('entrar') || t.includes('login') || isLoginCTA(t)) return false;
  }
  return null;
}
"""

# Step 6: only treat as success when we see clear signals — never default to ok=True on ambiguous UI.
_PH_OPTIONAL_VERIFY_CLICK_JS = """
() => {
  const cand = Array.from(document.querySelectorAll(
    'button, a[role="button"], [role="button"], input[type="submit"], .Nav-btn, .el-button--primary, .btn-primary'
  ));
  const re = /verify|check\\s*player|confirm\\s*id|validate|check\\s*id|query|search\\s*role|ok(?![a-z])/i;
  for (const el of cand) {
    if (!el || !el.getClientRects().length) continue;
    const t = (el.innerText || el.textContent || '').replace(/\\s+/g, ' ').trim();
    if (!t || t.length > 48) continue;
    if (re.test(t)) {
      try { el.click(); return true; } catch (e) {}
    }
  }
  return false;
}
"""

_PAYMENT_STRIP_SELECTOR = (
    '[class*="sectionNav-cartao"], .payment-button-container, '
    '[class*="Nav-footer"], [class*="payment-button"]'
)

# Cross-checks the SKU that ended up active vs the price/name the user actually purchased.
# Run twice in auto_order: once after package click, once again right before clicking Buy.
_VERIFY_SELECTION_JS = """
([liId, targetBrlCents, targetPhpCents, targetName]) => {
  const allBrlCents = (txt) => {
    const all = [...(txt || '').matchAll(/[Rr]\\$\\s*([0-9.,]+)/g)];
    return all.map(m => {
      const n = Number(String(m[1]).replace(/\\./g,'').replace(',','.'));
      return Number.isNaN(n) ? null : Math.round(n*100);
    }).filter(v => v !== null);
  };
  const allPhpCents = (txt) => {
    const arr = []; const re = /(?:\\u20b1|₱)\\s*([0-9][0-9.,]*(?:\\.[0-9]{1,2})?)|PHP\\s*([0-9][0-9.,]*(?:\\.[0-9]{1,2})?)/gi;
    let m; const s = txt || '';
    while ((m = re.exec(s)) !== null) {
      const raw = String(m[1] || m[2] || '').replace(/,/g, '');
      const n = Number(raw);
      if (!Number.isNaN(n) && n >= 0) arr.push(Math.round(n*100));
    }
    return arr;
  };
  const normalize = (s) => (s || '')
    .replace(/[\\u00d7\\u2715\\u2a2f\\u2716×]/g, 'x')
    .replace(/[^a-z0-9x+]/gi, '').toLowerCase();
  const stripped = normalize(targetName || '');
  const isActive = (el) => {
    if (!el) return false;
    const cls = (typeof el.className === 'string' ? el.className : String(el.className || '')).toLowerCase();
    if (/(^|\\s)(selected|active|current|on)(\\s|$)/.test(cls)) return true;
    if (/is-active|is-selected/.test(cls)) return true;
    if (el.getAttribute && el.getAttribute('aria-selected') === 'true') return true;
    return false;
  };
  const out = { ok: false, activeOk: false, summaryPriceMatch: null, summaryNameMatch: null };
  const byId = liId ? document.getElementById(String(liId)) : null;
  if (byId) {
    if (isActive(byId)) out.activeOk = true;
    else {
      const inner = byId.querySelector('.selected, .active, [aria-selected="true"], [class*="checked"]');
      if (inner) out.activeOk = true;
    }
  }
  const sels = [
    '.product-list-container',
    '[class*="order-summary"]',
    '[class*="OrderSummary"]',
    '[class*="payment-button-container"]',
    '[class*="Nav-footer"]',
    '[class*="check-info"]',
    '[class*="checkInfo"]',
    '[class*="total"]'
  ];
  let buf = ''; const seen = new Set();
  for (const s of sels) {
    for (const el of document.querySelectorAll(s)) {
      if (!el || !el.getClientRects().length) continue;
      const t = (el.innerText || '').replace(/\\s+/g, ' ').trim();
      if (!t || seen.has(t)) continue;
      seen.add(t); buf += ' | ' + t;
    }
  }
  buf = buf.slice(0, 2000);
  const hasBrl = targetBrlCents !== null && targetBrlCents !== undefined;
  const hasPhp = targetPhpCents !== null && targetPhpCents !== undefined;
  if (hasBrl) out.summaryPriceMatch = allBrlCents(buf).includes(Number(targetBrlCents));
  else if (hasPhp) out.summaryPriceMatch = allPhpCents(buf).includes(Number(targetPhpCents));
  if (stripped) out.summaryNameMatch = normalize(buf).includes(stripped);
  out.ok = !!(out.activeOk || out.summaryPriceMatch === true);
  return out;
}
"""

_PH_TOAST_TEXT_JS = """
() => {
  const out = [];
  const pick = (sel) => {
    for (const el of document.querySelectorAll(sel)) {
      if (!el || !el.getClientRects().length) continue;
      const t = (el.innerText || '').replace(/\\s+/g, ' ').trim();
      if (t.length > 2 && t.length < 800) out.push(t);
    }
  };
  pick('.el-message__content, .el-message--success, .el-message--error, .el-notification__content, .el-notification--success, .el-notification--error');
  pick('.el-dialog__body, .el-message-box__message, [class*="message-box__message"]');
  pick('.van-toast, .van-notify');
  pick('[class*="toast"], [class*="Toast"], [class*="notification--success"], [class*="notification--error"]');
  return out.slice(0, 12);
}
"""

_ORDER_BLOCKER_JS = """
() => {
  function visible(el) {
    if (!el) return false;
    const r = el.getBoundingClientRect();
    const st = window.getComputedStyle(el);
    if (st.visibility === "hidden" || st.display === "none" || parseFloat(st.opacity) < 0.05)
      return false;
    return r.width > 80 && r.height > 60;
  }
  // Challenge iframe only — the small anchor iframe is on many pages and must not block.
  const bframe = document.querySelector('iframe[src*="bframe"]');
  if (visible(bframe)) return { block: true, reason: "recaptcha_challenge" };
  const t = ((document.body && document.body.innerText) || "").toLowerCase();
  if (t.includes("verify you are human") || t.includes("verify you're not a robot")) {
    return { block: true, reason: "verify_text" };
  }
  return { block: false };
}
"""


def _body_text_indicates_order_success(page_lower: str, is_ph: bool) -> bool:
    """Match purchase-complete copy; avoid bare English 'success' (footer/marketing noise)."""
    en = (
        "order completed",
        "thank you for your order",
        "thank you for your purchase",
        "purchase successful",
        "successfully purchased",
        "payment successful",
        "your order has been placed",
        "your order was successful",
        "order placed successfully",
        "payment complete",
        "order confirmation",
    )
    pt = (
        "pedido realizado",
        "pedido conclu",
        "compra realizada",
        "pagamento aprovado",
        "pagamento com sucesso",  # BR success page: "Pagamento com sucesso!"
        "obrigado pela compra",
        "obrigado por sua compra",
        "realizado com sucesso",
        "com sucesso!",
        "foi realizado com sucesso",
    )
    ph_extra = (
        "purchase successful",
        "successfully purchased",
        "order placed successfully",
        "payment successful",
        "your order has been",
        "top-up successful",
        "top up successful",
        "recharge successful",
        "successfully recharged",
        "diamonds will be sent",
        "diamonds have been",
        "delivery successful",
        "recharge completed",
        "top-up completed",
        "order has been submitted",
        "submission successful",
        "payment is successful",
    )
    if any(p in page_lower for p in en):
        return True
    if any(p in page_lower for p in pt):
        return True
    if is_ph and any(p in page_lower for p in ph_extra):
        return True
    return False


def _url_indicates_checkout_success(url: str) -> bool:
    """Do not use generic 'order' — product URLs often contain it."""
    u = (url or "").lower()
    markers = (
        "/message/success",
        "/success",
        "checkout/success",
        "order-success",
        "order_success",
        "pedido-sucesso",
        "payment-success",
        "ordercomplete",
        "order_complete",
        "/receipt",
        "thank-you",
        "thankyou",
        "/sucesso",
        "order-confirmation",
        "pedido/confirm",
    )
    return any(m in u for m in markers)


def _body_text_indicates_payment_failure(page_lower: str) -> bool:
    phrases = (
        "payment failed",
        "order failed",
        "transação falhou",
        "pagamento falhou",
        "rejeitado",
        "transaction declined",
        "payment was declined",
        "unable to complete",
        "could not complete your order",
        "erro ao processar",
        "não foi possível concluir",
    )
    if any(p in page_lower for p in phrases):
        return True
    # generic "error" alone is too noisy; require checkout-ish context
    if "error" in page_lower and any(
        x in page_lower for x in ("payment", "order", "checkout", "pagamento", "pedido", "transação")
    ):
        return True
    if any(p in page_lower for p in ("falhou", "erro")) and any(
        x in page_lower for x in ("pagamento", "pedido", "compra", "checkout", "order")
    ):
        return True
    return False


async def _evaluate_checkout_outcome(page, is_ph: bool) -> dict:
    """
    Poll the page a few times (SPA / slow render), then return ok only on strong success signals.
    """
    try:
        await page.wait_for_load_state("domcontentloaded", timeout=10_000)
    except Exception:
        pass

    _out_attempts = 6 if is_ph else 4
    for attempt in range(_out_attempts):
        if attempt:
            await asyncio.sleep(0.85)
        if is_ph and attempt:
            try:
                await page.evaluate(
                    "() => { window.scrollTo(0, Math.min(document.body.scrollHeight || 0, 2000)); }"
                )
                await asyncio.sleep(0.4)
            except Exception:
                pass

        try:
            blocker = await page.evaluate(_ORDER_BLOCKER_JS)
        except Exception:
            blocker = {"block": False}

        if isinstance(blocker, dict) and blocker.get("block"):
            return {
                "ok": False,
                "message": (
                    "smile.one showed a verification step (reCAPTCHA). Automated checkout cannot complete it. "
                    "Please try again later or contact admin."
                ),
                "step": "recaptcha",
            }

        current_url = (page.url or "").lower()
        if "/customer/account/login" in current_url:
            msg = (
                "Session was lost during checkout (redirected to login). "
                "Please ask admin to renew the smile.one session."
            )
            if is_ph:
                msg += (
                    " For PH orders, admin should run Google login once and let the hub open "
                    "https://www.smile.one/ph/ before saving the session (cookies must include the PH storefront)."
                )
            return {
                "ok": False,
                "message": msg,
                "step": "session_lost_checkout",
            }

        _slice = 45000 if is_ph else 14000
        try:
            page_text = await page.evaluate(
                "() => (document.body && document.body.innerText) ? document.body.innerText.slice(0, %d) : ''"
                % _slice
            )
        except Exception:
            page_text = ""
        page_lower = (page_text or "").lower()

        if is_ph:
            try:
                _toast_chunks = await page.evaluate(_PH_TOAST_TEXT_JS)
            except Exception:
                _toast_chunks = []
            for _tc in _toast_chunks or []:
                _tl = (_tc or "").lower()
                if _body_text_indicates_order_success(_tl, is_ph):
                    return {"ok": True, "message": "Order completed successfully!", "step": "done"}
                if _body_text_indicates_payment_failure(_tl):
                    return {
                        "ok": False,
                        "message": "Order failed. Please try again or contact admin.",
                        "step": "payment_failed",
                    }
                if is_ph and (
                    ("invalid" in _tl and any(x in _tl for x in ("user id", "player id", "zone id", "role id", "character")))
                    or ("does not exist" in _tl and "player" in _tl)
                    or ("incorrect" in _tl and ("user" in _tl or "zone" in _tl))
                ):
                    return {
                        "ok": False,
                        "message": "smile.one rejected the Game ID or Zone. Verify PH MLBB User ID and Zone ID.",
                        "step": "invalid_account_fields",
                    }

        if "saldo insuficiente" in page_lower:
            return {
                "ok": False,
                "message": (
                    "Insufficient Smile Coin balance on the smile.one account used for orders. "
                    "Top up Smile Coins there or ask admin to recharge; Hub wallet is refunded when the order fails."
                ),
                "step": "insufficient_balance",
            }
        if "insufficient" in page_lower and any(
            x in page_lower for x in ("balance", "coin", "saldo", "smile coin", "smilecoin", "fund")
        ):
            return {
                "ok": False,
                "message": (
                    "Insufficient Smile Coin balance on the smile.one account used for orders. "
                    "Top up Smile Coins there or ask admin to recharge; Hub wallet is refunded when the order fails."
                ),
                "step": "insufficient_balance",
            }

        if _body_text_indicates_order_success(page_lower, is_ph):
            return {"ok": True, "message": "Order completed successfully!", "step": "done"}

        if _url_indicates_checkout_success(current_url):
            return {"ok": True, "message": "Order completed successfully!", "step": "done"}

        if _body_text_indicates_payment_failure(page_lower):
            return {
                "ok": False,
                "message": "Order failed. Please try again or contact admin.",
                "step": "payment_failed",
            }

        try:
            _logged_in = await page.evaluate(_CHECK_LOGIN_JS)
        except Exception:
            _logged_in = None
        if _logged_in is False:
            return {
                "ok": False,
                "message": (
                    "The smile.one session was lost during checkout (product page may still show). "
                    "Admin should refresh smile_auth.json login and Smile Coin balance."
                ),
                "step": "session_lost_checkout",
            }

        if is_ph and (
            ("invalid" in page_lower and any(x in page_lower for x in ("user id", "player id", "zone id", "role id", "character")))
            or ("does not exist" in page_lower and "player" in page_lower)
            or ("incorrect" in page_lower and ("user" in page_lower or "zone" in page_lower))
        ):
            return {
                "ok": False,
                "message": "smile.one rejected the Game ID or Zone. Verify PH MLBB User ID and Zone ID.",
                "step": "invalid_account_fields",
            }

        if is_ph and "please select package" in page_lower:
            return {
                "ok": False,
                "message": "Checkout did not leave package step — order was not confirmed on smile.one. Try again.",
                "step": "ph_checkout_stuck",
            }

    return {
        "ok": False,
        "message": (
            "Could not confirm the order on smile.one — no clear success page was detected. "
            "Check your smile.one order history; if nothing new appeared, try again."
        ),
        "step": "checkout_uncertain",
    }


async def auto_order(
    product_url: str,
    package_brl_cents=None,
    package_php_cents=None,
    package_name: str | None = None,
    package_index=None,
    use_saved_session: bool = True,
    form_data: dict | None = None,
    smile_li_id: str | None = None,
) -> dict:
    """
    smile.one auto-order flow (Smile Coins only):
      1. Load product page with admin's saved session
      2. Fill Game ID / Server ID from customer form_data
      3. Click the correct package LI
      4. Select Smile Coin payment
      5. Click "Comprar agora"
      6. Handle confirmation popup
    """
    if not async_playwright:
        return {"ok": False, "message": "Server config error: browser engine missing.", "step": "setup"}

    if "smile.one" not in product_url:
        return {"ok": False, "message": "Invalid product URL.", "step": "input"}

    if use_saved_session and not AUTH_FILE.exists():
        return {
            "ok": False,
            "message": "Order service is not ready yet. Please contact admin.",
            "step": "session_missing",
        }

    async with async_playwright() as p:
        launch_kw = {"headless": True}
        if (os.getenv("SMILE_AUTO_ORDER_USE_CHROME") or "").strip().lower() in ("1", "true", "yes"):
            ch = playwright_effective_channel()
            if ch:
                launch_kw["channel"] = ch
        browser = await p.chromium.launch(**launch_kw)

        if use_saved_session and AUTH_FILE.exists():
            context = await _new_context_with_saved_session(browser)
        else:
            context = await browser.new_context(user_agent=_DEFAULT_UA)

        context.set_default_navigation_timeout(45_000)
        context.set_default_timeout(30_000)

        # Block heavy resources we don't need for the order flow. Saves several
        # seconds per order on slow networks; XHR / scripts / CSS still load so
        # smile.one's React app and payment strip render correctly.
        _BLOCK_HOSTS = (
            "googletagmanager.com",
            "google-analytics.com",
            "googletagservices.com",
            "doubleclick.net",
            "facebook.com",
            "facebook.net",
            "connect.facebook.net",
            "tiktok.com",
            "hotjar.com",
            "clarity.ms",
            "twitter.com",
            "x.com",
            "youtube.com",
            "ytimg.com",
        )

        async def _resource_filter(route, request):
            try:
                rtype = request.resource_type
                if rtype in ("image", "media", "font"):
                    await route.abort()
                    return
                url_lo = (request.url or "").lower()
                if any(h in url_lo for h in _BLOCK_HOSTS):
                    await route.abort()
                    return
                await route.continue_()
            except Exception:
                try:
                    await route.continue_()
                except Exception:
                    pass

        try:
            await context.route("**/*", _resource_filter)
        except Exception as _re:
            print(f"[auto_order] resource filter setup skipped: {_re}")

        page = await context.new_page()
        page.set_default_navigation_timeout(45_000)
        page.set_default_timeout(30_000)

        def _on_dialog(dialog):
            try:
                asyncio.create_task(dialog.accept())
            except Exception:
                pass

        page.on("dialog", _on_dialog)

        try:
            is_ph = "/ph/" in (product_url or "").lower()
            # Brazil MLBB shares PH-style lazy diamond grid + sticky checkout; treat like PH for scroll / second tap.
            is_mlbb_br = (not is_ph) and ("mobilelegends" in (product_url or "").lower())

            # ── Step 0: Navigate ──
            # Saved storage_state already covers smile.one cookies → only do the priming
            # visit when the file is missing the cookie for the storefront host.
            if is_ph and not _auth_file_has_host("www.smile.one"):
                try:
                    await page.goto(
                        "https://www.smile.one/ph/",
                        wait_until="domcontentloaded",
                        timeout=30_000,
                    )
                    await asyncio.sleep(0.15)
                except Exception as e:
                    print(f"[auto_order] PH priming (cookie warm): {e}")
            print(f"[auto_order] Navigating to {product_url}")
            await page.goto(product_url, wait_until="domcontentloaded", timeout=60_000)
            try:
                await page.wait_for_selector(
                    "ul.PcDiamant-ul li, ul.commonDiamant-ul li, .product-list-container li, "
                    "[class*='PcDiamant'] li, [class*='diamant'] li",
                    timeout=20_000 if is_ph else 10_000,
                )
            except Exception:
                await asyncio.sleep(1.2)

            # PH merchant is an SPA: header/login widgets often hydrate after "load".
            # A single immediate _CHECK_LOGIN_JS can read False then turn True ~1–3s later.
            login_state = None
            _login_attempts = 12 if is_ph else 8
            for _ls_attempt in range(_login_attempts):
                login_state = await page.evaluate(_CHECK_LOGIN_JS)
                if login_state is not False:
                    break
                await asyncio.sleep(0.3)
            print(f"[auto_order] login_state = {login_state}")
            if login_state is False:
                return {
                    "ok": False,
                    "message": "Order service session expired. Please contact admin to renew.",
                    "step": "session_expired",
                }

            # Split metadata from real account fields — never try to fill __smile_li_id__ into the page
            fd_for_fill: dict = {}
            if form_data and isinstance(form_data, dict):
                fd_for_fill = {k: v for k, v in form_data.items() if k and not str(k).startswith("__")}
            meta_li = ""
            if form_data and isinstance(form_data, dict):
                v = form_data.get("__smile_li_id__")
                if v is not None:
                    meta_li = str(v).strip()
            ext_li = (smile_li_id or "").strip() if smile_li_id else ""
            target_li_id_resolved = ext_li or meta_li or None

            # Hub form may send user_id/zone_id (PH snapshot in manual_account_fields_scraped.json).
            # BR smile.one still expects uid/zone inputs — sync into game_id/server_id for _br_pairs.
            if fd_for_fill and not is_ph:
                if not str(fd_for_fill.get("game_id") or "").strip() and str(fd_for_fill.get("user_id") or "").strip():
                    fd_for_fill["game_id"] = fd_for_fill["user_id"]
                if not str(fd_for_fill.get("server_id") or "").strip() and str(fd_for_fill.get("zone_id") or "").strip():
                    fd_for_fill["server_id"] = fd_for_fill["zone_id"]

            # ── Step 1: Fill Game ID / Server ID ──
            if fd_for_fill:
                for key, value in fd_for_fill.items():
                    if value is None:
                        continue
                    val = str(value).strip()
                    if not val:
                        continue
                    filled = False
                    sels = [f"#{key}", f"input[name='{key}']", f"input[placeholder*='{key}' i]"]
                    # PH MLBB storefront uses #user_id / #zone_id (not game_id / server_id).
                    if is_ph and key == "game_id":
                        sels = ["#user_id", "input[placeholder*='USER ID' i]"] + sels
                    if is_ph and key == "server_id":
                        sels = ["#zone_id", "input[placeholder*='ZONE ID' i]"] + sels
                    for sel in sels:
                        try:
                            loc = page.locator(sel).first
                            if await loc.count() > 0 and await loc.is_visible():
                                await loc.click()
                                await loc.fill(val)
                                await asyncio.sleep(0.12)
                                filled = True
                                break
                        except Exception:
                            continue
                    if not filled:
                        try:
                            await page.evaluate(_FILL_FORM_JS, {key: val})
                        except Exception:
                            pass
                # BR /global: many merchants use uid/zone instead of game_id/server_id on the DOM.
                if not is_ph and fd_for_fill:
                    _br_pairs = [
                        (
                            str(fd_for_fill.get("game_id") or "").strip(),
                            ("uid", "player_id", "user_id", "playerId", "account_id", "role_id"),
                        ),
                        (
                            str(fd_for_fill.get("server_id") or "").strip(),
                            ("zone_id", "zone", "server", "world", "area_id"),
                        ),
                        (
                            str(fd_for_fill.get("email") or "").strip(),
                            ("email", "mail", "user_email", "recipient"),
                        ),
                        (
                            str(fd_for_fill.get("phone_number") or "").strip(),
                            ("phone", "mobile", "msisdn", "telephone", "number"),
                        ),
                    ]
                    for val, names in _br_pairs:
                        if not val:
                            continue
                        for nm in names:
                            try:
                                loc = page.locator(f"input[name='{nm}']").first
                                if await loc.count() > 0 and await loc.is_visible():
                                    await loc.click()
                                    await loc.fill(val)
                                    await asyncio.sleep(0.08)
                                    break
                            except Exception:
                                continue
                # PH MLBB: inputs may be name="uid" / name="zone_id" instead of game_id / server_id.
                if is_ph and fd_for_fill:
                    uid = str(fd_for_fill.get("game_id") or "").strip()
                    zid = str(fd_for_fill.get("server_id") or "").strip()
                    for nm, v in (
                        ("uid", uid),
                        ("user_id", uid),
                        ("player_id", uid),
                        ("zone_id", zid),
                        ("zone", zid),
                    ):
                        if not v:
                            continue
                        try:
                            loc = page.locator(f"input[name='{nm}']").first
                            if await loc.count() > 0 and await loc.is_visible():
                                await loc.click()
                                await loc.fill(v)
                                await asyncio.sleep(0.08)
                                print(f"[auto_order] PH fill input[name={nm}]")
                        except Exception:
                            pass
                await asyncio.sleep(min(ORDER_SLEEP_AFTER_FILL_SEC, 0.25))
                if fd_for_fill and (is_ph or is_mlbb_br):
                    try:
                        await page.evaluate(
                            """() => {
                              document.querySelectorAll('input, textarea, select').forEach((el) => {
                                try { el.dispatchEvent(new Event('blur', { bubbles: true })); } catch (e) {}
                              });
                            }"""
                        )
                    except Exception:
                        pass
                    await asyncio.sleep(0.25)
                    if is_ph and fd_for_fill:
                        try:
                            _v = await page.evaluate(_PH_OPTIONAL_VERIFY_CLICK_JS)
                            if _v:
                                print("[auto_order] PH optional verify / check ID click")
                                await asyncio.sleep(0.6)
                        except Exception as _ev:
                            print(f"[auto_order] PH verify (optional): {_ev}")
                    if is_ph and fd_for_fill:
                        for zsel in ("#zone_id", "input[name='zone_id']", "input[name='server_id']"):
                            try:
                                zloc = page.locator(zsel).first
                                if await zloc.count() > 0 and await zloc.is_visible():
                                    await zloc.press("Enter")
                                    print(f"[auto_order] PH pressed Enter on {zsel}")
                                    await asyncio.sleep(0.9)
                                    break
                            except Exception:
                                continue

            # PH / MLBB BR: scroll before waiting on li# — grid is lazy; visibility wait often times out headless.
            if is_ph or is_mlbb_br:
                try:
                    await page.evaluate(PH_SCROLL_PRIMING_JS)
                except Exception as e:
                    print(f"[auto_order] package-grid scroll priming (pre-li): {e}")
                await asyncio.sleep(0.35)

            if is_ph and fd_for_fill:
                _ruid = str(fd_for_fill.get("game_id") or "").strip()
                _rzid = str(fd_for_fill.get("server_id") or "").strip()
                if _ruid and _rzid:
                    for _sel, _val in (("#user_id", _ruid), ("#zone_id", _rzid)):
                        try:
                            _loc = page.locator(_sel).first
                            if await _loc.count() == 0:
                                continue
                            _cur = (await _loc.input_value()).strip()
                            if _cur != _val:
                                await _loc.click()
                                await _loc.fill(_val)
                                await asyncio.sleep(0.12)
                                print(f"[auto_order] PH re-filled {_sel} (was empty or stale)")
                        except Exception:
                            pass
                    await asyncio.sleep(0.3)

            # UID / zone often triggers a re-render — wait for target package row if we know its id
            if target_li_id_resolved:
                _tid = str(target_li_id_resolved).replace('"', '\\"')
                _li_wait_sel = f'[id="{_tid}"]' if is_ph else f'li[id="{_tid}"]'
                try:
                    if is_ph or is_mlbb_br:
                        await page.wait_for_selector(
                            _li_wait_sel,
                            timeout=15_000,
                            state="attached",
                        )
                    else:
                        await page.wait_for_selector(
                            _li_wait_sel,
                            timeout=10_000,
                            state="visible",
                        )
                except Exception as e:
                    print(f"[auto_order] wait_for_selector package id: {e}")
                await asyncio.sleep(0.2)

            # ── Step 2: Select package ──
            target_cents = int(package_brl_cents) if package_brl_cents is not None else None
            target_php_cents = int(package_php_cents) if package_php_cents is not None else None
            target_idx = int(package_index) if package_index is not None else None
            target_name = (package_name or "").strip()
            target_li_id = target_li_id_resolved

            print(
                f"[auto_order] Selecting package: brl_cents={target_cents}, php_cents={target_php_cents}, "
                f"name='{target_name}', idx={target_idx}, li_id={target_li_id}"
            )
            _pkg_js = SELECT_PACKAGE_PH_JS if is_ph else _SELECT_PACKAGE_JS
            _pkg_args = [target_cents, target_idx, target_name, target_li_id, target_php_cents]
            sel_ctx = page.main_frame
            pkg_result = await sel_ctx.evaluate(_pkg_js, _pkg_args)
            if (not (pkg_result or {}).get("ok")) and is_ph and (pkg_result or {}).get("reason") == "no_package_lis":
                for fr in list(page.frames):
                    if fr is page.main_frame:
                        continue
                    try:
                        alt = await fr.evaluate(_pkg_js, _pkg_args)
                        print(f"[auto_order] package eval in frame {fr.url}: {alt}")
                        if alt.get("ok"):
                            pkg_result = alt
                            sel_ctx = fr
                            break
                    except Exception as exc:
                        print(f"[auto_order] package frame skip: {exc}")
            print(f"[auto_order] Package selection result: {pkg_result}")
            if not pkg_result or not pkg_result.get("ok"):
                _reason = (pkg_result or {}).get("reason") or "no_match"
                _reason_map = {
                    "no_package_lis": (
                        "smile.one did not load any packages on the product page. "
                        "Please refresh and try again."
                    ),
                    "catalog_li_id_not_found": (
                        "The selected package row was not found on smile.one "
                        "(catalog may be outdated). Refresh the product page and try again."
                    ),
                    "catalog_li_id_outside_packages": (
                        "smile.one returned the package element outside the expected list. "
                        "Please refresh and try again."
                    ),
                    "catalog_li_id_mismatch": (
                        "smile.one's package row no longer matches your selection. "
                        "Please refresh the product page and try again."
                    ),
                    "catalog_li_id_price_mismatch": (
                        "Price mismatch: smile.one shows a different price for this package now. "
                        "Please refresh the product page and re-place the order."
                    ),
                    "catalog_li_id_name_mismatch": (
                        "Package name mismatch: smile.one shows a different SKU for this row now. "
                        "Please refresh the product page and re-place the order."
                    ),
                    "ambiguous_price": (
                        "Multiple packages share the same price on smile.one — "
                        "auto-order refused to guess. Please contact admin."
                    ),
                    "ambiguous_php_price": (
                        "Multiple packages share the same ₱ price on smile.one — "
                        "auto-order refused to guess. Please contact admin."
                    ),
                    "price_not_found": (
                        "smile.one no longer has a package at the price you ordered. "
                        "Please refresh the product page and try again."
                    ),
                }
                return {
                    "ok": False,
                    "message": _reason_map.get(
                        _reason,
                        "Could not find the selected package on smile.one. Please refresh and try again.",
                    ),
                    "step": "select_package",
                    "detail": {"reason": _reason},
                }

            # Use Playwright's click (simulates real user interaction) instead of JS click
            li_id = pkg_result.get("liId", "")
            pkg_idx = pkg_result.get("index")
            clicked_pkg = False
            if li_id:
                try:
                    _id_css = f'[id="{li_id}"]' if is_ph else f'li[id="{li_id}"]'
                    loc = sel_ctx.locator(_id_css).first
                    await loc.wait_for(state="visible", timeout=10_000)
                    await loc.scroll_into_view_if_needed()
                    await loc.click(timeout=12_000, force=(is_ph or is_mlbb_br))
                    clicked_pkg = True
                    print(f"[auto_order] Clicked package via Playwright {_id_css}")
                except Exception as e:
                    print(f"[auto_order] Playwright click by id failed: {e}")

            if not clicked_pkg and pkg_idx is not None:
                try:
                    sel = PH_PACKAGE_LI_LOCATOR if is_ph else "ul.PcDiamant-ul > li, ul.commonDiamant-ul > li"
                    loc = sel_ctx.locator(sel).nth(pkg_idx)
                    if await loc.count() > 0:
                        await loc.scroll_into_view_if_needed()
                        await loc.click(force=(is_ph or is_mlbb_br))
                        clicked_pkg = True
                        print(f"[auto_order] Clicked package via Playwright nth({pkg_idx})")
                except Exception as e:
                    print(f"[auto_order] Playwright click by index failed: {e}")

            if not clicked_pkg:
                try:
                    await sel_ctx.evaluate("(liId) => { const el = document.getElementById(liId); if (el) el.click(); }", li_id)
                    clicked_pkg = True
                    print(f"[auto_order] Clicked package via JS fallback")
                except Exception:
                    pass

            if not clicked_pkg:
                return {
                    "ok": False,
                    "message": "Could not click the selected package.",
                    "step": "click_package",
                }

            await asyncio.sleep(ORDER_SLEEP_AFTER_CLICK_SEC)
            # PH / MLBB BR: second tap so checkout leaves "PLEASE SELECT PACKAGE" / package step.
            if (is_ph or is_mlbb_br) and li_id:
                await asyncio.sleep(0.4)
                try:
                    _id2 = f'[id="{li_id}"]'
                    await sel_ctx.locator(_id2).first.click(timeout=10_000, force=(is_ph or is_mlbb_br))
                    print("[auto_order] second tap on selected package (PH or MLBB BR)")
                except Exception as e:
                    print(f"[auto_order] second tap on package skipped: {e}")
                await asyncio.sleep(0.3)

            # SAFETY GATE #1 — verify after package click that smile.one's
            # active/highlighted package matches the user's order.
            if li_id:
                try:
                    _ver = await page.evaluate(
                        _VERIFY_SELECTION_JS,
                        [li_id, target_cents, target_php_cents, target_name],
                    )
                    print(f"[auto_order] verify_selection (post-click): {_ver}")
                    if _ver and _ver.get("summaryPriceMatch") is False and not _ver.get("activeOk"):
                        return {
                            "ok": False,
                            "message": (
                                "Auto-order safety check failed: smile.one is not showing the package "
                                "you ordered. Aborted before payment. Please refresh the product and try again."
                            ),
                            "step": "verify_selection_mismatch",
                        }
                except Exception as _ev:
                    print(f"[auto_order] verify_selection (post-click) error (continuing): {_ev}")

            # ── Step 3: Select Smile Coin payment ──
            # After package click the checkout panel may render asynchronously; evaluate() can
            # block until the page settles — wait for payment UI first, then click with timeouts.
            print("[auto_order] Step 3: waiting for payment methods…")
            try:
                await page.wait_for_load_state("domcontentloaded", timeout=10_000)
            except Exception as e:
                print(f"[auto_order] load_state after package (ignored): {e}")
            try:
                if is_ph:
                    _seen_strip = False
                    for tgt in ph_payment_targets(page, sel_ctx):
                        try:
                            await tgt.wait_for_selector(
                                _PAYMENT_STRIP_SELECTOR,
                                timeout=9_000,
                                state="visible",
                            )
                            print("[auto_order] payment strip visible (PH multi-frame wait)")
                            _seen_strip = True
                            break
                        except Exception:
                            continue
                    if not _seen_strip:
                        await page.wait_for_selector(
                            _PAYMENT_STRIP_SELECTOR,
                            timeout=7_000,
                            state="visible",
                        )
                else:
                    await page.wait_for_selector(
                        _PAYMENT_STRIP_SELECTOR,
                        timeout=15_000,
                        state="visible",
                    )
            except Exception as e:
                print(f"[auto_order] wait payment strip: {e}")
            await asyncio.sleep(0.22)

            pay_result: dict = {"ok": False}
            pay_locators = [
                'div[class*="sectionNav-cartao"][class*="smilecoin"]',
                '[class*="sectionNav-cartao"][class*="smilecoin"]',
            ]
            _pay_tgts = ph_payment_targets(page, sel_ctx) if is_ph else [page]
            _pay_js = PH_CLICK_PAYMENT_JS if is_ph else _CLICK_PAYMENT_JS
            for tgt in _pay_tgts:
                if pay_result.get("ok"):
                    break
                for psel in pay_locators:
                    try:
                        pl = tgt.locator(psel).first
                        if await pl.count() > 0:
                            await pl.scroll_into_view_if_needed()
                            await pl.click(timeout=12_000)
                            pay_result = {"ok": True, "method": "playwright", "sel": psel}
                            print(f"[auto_order] Smile Coin click OK: {psel}")
                            break
                    except Exception as e:
                        print(f"[auto_order] Smile Coin locator {psel}: {e}")
                if not pay_result.get("ok"):
                    try:
                        pay_result = await asyncio.wait_for(
                            tgt.evaluate(_pay_js),
                            timeout=20.0,
                        )
                        print(f"[auto_order] Smile Coin evaluate: {pay_result}")
                    except asyncio.TimeoutError:
                        pay_result = {"ok": False, "reason": "evaluate_timeout"}
                        print("[auto_order] Smile Coin evaluate TIMEOUT")
                if pay_result.get("ok"):
                    break

            if not pay_result or not pay_result.get("ok"):
                reason = (pay_result or {}).get("reason", "")
                if reason == "no_smile_coin_option":
                    return {
                        "ok": False,
                        "message": "Smile Coin payment not available. Admin balance may be insufficient.",
                        "step": "no_smile_coin",
                    }
                if reason == "evaluate_timeout":
                    return {
                        "ok": False,
                        "message": "Payment UI did not respond in time. Please try again.",
                        "step": "select_payment_timeout",
                    }
                return {
                    "ok": False,
                    "message": "Payment options not loaded. Please try again.",
                    "step": "select_payment",
                }
            await asyncio.sleep(min(ORDER_SLEEP_AFTER_CLICK_SEC, 0.3))
            if is_ph:
                try:
                    _buy_ready = await _wait_ph_buy_button_ready(page, sel_ctx, timeout_ms=18_000)
                    print(f"[auto_order] PH buy button ready={_buy_ready}")
                except Exception as e:
                    print(f"[auto_order] PH buy button wait: {e}")

            # SAFETY GATE #2 — pre-Buy total cross-check. By this point Smile Coin
            # is selected and the summary shows the final price + SKU; if it disagrees
            # with what the user purchased, refuse the order so the wallet stays untouched.
            if li_id:
                try:
                    _ver2 = await page.evaluate(
                        _VERIFY_SELECTION_JS,
                        [li_id, target_cents, target_php_cents, target_name],
                    )
                    print(f"[auto_order] verify_selection (pre-buy): {_ver2}")
                    if (
                        _ver2
                        and _ver2.get("summaryPriceMatch") is False
                        and _ver2.get("summaryNameMatch") is not True
                    ):
                        return {
                            "ok": False,
                            "message": (
                                "Pre-buy safety check failed: smile.one summary shows a different package or "
                                "price than what you ordered. Order aborted before payment. Please refresh the "
                                "product page and try again."
                            ),
                            "step": "pre_buy_verify_mismatch",
                        }
                except Exception as _ev2:
                    print(f"[auto_order] verify_selection (pre-buy) error (continuing): {_ev2}")

            # ── Step 4: Buy / checkout (PH often English "Buy now"; BR stays Comprar agora) ──
            print("[auto_order] Step 4: checkout button…")
            buy_clicked = False
            buy_selectors = [
                ".payment-button-container .Nav-btn",
                ".payment-button-container span.pay-btn-label",
                ".Nav-footer .Nav-btn",
                "div.Nav-btn",
                "span.pay-btn-label",
            ]
            _buy_tgts = ph_payment_targets(page, sel_ctx) if is_ph else [page]
            for tgt in _buy_tgts:
                if buy_clicked:
                    break
                for bsel in buy_selectors:
                    try:
                        bl = tgt.locator(bsel).first
                        if await bl.count() == 0:
                            continue
                        await bl.scroll_into_view_if_needed()
                        await bl.click(timeout=15_000)
                        buy_clicked = True
                        print(f"[auto_order] Buy clicked: {bsel}")
                        break
                    except Exception as e:
                        print(f"[auto_order] buy try {bsel}: {e}")
                if not buy_clicked:
                    try:
                        await tgt.get_by_text("Comprar agora", exact=False).first.click(timeout=15_000)
                        buy_clicked = True
                        print("[auto_order] Buy clicked: text Comprar agora")
                    except Exception as e:
                        print(f"[auto_order] Comprar agora try: {e}")
                if is_ph and not buy_clicked:
                    for lbl in PH_EXTRA_BUY_TEXTS:
                        try:
                            await tgt.get_by_text(lbl, exact=False).first.click(timeout=12_000)
                            buy_clicked = True
                            print(f"[auto_order] Buy clicked: PH label {lbl!r}")
                            break
                        except Exception as e:
                            print(f"[auto_order] PH buy text {lbl!r}: {e}")
                if buy_clicked:
                    break

            if not buy_clicked:
                return {
                    "ok": False,
                    "message": "Could not complete purchase. Please try again.",
                    "step": "click_buy",
                }

            await asyncio.sleep(0.45)
            try:
                await page.keyboard.press("Enter")
            except Exception:
                pass
            await asyncio.sleep(0.15)

            # ── Step 5: Handle confirmation popup ──
            print("[auto_order] Step 5: confirmation popup…")
            for attempt in range(8):
                try:
                    confirm_found = False
                    _confirm_targets = [page] + [f for f in page.frames if f != page.main_frame]
                    for ctx in _confirm_targets:
                        for sel in (
                            "#smileone-notifi-confirm",
                            "#smileone-install-confirm",
                            ".smileone-notification-confirm",
                        ):
                            try:
                                loc = ctx.locator(sel).first
                                if await loc.count() == 0:
                                    continue
                                if not await loc.is_visible():
                                    continue
                                try:
                                    await loc.click(timeout=4_000)
                                except Exception:
                                    await loc.evaluate("el => el.click()")
                                confirm_found = True
                                _where = "main" if ctx is page else "frame"
                                print(f"[auto_order] Confirmation clicked: {sel} ({_where})")
                                break
                            except Exception:
                                continue
                        if confirm_found:
                            break
                    if not confirm_found:
                        for pat in (
                            re.compile(r"^\s*confirm\s*$", re.I),
                            re.compile(r"^ok$", re.I),
                            re.compile(r"^yes$", re.I),
                        ):
                            try:
                                await page.get_by_role("button", name=pat).first.click(timeout=3_000)
                                confirm_found = True
                                print(f"[auto_order] Confirmation clicked: role button /{pat.pattern}/")
                                break
                            except Exception:
                                continue
                    if confirm_found:
                        break
                except Exception:
                    pass
                try:
                    await page.keyboard.press("Enter")
                except Exception:
                    pass
                await asyncio.sleep(0.22)

            # Race URL navigation against in-page success copy so we exit ASAP.
            _post_confirm_ms = 30_000 if is_ph else 22_000
            _success_text_sel = (
                "text=/pagamento\\s+com\\s+sucesso|pedido\\s+(realizado|conclu)|"
                "order\\s+completed|successfully\\s+purchased|successfully\\s+recharged|"
                "payment\\s+successful|top[- ]?up\\s+successful|recharge\\s+successful|"
                "order\\s+placed\\s+successfully/i"
            )

            async def _wait_url():
                try:
                    await page.wait_for_url(
                        lambda url: (
                            "/message/success" in url.lower()
                            or "/customer/account/login" in url.lower()
                            or "order-success" in url.lower()
                            or "/sucesso" in url.lower()
                        ),
                        timeout=_post_confirm_ms,
                    )
                    return "url"
                except Exception:
                    return None

            async def _wait_text():
                try:
                    await page.wait_for_selector(_success_text_sel, timeout=_post_confirm_ms)
                    return "text"
                except Exception:
                    return None

            try:
                done, pending = await asyncio.wait(
                    [asyncio.create_task(_wait_url()), asyncio.create_task(_wait_text())],
                    return_when=asyncio.FIRST_COMPLETED,
                )
                _which = next(iter([t.result() for t in done if t.result()]), None)
                for t in pending:
                    t.cancel()
                print(f"[auto_order] Post-confirm settled via: {_which} url={page.url}")
            except Exception as e:
                print(f"[auto_order] Post-confirm wait error: {e}")

            await asyncio.sleep(0.35)
            for sel in (
                "#smileone-notifi-confirm",
                "#smileone-install-confirm",
                ".smileone-notification-confirm",
            ):
                try:
                    loc = page.locator(sel).first
                    if await loc.count() > 0 and await loc.is_visible():
                        await loc.click(timeout=3_000)
                        print(f"[auto_order] Post-confirm extra click: {sel}")
                except Exception:
                    continue

            # ── Step 6: Read result from page (strict — no ambiguous success) ──
            print("[auto_order] Step 6: reading result…")
            try:
                snap = await page.evaluate(
                    "() => (document.body && document.body.innerText) ? document.body.innerText.slice(0, 300) : ''"
                )
            except Exception:
                snap = ""
            print(f"[auto_order] Final URL: {page.url}")
            print(f"[auto_order] Page text (first 300): {(snap or '')[:300]}")

            return await _evaluate_checkout_outcome(page, is_ph)

        except PlaywrightTimeout:
            return {"ok": False, "message": "Order timed out. smile.one may be slow — please try again.", "step": "timeout"}
        except Exception as e:
            return {"ok": False, "message": f"Order error. Please try again. ({str(e)[:80]})", "step": "error"}
        finally:
            await browser.close()


def run_google_login() -> dict:
    """Sync - open browser for Google login"""
    try:
        from dotenv import load_dotenv

        load_dotenv(Path(__file__).parent / ".env")
    except ImportError:
        pass
    return asyncio.run(do_google_login(headed=True))


def run_auto_order_via_node(
    product_url: str,
    package_brl_cents=None,
    package_php_cents=None,
    package_name: str | None = None,
    package_index=None,
    form_data: dict | None = None,
    smile_li_id: str | None = None,
    progress_callback=None,
) -> dict:
    """
    Run the Playwright checkout flow in Node.js (node/smile_auto_order/run_auto_order.mjs).
    Same inputs/outputs as auto_order — used by default so the server does not depend on Python Playwright for orders.
    If progress_callback is set, it receives each stderr line from Node that contains "[auto_order]" (for live UI).
    """
    root = Path(__file__).resolve().parent
    script = root / "node" / "smile_auto_order" / "run_auto_order.mjs"
    if not script.is_file():
        return {"ok": False, "message": "Node auto-order runner is missing.", "step": "setup"}

    payload = {
        "url": product_url,
        "package_brl_cents": package_brl_cents,
        "package_php_cents": package_php_cents,
        "package_name": package_name or "",
        "package_index": package_index,
        "form_data": form_data,
        "smile_li_id": smile_li_id,
    }
    _stdin_bytes = json.dumps(payload, ensure_ascii=False).encode("utf-8")
    # Headless checkout: bundled Chromium is more reliable with smile_auth.json than
    # PLAYWRIGHT_CHANNEL=chrome (kept in .env for headed Google login). Opt-in: SMILE_AUTO_ORDER_USE_CHROME=1
    _sub_env = {**os.environ}
    if (os.getenv("SMILE_AUTO_ORDER_USE_CHROME") or "").strip().lower() not in ("1", "true", "yes"):
        _sub_env.pop("PLAYWRIGHT_CHANNEL", None)

    stderr_chunks: list[bytes] = []
    _timeout = float(ORDER_NODE_SUBPROCESS_TIMEOUT_SEC)

    def _pump_stderr():
        try:
            if not proc.stderr:
                return
            for raw in iter(proc.stderr.readline, b""):
                stderr_chunks.append(raw)
                if not progress_callback or b"[auto_order]" not in raw:
                    continue
                try:
                    line = raw.decode("utf-8", errors="replace").rstrip()
                except Exception:
                    continue
                if line:
                    try:
                        progress_callback(line)
                    except Exception:
                        pass
        except Exception:
            pass

    try:
        proc = subprocess.Popen(
            ["node", str(script)],
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            cwd=str(root),
            env=_sub_env,
        )
    except FileNotFoundError:
        return {
            "ok": False,
            "message": (
                "Node.js is not installed or not on PATH. Install Node LTS, then from the project folder run: "
                "cd node/smile_auto_order && npm install && npx playwright install chromium"
            ),
            "step": "setup",
        }

    try:
        if proc.stdin:
            proc.stdin.write(_stdin_bytes)
            proc.stdin.close()
    except Exception as e:
        try:
            proc.kill()
        except Exception:
            pass
        return {"ok": False, "message": f"Failed to start Node runner: {str(e)[:120]}", "step": "error"}

    t_err = threading.Thread(target=_pump_stderr, daemon=True)
    t_err.start()
    try:
        stdout_bytes = proc.stdout.read() if proc.stdout else b""
    except Exception:
        stdout_bytes = b""

    try:
        proc.wait(timeout=_timeout)
    except subprocess.TimeoutExpired:
        try:
            proc.kill()
        except Exception:
            pass
        try:
            t_err.join(timeout=2.0)
        except Exception:
            pass
        return {"ok": False, "message": "Order timed out. smile.one may be slow — please try again.", "step": "timeout"}

    try:
        t_err.join(timeout=3.0)
    except Exception:
        pass

    err_full = b"".join(stderr_chunks).decode("utf-8", errors="replace").strip()
    if err_full:
        lines = err_full.splitlines()
        important = [ln for ln in lines if "[auto_order]" in ln]
        emit = important if len(important) >= 8 else lines[-45:]
        for line in emit:
            print(f"[auto_order_node] {line}")

    out = (stdout_bytes or b"").decode("utf-8", errors="replace").strip()
    if not out:
        msg = err_full[:220] or "No output from Node runner."
        return {"ok": False, "message": msg, "step": "error"}

    try:
        result = json.loads(out.splitlines()[-1])
    except json.JSONDecodeError:
        return {
            "ok": False,
            "message": f"Invalid JSON from Node runner: {out[:180]}",
            "step": "error",
        }

    if not isinstance(result, dict):
        return {"ok": False, "message": "Node runner returned non-object JSON.", "step": "error"}
    return result


def run_auto_order(
    product_url: str,
    package_brl_cents=None,
    package_php_cents=None,
    package_name: str | None = None,
    package_index=None,
    form_data: dict | None = None,
    smile_li_id: str | None = None,
    progress_callback=None,
) -> dict:
    """Sync wrapper — Smile Coins checkout via Node Playwright by default (see run_auto_order_via_node)."""
    try:
        from dotenv import load_dotenv

        load_dotenv(Path(__file__).parent / ".env")
    except ImportError:
        pass

    use_py = (os.getenv("SMILE_AUTO_ORDER_USE_PYTHON") or "").strip().lower() in ("1", "true", "yes")
    if use_py:
        return asyncio.run(
            auto_order(
                product_url,
                package_brl_cents=package_brl_cents,
                package_php_cents=package_php_cents,
                package_name=package_name,
                package_index=package_index,
                use_saved_session=True,
                form_data=form_data,
                smile_li_id=smile_li_id,
            )
        )
    return run_auto_order_via_node(
        product_url,
        package_brl_cents=package_brl_cents,
        package_php_cents=package_php_cents,
        package_name=package_name,
        package_index=package_index,
        form_data=form_data,
        smile_li_id=smile_li_id,
        progress_callback=progress_callback,
    )


async def scrape_checkout_requirements(
    product_url: str,
    package_brl_cents=None,
    package_php_cents=None,
    package_name: str | None = None,
    package_index=None,
    use_saved_session: bool = True,
    first_screen_only: bool = False,
) -> dict:
    """
    checkout page ထဲက fill လိုတဲ့ form fields တွေကို scrape လုပ်တယ်။
    """
    if not async_playwright:
        return {"ok": False, "message": "Playwright not installed.", "step": "setup"}
    if "smile.one" not in product_url:
        return {"ok": False, "message": "Invalid product URL", "step": "input"}
    if use_saved_session and not AUTH_FILE.exists():
        return {"ok": False, "message": "Please login first.", "step": "login_required"}

    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        try:
            if use_saved_session and AUTH_FILE.exists():
                context = await _new_context_with_saved_session(browser)
            else:
                context = await browser.new_context(user_agent=_DEFAULT_UA)
            context.set_default_navigation_timeout(_NAV_TIMEOUT_MS)
            context.set_default_timeout(_NAV_TIMEOUT_MS)

            page = await context.new_page()
            page.set_default_navigation_timeout(_NAV_TIMEOUT_MS)
            page.set_default_timeout(_NAV_TIMEOUT_MS)
            await page.goto(product_url, wait_until="domcontentloaded", timeout=_NAV_TIMEOUT_MS)
            await asyncio.sleep(CHECKOUT_REQ_SLEEP_SHORT_SEC)

            if first_screen_only:
                # Lazy grids / React: give time and scroll so account-ID inputs mount before we read the DOM.
                await asyncio.sleep(1.8)
                try:
                    await page.evaluate(
                        "() => { window.scrollTo(0, Math.min(800, (document.body && document.body.scrollHeight) || 800)); }"
                    )
                except Exception:
                    pass
                await asyncio.sleep(1.0)
                fields = await page.evaluate(_FORM_FIELDS_JS)
                return {"ok": True, "fields": _clean_fields(fields)}

            is_ph_co = "/ph/" in (product_url or "").lower()
            _pkg_js_co = SELECT_PACKAGE_PH_JS if is_ph_co else _SELECT_PACKAGE_JS

            # Click package using the same smile.one logic as auto_order (PH uses order_automation_ph only).
            if package_brl_cents is not None or package_php_cents is not None or package_index is not None:
                target_cents = int(package_brl_cents) if package_brl_cents is not None else None
                target_php_cents = int(package_php_cents) if package_php_cents is not None else None
                target_idx = int(package_index) if package_index is not None else None
                target_name_co = (package_name or "").strip()
                try:
                    await page.evaluate(
                        _pkg_js_co,
                        [target_cents, target_idx, target_name_co, None, target_php_cents],
                    )
                except Exception:
                    pass
                await asyncio.sleep(CHECKOUT_REQ_SLEEP_SHORT_SEC)

            fields = await page.evaluate(_FORM_FIELDS_JS)

            return {"ok": True, "fields": _clean_fields(fields)}
        except Exception as e:
            return {"ok": False, "message": f"{str(e)[:140]}", "step": "error"}
        finally:
            await browser.close()


def run_checkout_requirements(
    product_url: str,
    package_brl_cents=None,
    package_php_cents=None,
    package_name: str | None = None,
    package_index=None,
    first_screen_only: bool = False,
    use_saved_session: bool = True,
) -> dict:
    try:
        from dotenv import load_dotenv
        load_dotenv(Path(__file__).parent / ".env")
    except ImportError:
        pass

    return asyncio.run(
        scrape_checkout_requirements(
            product_url,
            package_brl_cents=package_brl_cents,
            package_php_cents=package_php_cents,
            package_name=package_name,
            package_index=package_index,
            use_saved_session=use_saved_session,
            first_screen_only=first_screen_only,
        )
    )
