"""
G2A iDEAL link generator  (v2 - fresh profile per order)
=========================================================
Each run in gologin mode:
  1. Creates a NEW GoLogin cloud browser profile (clean fingerprint)
  2. Creates and links a NEW GoLogin residential NL proxy (own IP per profile)
     (PROXY_PROVIDER=iproyal switches back to iProyal; PROXY_HOST=... overrides)
  3. Picks the next available email from the panel DB (--db flag)
  4. Runs the G2A checkout flow and captures the iDEAL link
  5. Saves the link + email to the panel DB
  6. Deletes the GoLogin profile (cleanup)

Usage:
    python g2a_ideal_link.py gologin --db --price 86.26   (recommended)
    python g2a_ideal_link.py gologin --price 149.16       (fresh profile, hardcoded email)
    python g2a_ideal_link.py local --price 76.61          (local Chrome, reuses profile)
    python g2a_ideal_link.py local --db --price 76.61     (local Chrome, panel email)

--price selects one of the fixed EUR price tiers in PRICING below. Each tier
maps to one or more Rewarble Crypto Gift Cards (USD) that are all added to the
cart before checkout. The tier price is saved with the link (file + panel DB).
"""

import datetime
import json
import os
import pathlib
import random
import re
import string
import subprocess
import sys
import time
import urllib.error
import urllib.parse
import urllib.request

from playwright.sync_api import sync_playwright, TimeoutError as PWTimeout

# ----------------------------------------------------------------- settings
MODE = "gologin"
for a in sys.argv[1:]:
    if a in ("local", "gologin"):
        MODE = a
if not MODE:
    MODE = os.environ.get("G2A_MODE", "local").lower()

GOLOGIN_TOKEN = os.environ.get("GL_API_TOKEN") or (
    "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
    ".eyJzdWIiOiI2YWQ4NjA5YmM1OGUyMTQ1Nzc4OGQ2OGIiLCJ0eXBlIjoiZGV2Iiwiand0aWQiOiI2YWQ4NjJkZmFlYzc3OGI5ZDA5YjVhODgifQ"
    ".9blypFa-suolTucuvegSmVwQlyW9-WGwi2OqexVz9Ms"
)
GOLOGIN_PROFILE_NAME = "g2a-ideal"
GOLOGIN_CONNECT = "cloudbrowser.gologin.com/connect"
GOLOGIN_API = "https://api.gologin.com"

# iProyal proxy base (NL residential). A random session id is generated on
# every run so each order gets a fresh IP.
IPROYAL_USER = "UujDKFyv8yJDruUo"
IPROYAL_PASS_BASE = "Pass1233993_country-nl"
IPROYAL_SESSION_LIFETIME = "5m"

# Which proxy every new profile gets:
#   "gologin"  GoLogin's own residential proxies. One brand-new proxy is
#              created and linked per profile (= its own IP), country
#              GOLOGIN_PROXY_COUNTRY. Uses the plan's included proxy traffic.
#   "iproyal"  iProyal NL with a random session id per run (settings above).
# Setting PROXY_HOST/PROXY_PORT/PROXY_USER/PROXY_PASS in the environment
# overrides both and uses that proxy as-is (see fresh_proxy()).
PROXY_PROVIDER = os.environ.get("PROXY_PROVIDER", "gologin").lower()
GOLOGIN_PROXY_COUNTRY = os.environ.get("GOLOGIN_PROXY_COUNTRY", "nl").lower()

EMAIL = "ossama10@gmail.com"
try:
    from faker import Faker
    _fake = Faker("nl_NL")
    FIRST_NAME = _fake.first_name()
    LAST_NAME = _fake.last_name()
except ImportError:
    FIRST_NAME = "Ossama"
    LAST_NAME = "De Vries"
# Rewarble Crypto Gift Card (Key, GLOBAL) product pages, by USD amount.
_RW = "https://www.g2a.com/rewarble-crypto-gift-card-{amt}-usd-by-rewarble-key-global-i{pid}"
REWARBLE_CARDS = {
    25:  _RW.format(amt=25,  pid="10000505309002"),
    50:  _RW.format(amt=50,  pid="10000505309003"),
    60:  _RW.format(amt=60,  pid="10000505309071"),
    120: _RW.format(amt=120, pid="10000505309096"),
    150: _RW.format(amt=150, pid="10000505309026"),
}

