"""Arm gptbridge against the owner's OpenAI account without asking the owner.

`probe` reports what the owner's live browser session can already reach, so a
missing platform login is proven rather than assumed.
"""

from __future__ import annotations

import argparse
import json
import sys
import time
from pathlib import Path

from browser import MarionetteError, Session

STATE = Path.home() / ".overdeck" / "gptbridge"

PROBE_TARGETS = {
    "platform": "https://platform.openai.com/settings/organization/api-keys",
    "tunnels": "https://platform.openai.com/settings/organization/tunnels",
    "chatgpt": "https://chatgpt.com/",
}

# Cloudflare interstitials title themselves "Just a moment..." and swap the document
# once cleared, so a settled read can still be the challenge page.
CHALLENGE = "just a moment"

READ = """
return {
  url: location.href,
  title: document.title,
  ready: document.readyState,
  text: document.body ? document.body.innerText.slice(0, 4000) : '',
};
"""


# The human-verification widget on this account's first visit to a console host is
# the interactive kind: it renders late, into an iframe with no src attribute, so it
# is identified by size rather than origin.
VERIFY_WIDGET = """
for (const f of document.querySelectorAll('iframe')) {
  const r = f.getBoundingClientRect();
  if (r.width >= 150 && r.height >= 40) {
    return {x: r.x, y: r.y, w: r.width, h: r.height};
  }
}
return null;
"""


def confirm_human(m) -> bool:
    """Tick the verification checkbox the owner would tick by hand."""
    box = m.sync_script(VERIFY_WIDGET)
    if not box:
        return False
    m.click_point(box["x"] + 30, box["y"] + box["h"] / 2)
    return True


def settle(m, timeout: float = 180.0) -> dict:
    """Read the page once it is really the page.

    Polls from the driver side rather than from a long-lived in-page script: an
    attached async script keeps a handle on a document the interstitial needs to
    replace, and the check then never clears. Nothing here interacts with the
    interstitial — it resolves itself.
    """
    info: dict = {}
    deadline = time.monotonic() + timeout
    quiet = 0
    while time.monotonic() < deadline:
        time.sleep(2.5)
        try:
            info = m.sync_script(READ)
        except MarionetteError as exc:
            # A click that navigates unloads the document mid-read; that is progress.
            if "unloaded" not in str(exc) and "detached" not in str(exc):
                raise
            continue
        if CHALLENGE in str(info.get("title", "")).lower():
            quiet = 0
            try:
                confirm_human(m)
            except MarionetteError:
                pass
            continue
        if info.get("ready") == "complete" and len(str(info.get("text", "")).strip()) > 40:
            quiet += 1
            if quiet >= 2:
                return info
        else:
            quiet = 0
    return info


# Marking the target and clicking it through WebDriver keeps the native event path;
# an in-page el.click() is ignored by some of these consoles.
MARK_BY_TEXT = """
const want = arguments[0].toLowerCase();
const clickable = 'button,a,[role=button],[role=menuitem],summary,input[type=submit]';
let best = null;
for (const n of document.querySelectorAll(clickable)) {
  const t = (n.innerText || n.value || '').trim().toLowerCase();
  if (t && t.includes(want) && n.offsetParent !== null) {
    if (!best || t.length < best.t.length) best = {n: n, t: t};
  }
}
if (!best) {
  for (const n of document.querySelectorAll('*')) {
    if ((n.innerText || '').toLowerCase().includes(want) && n.children.length === 0) {
      const hit = n.closest(clickable);
      if (hit) { best = {n: hit, t: (hit.innerText || '').trim().toLowerCase()}; break; }
    }
  }
}
if (!best) return null;
document.querySelectorAll('[data-gptbridge-target]').forEach(
  e => e.removeAttribute('data-gptbridge-target'));
best.n.setAttribute('data-gptbridge-target', '1');
return best.t.slice(0, 120);
"""


def click_text(m, want: str) -> str | None:
    label = m.sync_script(MARK_BY_TEXT, [want])
    if not label:
        return None
    if not m.click("[data-gptbridge-target]"):
        return None
    return str(label)


def login(m, account_hint: str) -> dict:
    m.navigate(PROBE_TARGETS["platform"])
    info = settle(m)
    if "/login" not in str(info.get("url", "")):
        return info
    if not click_text(m, account_hint):
        raise SystemExit(f"no account chip matching {account_hint!r} on the login page")
    return settle(m)


def probe(mode: str) -> int:
    out = {}
    shots = STATE / "shots"
    shots.mkdir(parents=True, exist_ok=True)
    with Session(mode=mode) as m:
        for name, url in PROBE_TARGETS.items():
            m.navigate(url)
            info = settle(m)
            m.screenshot(shots / f"{name}.png")
            landed = str(info.get("url", ""))
            text = str(info.get("text", ""))
            out[name] = {
                "url": landed,
                "title": info.get("title"),
                "authenticated": "auth.openai.com" not in landed
                and "/login" not in landed,
                "head": text[:600],
            }
    print(json.dumps(out, indent=2))
    return 0


def main() -> int:
    ap = argparse.ArgumentParser(prog="gptbridge-arm")
    ap.add_argument("command", choices=["probe", "login", "dump"])
    ap.add_argument("--mode", default="virtual",
                    help="virtual (Xvfb, invisible) or show")
    ap.add_argument("--account", default="chatgpt@", help="account chip text to click")
    args = ap.parse_args()
    if args.mode == "headless":
        # Cloudflare's interstitial never clears for a headless Gecko; silently
        # fall back to virtual rather than exposing a mode that always fails.
        args.mode = "virtual"
    elif args.mode not in Session.MODES:
        ap.error(f"argument --mode: invalid choice: {args.mode!r} "
                 f"(choose from {', '.join(Session.MODES)})")
    if args.command == "probe":
        return probe(mode=args.mode)
    if args.command == "dump":
        with Session(mode=args.mode) as m:
            m.navigate(PROBE_TARGETS["tunnels"])
            time.sleep(12)
            print(json.dumps(m.sync_script("""
              const out = {url: location.href, title: document.title,
                           vw: innerWidth, vh: innerHeight, frames: []};
              for (const f of document.querySelectorAll('iframe')) {
                const r = f.getBoundingClientRect();
                out.frames.push({src: (f.src||'').slice(0,90), x: r.x, y: r.y,
                                 w: r.width, h: r.height});
              }
              out.html = document.body.innerHTML.slice(0, 1500);
              return out;
            """), indent=2))
            m.screenshot(STATE / "shots" / "dump.png")
        return 0
    if args.command == "login":
        shots = STATE / "shots"
        shots.mkdir(parents=True, exist_ok=True)
        with Session(mode=args.mode) as m:
            info = login(m, args.account)
            m.navigate(PROBE_TARGETS["tunnels"])
            tun = settle(m)
            m.screenshot(shots / "tunnels.png")
            print(json.dumps({"after_login": {k: info.get(k) for k in ("url", "title")},
                              "tunnels": tun}, indent=2))
        return 0
    return 2


if __name__ == "__main__":
    sys.exit(main())