# Fixed price tiers: EUR amount -> USD cards that make up the order.
# Every card in the list is added to the cart; the iDEAL link is generated
# for the whole cart and the tier price is what gets saved.
# cron_generate_links.py runs one link per tier in parallel on every tick,
# so adding a tier here is all it takes to start generating it.
PRICING = {
    "76.61":  [50, 25],
    "86.26":  [60, 25],
    "149.16": [150],
    # "50.96":  [50],
    # "60.28":  [60],
}

# Tier-dependent settings. Empty until select_tier() runs (done in __main__),
# so the module can be imported by the cron scripts without side effects.
PRICE_EUR = ""
CARD_AMOUNTS = []
PRODUCT_URLS = []
CARD_NAME = ""
PRODUCT_URL = ""


def _parse_price_arg():
    """Read --price X / --price=X from argv (or env G2A_PRICE)."""
    raw = os.environ.get("G2A_PRICE", "")
    args = sys.argv[1:]
    for i, a in enumerate(args):
        if a.startswith("--price="):
            raw = a.split("=", 1)[1]
        elif a == "--price" and i + 1 < len(args):
            raw = args[i + 1]
    return raw


def _normalize_price(raw: str) -> str:
    m = re.search(r"\d+(?:[.,]\d+)?", raw or "")
    if not m:
        return ""
    return f"{float(m.group(0).replace(',', '.')):.2f}"


def select_tier(raw_price: str) -> None:
    """Resolve a --price value to a tier and fill the tier-dependent globals."""
    global PRICE_EUR, CARD_AMOUNTS, PRODUCT_URLS, CARD_NAME, PRODUCT_URL
    PRICE_EUR = _normalize_price(raw_price)
    if PRICE_EUR not in PRICING:
        tiers = ", ".join(PRICING)
        print(
            f"Missing or unknown --price {PRICE_EUR or '(none)'!r}.\n"
            f"Pick one of the fixed tiers (EUR): {tiers}\n"
            f"Example: python g2a_ideal_link.py gologin --db --price 86.26",
            file=sys.stderr,
        )
        sys.exit(2)
    CARD_AMOUNTS = PRICING[PRICE_EUR]
    PRODUCT_URLS = [REWARBLE_CARDS[a] for a in CARD_AMOUNTS]
    CARD_NAME = "Rewarble Crypto Gift Card " + " + ".join(
        f"{a} USD" for a in CARD_AMOUNTS
    )
    PRODUCT_URL = " | ".join(PRODUCT_URLS)   # stored in the panel DB


CART_URL = "https://www.g2a.com/page/cart"
PAYMENT_COUNTRY = "NL"
IDEAL_HOST = "pay.ideal.nl"
# How long to wait for the pay.ideal.nl page after clicking pay. If it has not
# appeared after this, it is not coming: give up, mark the run failed, free the
# GoLogin slot. (Was 15 for manual runs; cron runs need it short.)
LINK_WAIT_MINUTES = 3

USE_PANEL_DB = "--db" in sys.argv or os.environ.get("USE_PANEL_DB", "")
# --------------------------------------------------------------------------

SEL_ADD_TO_CART = '[data-testid="ppa-payment__btn"]'
SEL_CART_COUNT = '[data-testid="cart-item-count"]'
SEL_CART_ITEM = '[data-testid="cartPage-cartItem"]'
SEL_CART_EMAIL = '[data-testid="cartPage-emailInput"]'
SEL_CART_REMOVE = '[data-testid="cart-item-remove"]'
SEL_COUNTRY = 'select:has(option[value="NL"])'
RX_IDEAL_RADIO = re.compile(r"ideal", re.I)
RX_IDEAL_LABEL = re.compile(r"^\s*iDeal\s*$", re.I)
RX_PAY_BUTTON = re.compile(r"ideal", re.I)
RX_PRICE = re.compile(r"\d[\d.,]*\s*(USD|EUR)")
RX_CONTINUE_PAY = re.compile(
    r"(continue|doorgaan|weiter|continuer|continua|kontynuuj)"
    r".*(payment|betaling|zahlung|paiement|pagamento|platba)",
    re.I,
)
RX_NO_THANKS = re.compile(
    r"no,?\s*thanks|nee,?\s*bedankt|nein,?\s*danke", re.I
)
RX_COOKIE = re.compile(r"cookie", re.I)
SEL_NAME_FORM = "form:has(input[type='text'])"

HERE = pathlib.Path(__file__).resolve().parent
LINKS_FILE = HERE / "ideal_links.txt"
PROFILE_DIR = HERE / "browser_profile"
# MySQL connection settings - must match config.php on the server
MYSQL_HOST = os.environ.get("PANEL_DB_HOST", "localhost")
MYSQL_DB   = os.environ.get("PANEL_DB_NAME", "heroorzero_magicheckout")
MYSQL_USER = os.environ.get("PANEL_DB_USER", "heroorzero_magicheckout")
MYSQL_PASS = os.environ.get("PANEL_DB_PASS", "GYNSFP[dplii+wbb")
DEBUG_DIR = HERE / "debug"


# ----------------------------------------------------------------- helpers
def log(msg: str) -> None:
    print(f"[{datetime.datetime.now():%H:%M:%S}] {msg}", flush=True)


def _random_id(length=8):
    return "".join(random.choices(string.ascii_letters + string.digits, k=length))


def fresh_proxy():
    """Proxy for this run.

    Default: iProyal NL with a random session id = new IP per run.
    Override for a test (or another provider) with environment variables:
        PROXY_HOST=gate.decodo.com PROXY_PORT=10005 \\
        PROXY_USER=... PROXY_PASS=... python g2a_ideal_link.py ...
    With an override the proxy is used exactly as given (no session id added).
    """
    host = os.environ.get("PROXY_HOST", "").strip()
    if host:
        proxy = {
            "mode": os.environ.get("PROXY_MODE", "http"),
            "host": host,
            "port": int(os.environ.get("PROXY_PORT", "0") or 0),
            "username": os.environ.get("PROXY_USER", ""),
            "password": os.environ.get("PROXY_PASS", ""),
        }
        if not proxy["port"]:
            log("PROXY_HOST is set but PROXY_PORT is missing")
            sys.exit(1)
        log(f"Proxy override: {proxy['host']}:{proxy['port']} user={proxy['username']}")
        return proxy

    sid = _random_id()
    pw = f"{IPROYAL_PASS_BASE}_session-{sid}_lifetime-{IPROYAL_SESSION_LIFETIME}"
    log(f"Proxy session: {sid}")
    return {
        "mode": "http",
        "host": "geo.iproyal.com",
        "port": 12321,
        "username": IPROYAL_USER,
        "password": pw,
    }


def diagnose(page, name: str) -> None:
    try:
        DEBUG_DIR.mkdir(exist_ok=True)
        stamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
        base = DEBUG_DIR / f"{stamp}_{re.sub(r'[^a-z0-9]+', '_', name.lower())}"
        page.screenshot(path=str(base) + ".png", full_page=False)
        info = [f"URL: {page.url}", ""]
        dialogs = page.locator("[role='dialog'], [role='alertdialog']")
        for i in range(min(dialogs.count(), 5)):
            try:
                if dialogs.nth(i).is_visible():
                    info.append("DIALOG: " + dialogs.nth(i).inner_text()[:600])
            except Exception:
                pass
        info.append("")
        info.append(page.inner_text("body")[:3000])
        (pathlib.Path(str(base) + ".txt")).write_text(
            "\n".join(info), encoding="utf-8"
        )
        log(f"       debug saved: {base}.png / .txt")
    except Exception as exc:
        log(f"       (could not save debug info: {exc})")


def try_step(name, fn, page=None):
    try:
        fn()
        log(f"OK   - {name}")
        return True
    except Exception as exc:
        first = str(exc).splitlines()[0] if str(exc) else type(exc).__name__
        log(f"SKIP - {name}: {first}")
        if MODE == "local":
            log("       Please do this step yourself in the browser window.")
        else:
            log("       (cloud mode - check debug files)")
        if page is not None:
            diagnose(page, name)
        return False


# ----------------------------------------------------------------- cookies
def dismiss_cookies(page) -> None:
    for sel in (
        "#uc-btn-accept-banner",
        "[data-testid='uc-accept-all-button']",
        "#onetrust-accept-btn-handler",
        "#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll",
        ".cc-accept-all",
        "[data-consent='accept']",
    ):
        try:
            btn = page.locator(sel).first
            if btn.count() and btn.is_visible():
                btn.click()
                page.wait_for_timeout(1000)
                log(f"cookie consent accepted via {sel}")
                return
        except Exception:
            pass

    try:
        dialogs = page.locator("[role='dialog'], [role='alertdialog']")
        for i in range(min(dialogs.count(), 6)):
            d = dialogs.nth(i)
            if not d.is_visible():
                continue
            if not RX_COOKIE.search(d.inner_text(timeout=2_000) or ""):
                continue
            accept_rx = re.compile(
                r"(accept\s*all|alle\s*accept|alles\s*akzeptieren"
                r"|tout\s*accepter|accepteer\s*alles"
                r"|alle\s*cookies\s*accepteren|accept\s*cookies"
                r"|toestemming|akkoord|agree|zustimmen)",
                re.I,
            )
            btns = d.get_by_role("button")
            for bi in range(btns.count()):
                b = btns.nth(bi)
                try:
                    if b.is_visible() and accept_rx.search(
                        b.inner_text(timeout=1_000) or ""
                    ):
                        b.click()
                        page.wait_for_timeout(1000)
                        log("cookie consent accepted (text match)")
                        return
                except Exception:
                    pass
            last_visible = None
            for bi in range(btns.count()):
                try:
                    if btns.nth(bi).is_visible():
                        last_visible = btns.nth(bi)
                except Exception:
                    pass
            if last_visible:
                last_visible.click()
                page.wait_for_timeout(1000)
                log("cookie consent accepted (last button)")
                return
    except Exception as exc:
        log(f"(cookie dialog strategy 2 skipped: {type(exc).__name__})")

    try:
        page.evaluate(
            """() => {
            for (const b of document.querySelectorAll(
                'button, [role="button"], a.button'
            )) {
                const t = (b.textContent || '').toLowerCase();
                if ((t.includes('accept') || t.includes('akkoord')
                     || t.includes('toestemming')
                     || t.includes('accepteer')) && b.offsetParent !== null) {
                    b.click(); return true;
                }
            }
            return false;
        }"""
        )
        page.wait_for_timeout(1000)
        log("cookie consent accepted (JS fallback)")
    except Exception as exc:
        log(f"(cookie dialog check skipped: {type(exc).__name__})")


def find_ideal_page(context):
    for p in context.pages:
        if IDEAL_HOST in p.url:
            return p
    return None


def save_link(url: str) -> None:
    stamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    with LINKS_FILE.open("a", encoding="utf-8") as f:
        f.write(f"{stamp}  {PRICE_EUR} EUR  {CARD_NAME}  {url}\n")
    clip = ""
    if os.name == "nt":  # Windows only: copy to clipboard for manual runs
        try:
            subprocess.run(
                "clip", input=url.encode("utf-8"), check=True, shell=True
            )
            clip = " (copied to clipboard)"
        except Exception:
            pass
    log(f"Saved to {LINKS_FILE}{clip}")
    print(
        "\n" + "=" * 70
        + f"\nIDEAL LINK ({PRICE_EUR} EUR - {CARD_NAME}):\n{url}\n"
        + "=" * 70 + "\n"
    )


# ------------------------------------------------------------ HTTP helper
def _http(method: str, url: str, token: str = "", body=None):
    data = None
    req = urllib.request.Request(url, method=method)
    if token:
        req.add_header("Authorization", f"Bearer {token}")
    if body is not None:
        data = json.dumps(body).encode("utf-8")
        req.add_header("Content-Type", "application/json")
    try:
        with urllib.request.urlopen(req, data=data, timeout=120) as resp:
            raw = resp.read()
            status, headers = resp.status, dict(resp.headers)
    except urllib.error.HTTPError as exc:
        raw = exc.read()
        status, headers = exc.code, dict(exc.headers)
    try:
        parsed = json.loads(raw) if raw else None
    except ValueError:
        parsed = None
    return status, headers, parsed


# ------------------------------------------------------------ GoLogin
def create_gologin_profile(email: str = "") -> str:
    """Create a brand-new GoLogin profile for this single order."""
    name = email if email else f"{GOLOGIN_PROFILE_NAME}-{_random_id(6)}"
    log(f"Creating new GoLogin profile '{name}' ...")
    status, _, data = _http(
        "POST",
        f"{GOLOGIN_API}/browser/quick",
        GOLOGIN_TOKEN,
        {"os": "win", "osSpec": "win11", "name": name},
    )
    pid = (data or {}).get("id") if isinstance(data, dict) else None
    if status >= 400 or not pid:
        log(f"Could not create GoLogin profile (HTTP {status}): {data}")
        sys.exit(1)
    log(f"Created GoLogin profile {pid}")
    return pid


def delete_gologin_profile(profile_id: str) -> None:
    """Delete a GoLogin profile after the order is done."""
    status, _, _ = _http(
        "DELETE", f"{GOLOGIN_API}/browser/{profile_id}", GOLOGIN_TOKEN
    )
    if status < 300:
        log(f"Deleted GoLogin profile {profile_id}")
    else:
        log(f"Warning: could not delete profile {profile_id} (HTTP {status})")


def add_gologin_proxy(profile_id: str, country: str) -> None:
    """Create a new GoLogin residential proxy for `country` and link it to the
    profile. Same call the official GoLogin SDK makes
    (addGologinProxyToProfile): POST /users-proxies/mobile-proxy with
    isMobile=false, isDc=false = residential. Every call makes a separate
    proxy, so every profile gets its own IP."""
    status, _, data = _http(
        "POST",
        f"{GOLOGIN_API}/users-proxies/mobile-proxy",
        GOLOGIN_TOKEN,
        {
            "countryCode": country,
            "isDc": False,
            "isMobile": False,
            "profileIdToLink": profile_id,
        },
    )
    if status >= 400 or not isinstance(data, dict):
        log(f"Could not create GoLogin residential proxy (HTTP {status}): {data}")
        delete_gologin_profile(profile_id)
        sys.exit(1)
    limit = int(data.get("trafficLimitBytes") or 0)
    used = int(data.get("trafficUsedBytes") or 0)
    if limit and used >= limit:
        log(
            f"GoLogin proxy traffic limit reached "
            f"({used / 1e9:.2f} / {limit / 1e9:.2f} GB) - top up or switch provider"
        )
        delete_gologin_profile(profile_id)
        sys.exit(1)
    where = ", ".join(
        str(data[k]) for k in ("country", "city", "lastIp") if data.get(k)
    )
    traffic = f" | traffic {used / 1e9:.2f}/{limit / 1e9:.2f} GB" if limit else ""
    log(
        f"GoLogin residential proxy linked: {data.get('host', '?')}:"
        f"{data.get('port', '?')} [{country.upper()} {where}]{traffic}"
    )


def bind_custom_proxy(profile_id: str) -> None:
    """Attach an iProyal (or env-override) proxy to the profile."""
    proxy = fresh_proxy()
    status, _, data = _http(
        "PATCH",
        f"{GOLOGIN_API}/browser/{profile_id}/proxy",
        GOLOGIN_TOKEN,
        {
            "mode": proxy["mode"],
            "host": proxy["host"],
            "port": int(proxy["port"]),
            "username": proxy["username"],
            "password": proxy["password"],
        },
    )
    if status >= 400:
        log(f"Could not bind proxy to profile (HTTP {status}): {data}")
        delete_gologin_profile(profile_id)
        sys.exit(1)
    log(
        f"Proxy bound: {proxy['mode']}://{proxy['host']}:{proxy['port']}"
    )


def launch_gologin(pw, email: str = ""):
    """Fresh GoLogin profile + fresh proxy per order."""
    if not GOLOGIN_TOKEN:
        log("GoLogin mode needs GOLOGIN_TOKEN (or env GL_API_TOKEN).")
        sys.exit(1)
    log(f"Using identity: {FIRST_NAME} {LAST_NAME}")

    profile_id = create_gologin_profile(email)

    # Env override (PROXY_HOST=...) always wins; otherwise PROXY_PROVIDER.
    if os.environ.get("PROXY_HOST", "").strip() or PROXY_PROVIDER == "iproyal":
        bind_custom_proxy(profile_id)
    elif PROXY_PROVIDER == "gologin":
        add_gologin_proxy(profile_id, GOLOGIN_PROXY_COUNTRY)
    else:
        log(f"Unknown PROXY_PROVIDER {PROXY_PROVIDER!r}; use 'gologin' or 'iproyal'.")
        delete_gologin_profile(profile_id)
        sys.exit(1)

    query = urllib.parse.urlencode(
        {"token": GOLOGIN_TOKEN, "profile": profile_id}
    )
    status, headers, _ = _http(
        "GET", f"https://{GOLOGIN_CONNECT}?{query}"
    )
    if status >= 400:
        reason = headers.get("X-Error-Reason", "")
        log(f"GoLogin refused to start profile (HTTP {status}): {reason}")
        sys.exit(1)
    log(f"GoLogin cloud profile {profile_id} started (fresh)")
    browser = pw.chromium.connect_over_cdp(
        f"wss://{GOLOGIN_CONNECT}?{query}"
    )
    context = (
        browser.contexts[0] if browser.contexts else browser.new_context()
    )
    log("Browser: GoLogin cloud (new profile)")

    def finish():
        try:
            browser.close()
        except Exception:
            pass
        _http(
            "DELETE",
            f"{GOLOGIN_API}/browser/{profile_id}/web",
            GOLOGIN_TOKEN,
        )
        log("GoLogin cloud profile stopped (profile kept)")

    return context, finish


def launch_local(pw):
    for channel in ("chrome", "msedge", None):
        try:
            context = pw.chromium.launch_persistent_context(
                str(PROFILE_DIR),
                channel=channel,
                headless=False,
                viewport={"width": 1280, "height": 900},
                args=["--disable-blink-features=AutomationControlled"],
            )
            log(f"Browser: {channel or 'bundled chromium'}")
            return context, context.close
        except Exception as exc:
            log(
                f"Could not start {channel or 'bundled chromium'}: "
                f"{str(exc).splitlines()[0]}"
            )
    log(
        "No browser could be started. "
        "Run: python -m playwright install chromium"
    )
    sys.exit(1)


def open_browser(pw, email: str = ""):
    if MODE == "gologin":
        return launch_gologin(pw, email)
    if MODE == "local":
        return launch_local(pw)
    log(f"Unknown MODE {MODE!r}; use 'local' or 'gologin'.")
    sys.exit(1)


def set_text(box, value: str, label: str) -> None:
    for attempt in range(3):
        box.click()
        box.press("Control+A")
        box.press("Backspace")
        box.type(value, delay=30)
        box.press("Tab")
        box.page.wait_for_timeout(400)
        if box.input_value().strip().lower() == value.lower():
            log(f"{label}: {box.input_value()}")
            return
        log(f"{label} not accepted (attempt {attempt + 1}), retrying")
    raise RuntimeError(f"could not set {label}")


# ------------------------------------------------------------ Panel DB (MySQL)
def panel_db():
    try:
        import mysql.connector
        conn = mysql.connector.connect(
            host=MYSQL_HOST, database=MYSQL_DB,
            user=MYSQL_USER, password=MYSQL_PASS,
        )
        return conn
    except Exception as exc:
        log(f"MySQL connection failed: {exc}")
        return None


def panel_pick_email():
    conn = panel_db()
    if not conn:
        return None, None
    cur = conn.cursor(dictionary=True)
    # Several copies of this script run at the same time (one per price
    # tier, started by cron_generate_links.py). Lock the row we pick so two
    # runs can never grab the same email.
    conn.start_transaction()
    cur.execute(
        "SELECT id, email FROM emails WHERE status = 'available' "
        "ORDER BY last_used_at IS NULL DESC, last_used_at ASC, created_at ASC "
        "LIMIT 1 FOR UPDATE"
    )
    row = cur.fetchone()
    if not row:
        conn.rollback()
        cur.close()
        conn.close()
        log("No available emails in the panel DB")
        return None, None
    cur.execute(
        "UPDATE emails SET status = 'in_use', last_used_at = %s WHERE id = %s",
        (int(time.time()), row["id"]),
    )
    conn.commit()
    cur.close()
    conn.close()
    log(f"Picked email from panel: {row['email']} (id={row['id']})")
    return row["id"], row["email"]


def panel_report_order(
    email_id, email_addr, ideal_link="", price="", error=""
):
    conn = panel_db()
    if not conn:
        return
    now = int(time.time())
    status = "completed" if ideal_link else ("failed" if error else "pending")
    cur = conn.cursor()
    cur.execute(
        "INSERT INTO orders(email_id, email_address, product_name, "
        "product_url, price, currency, ideal_link, status, "
        "error_message, created_at, completed_at) "
        "VALUES(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)",
        (
            email_id,
            email_addr,
            CARD_NAME,
            PRODUCT_URL,
            price,
            "EUR",
            ideal_link,
            status,
            error,
            now,
            now if ideal_link or error else None,
        ),
    )
    email_status = "used" if ideal_link else ("error" if error else "in_use")
    cur.execute(
        "UPDATE emails SET status = %s WHERE id = %s", (email_status, email_id)
    )
    conn.commit()
    cur.close()
    conn.close()
    log(f"Order reported to panel DB (status={status})")


# ------------------------------------------------------------ main flow
def main() -> None:
    db_email_id, db_email_addr = None, None
    active_email = EMAIL
    if USE_PANEL_DB:
        db_email_id, db_email_addr = panel_pick_email()
        if db_email_addr:
            active_email = db_email_addr
        else:
            log("Falling back to hardcoded EMAIL")

    log(
        f"=== Order start | email={active_email} | mode={MODE} "
        f"| price={PRICE_EUR} EUR | cards={CARD_NAME} ==="
    )

    with sync_playwright() as pw:
        context, finish = open_browser(pw, active_email)
        page = context.pages[0] if context.pages else context.new_page()
        page.set_default_timeout(25_000)
        if MODE == "gologin":
            try:
                page.goto("about:blank", wait_until="load", timeout=10_000)
            except Exception:
                pass
            page.set_viewport_size({"width": 1280, "height": 900})
            page.wait_for_timeout(500)

        def check_ip():
            """Log the IP the browser really goes out with, and its country."""
            ip = cc = city = isp = None
            raw = ""
            # Two independent lookups; the first that answers properly wins.
            for url in (
                "https://ip.decodo.com/json",
                "https://ipinfo.io/json",
            ):
                try:
                    page.goto(url, wait_until="domcontentloaded", timeout=20_000)
                    raw = page.inner_text("body").strip()
                    info = json.loads(raw)
                except Exception:
                    continue
                if "proxy" in info:            # ip.decodo.com format
                    ip = info.get("proxy", {}).get("ip")
                    cc = (info.get("country") or {}).get("code")
                    city = (info.get("city") or {}).get("name")
                    isp = (info.get("isp") or {}).get("isp")
                else:                          # ipinfo.io format
                    ip = info.get("ip")
                    cc = info.get("country")
                    city = info.get("city")
                    isp = info.get("org")
                if ip:
                    break
            if not ip:
                log(f"Exit IP: lookup failed (last answer: {raw[:120]!r})")
                return
            log(f"Exit IP: {ip} | {cc} {city} | {isp}")
            if cc != PAYMENT_COUNTRY:
                log(
                    f"WARNING: proxy country is {cc}, "
                    f"expected {PAYMENT_COUNTRY} - iDEAL may not be offered"
                )

        try_step("check proxy IP", check_ip, page)

        def cart_count() -> int:
            el = page.locator(SEL_CART_COUNT).first
            try:
                return (
                    int(el.inner_text(timeout=3_000).strip() or 0)
                    if el.count()
                    else 0
                )
            except Exception:
                return 0

        def empty_cart():
            page.goto(CART_URL, wait_until="domcontentloaded")
            dismiss_cookies(page)
            row = page.locator(SEL_CART_ITEM).first
            try:
                row.wait_for(state="visible", timeout=8_000)
            except PWTimeout:
                log("cart already empty")
                return
            for _ in range(10):
                btns = page.locator(SEL_CART_REMOVE)
                target = None
                for i in range(btns.count()):
                    if btns.nth(i).is_visible():
                        target = btns.nth(i)
                        break
                if target is None:
                    break
                target.click()
                confirm = page.get_by_role(
                    "button",
                    name=re.compile(
                        r"^(remove|yes|confirm|delete|verwijder|ja|bevestig)",
                        re.I,
                    ),
                ).first
                try:
                    confirm.wait_for(state="visible", timeout=2_000)
                    confirm.click()
                except PWTimeout:
                    pass
                page.wait_for_timeout(1500)
            page.wait_for_timeout(1000)
            left = page.locator(SEL_CART_ITEM).count()
            if left:
                raise RuntimeError(f"{left} item(s) still in the cart")
            log("cart emptied")

        # A GoLogin profile is created fresh for every run, so its cart is
        # always empty: skip the check (saves ~10 s per link). Local mode
        # reuses one Chrome profile, so there it still matters.
        if MODE == "local":
            try_step("empty old cart", empty_cart, page)
        else:
            log("fresh profile - skipping empty-cart step")

        def add_to_cart():
            for idx, (amount, url) in enumerate(
                zip(CARD_AMOUNTS, PRODUCT_URLS), start=1
            ):
                page.goto(url, wait_until="domcontentloaded")
                dismiss_cookies(page)
                btn = page.locator(SEL_ADD_TO_CART).first
                btn.wait_for(state="visible")
                btn.click()
                page.wait_for_function(
                    f"() => {{ const e = document.querySelector('{SEL_CART_COUNT}');"
                    f" return e && parseInt(e.textContent) >= {idx}; }}",
                    timeout=15_000,
                )
                log(f"added {amount} USD card - cart count now {cart_count()}")
                page.wait_for_timeout(1000)

        try_step("add to cart", add_to_cart, page)

        def continue_button():
            btn = page.get_by_role("button", name=RX_PRICE).first
            if btn.count() and btn.is_visible():
                return btn
            btn = page.get_by_role("button", name=RX_CONTINUE_PAY).first
            if btn.count() and btn.is_visible():
                return btn
            btn = page.get_by_role(
                "button",
                name=re.compile(r"(continue|doorgaan|weiter)", re.I),
            ).first
            return btn

        def dismiss_upsell():
            page.wait_for_timeout(3000)
            if "/payments" in page.url:
                return
            no = page.get_by_role("button", name=RX_NO_THANKS).first
            if no.count() and no.is_visible():
                no.click()
                log("closed 'Add to save more' panel")
            else:
                page.keyboard.press("Escape")
                log("pressed Escape to close any side panel")
            page.wait_for_timeout(2000)
            if "/payments" not in page.url:
                btn = continue_button()
                if btn.count() and btn.is_visible():
                    btn.click()

        def check_cart_total():
            """Log the total G2A shows and warn if it differs from the tier."""
            try:
                btn = page.get_by_role("button", name=RX_PRICE).first
                if not (btn.count() and btn.is_visible()):
                    return
                m = re.search(
                    r"(\d[\d.,]*)\s*(USD|EUR)", btn.inner_text(timeout=3_000)
                )
                if not m:
                    return
                shown = float(m.group(1).replace(",", "."))
                cur = m.group(2)
                log(f"cart total shown: {shown:.2f} {cur} (tier {PRICE_EUR} EUR)")
                if cur == "EUR" and abs(shown - float(PRICE_EUR)) > 0.50:
                    log(
                        f"WARNING: G2A total {shown:.2f} EUR differs from "
                        f"tier price {PRICE_EUR} EUR"
                    )
            except Exception as exc:
                log(f"(could not read cart total: {type(exc).__name__})")

        def cart():
            page.goto(CART_URL, wait_until="domcontentloaded")
            dismiss_cookies(page)
            page.locator(SEL_CART_ITEM).first.wait_for(state="visible")
            n = cart_count()
            want = len(PRODUCT_URLS)
            if n != want:
                log(f"cart holds {n} items, expected {want} - resetting")
                empty_cart()
                add_to_cart()
                page.goto(CART_URL, wait_until="domcontentloaded")
                page.locator(SEL_CART_ITEM).first.wait_for(state="visible")
            box = page.locator(SEL_CART_EMAIL).first
            try:
                box.wait_for(state="visible", timeout=8_000)
                set_text(box, active_email, "email in cart")
            except PWTimeout:
                log("no email box shown (already logged in?) - continuing")
            check_cart_total()
            continue_button().click()
            dismiss_upsell()
            page.wait_for_url(re.compile(r"/payments"), timeout=20_000)
            page.wait_for_timeout(1500)

        try_step("cart / email / continue", cart, page)

        def choose_ideal():
            country = page.locator(SEL_COUNTRY).first
            if country.count() and country.input_value() != PAYMENT_COUNTRY:
                country.select_option(PAYMENT_COUNTRY)
                page.wait_for_timeout(2000)
            radio = page.get_by_role("radio", name=RX_IDEAL_RADIO).first
            radio.wait_for(state="attached")
            if not radio.is_checked():
                page.get_by_text(RX_IDEAL_LABEL).first.click()
                page.wait_for_timeout(800)
            if not radio.is_checked():
                radio.check(force=True)
            log("iDEAL selected")
            page.get_by_role("button", name=RX_PAY_BUTTON).first.click()

        try_step("select iDEAL and pay", choose_ideal, page)

        def personal_details():
            form = page.locator(SEL_NAME_FORM).first
            first = form.locator("input[type='text']").nth(0)
            last = form.locator("input[type='text']").nth(1)
            try:
                first.wait_for(state="visible", timeout=6_000)
            except Exception:
                return
            if FIRST_NAME and not first.input_value().strip():
                set_text(first, FIRST_NAME, "first name")
            if LAST_NAME and not last.input_value().strip():
                set_text(last, LAST_NAME, "last name")
            if first.input_value().strip() and last.input_value().strip():
                form.locator("button[type='submit']").first.click()
            else:
                log("Type your name in the window, then press 'Continue to bank'.")

        try_step("personal details", personal_details, page)

        log(f"Waiting up to {LINK_WAIT_MINUTES} min for the iDEAL page...")
        deadline = time.time() + LINK_WAIT_MINUTES * 60
        ideal = None
        while time.time() < deadline:
            ideal = find_ideal_page(context)
            if ideal:
                break
            try:
                page.wait_for_timeout(1000)
            except Exception:
                time.sleep(1)

        if not ideal:
            log("No iDEAL page appeared. Nothing saved.")
            if db_email_id:
                panel_report_order(
                    db_email_id, active_email, price=PRICE_EUR,
                    error="No iDEAL page appeared",
                )
            finish()
            sys.exit(1)

        save_link(ideal.url)
        if db_email_id:
            panel_report_order(
                db_email_id, active_email,
                ideal_link=ideal.url, price=PRICE_EUR,
            )
        if MODE == "local":
            try:
                input("Link saved. Press Enter to close the browser... ")
            except EOFError:
                page.wait_for_timeout(3000)
        finish()
        log("=== Order complete ===")


if __name__ == "__main__":
    select_tier(_parse_price_arg())
    try:
        main()
    except PWTimeout:
        log("Timed out. Run the script again.")
        sys.exit(1)
