#!/usr/bin/env python3
"""
Probe route-explorer.com end-to-end from outside our iOS app.

Tests, in order:
  1. Plain requests.get('/api/token') with browser-shaped headers.
  2. Homepage → cookies → retry /api/token (same session).
  3. cloudscraper (Cloudflare-aware) if installed.
  4. playwright headless Chromium → load homepage → accept cookies →
     click Retry → wait for /api/token to return 200, capture cookies,
     re-issue /api/token from a plain requests session using those cookies.
  5. If we ever land a token: call /api/flight-search for DAL→HOU today
     and dump the flight numbers + times.
  6. Verify public Vercel blob data (the catalog path).

The point: prove or disprove that *anything* outside Safari-with-history
can reach /api/flight-search, and if it can, what it took.

Usage: python3 probe_route_explorer.py
"""
from __future__ import annotations
import json
import sys
import time
from datetime import date

BASE = "https://route-explorer.com"
BLOB = "https://g80l6xxwjkrjoai7.public.blob.vercel-storage.com"

HEADERS_SAFARI_IPHONE = {
    "User-Agent": (
        "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) "
        "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 "
        "Mobile/15E148 Safari/604.1"
    ),
    "Accept": "application/json",
    "Accept-Language": "en-US,en;q=0.9",
    "Origin": BASE,
    "Referer": BASE + "/",
}


def line(s=""):
    print(s, flush=True)


def section(title: str):
    line()
    line("=" * 72)
    line(f"  {title}")
    line("=" * 72)


# ---------------------------------------------------------------------------

def test_plain_requests():
    section("1. Plain requests with browser-shaped headers")
    import requests
    r = requests.get(f"{BASE}/api/token", headers=HEADERS_SAFARI_IPHONE, timeout=15)
    line(f"  /api/token  →  HTTP {r.status_code}")
    line(f"  body: {r.text[:300]}")
    line(f"  set-cookies: {[c.name for c in r.cookies]}")
    return r


def test_session_homepage_first():
    section("2. requests.Session: homepage → cookies → retry /api/token")
    import requests
    s = requests.Session()
    s.headers.update(HEADERS_SAFARI_IPHONE)

    r1 = s.get(BASE + "/", timeout=15)
    line(f"  GET /         → HTTP {r1.status_code}  cookies: {[c.name for c in s.cookies]}")

    r2 = s.get(f"{BASE}/api/token", timeout=15)
    line(f"  GET /api/token→ HTTP {r2.status_code}  body: {r2.text[:200]}")
    line(f"  cookies after: {[c.name for c in s.cookies]}")
    return s, r2


def test_cloudscraper():
    section("3. cloudscraper (if installed)")
    try:
        import cloudscraper  # type: ignore
    except ImportError:
        line("  cloudscraper NOT installed. (pip install cloudscraper)")
        return None
    s = cloudscraper.create_scraper()
    r = s.get(f"{BASE}/api/token", timeout=30)
    line(f"  /api/token → HTTP {r.status_code}")
    line(f"  body: {r.text[:300]}")
    line(f"  cookies: {[c.name for c in s.cookies]}")
    return s if r.status_code == 200 else None


def test_playwright(headless: bool = True, label: str = "headless"):
    section(f"4. Playwright Chromium ({label}) — full clearance dance")
    try:
        from playwright.sync_api import sync_playwright  # type: ignore
    except ImportError:
        line("  playwright NOT installed. (pip install playwright && playwright install chromium)")
        return None

    with sync_playwright() as p:
        # In headed mode, use the full chromium build, not the headless shell.
        if headless:
            browser = p.chromium.launch(headless=True)
        else:
            browser = p.chromium.launch(headless=False, args=["--disable-blink-features=AutomationControlled"])
        ctx = browser.new_context(
            user_agent=HEADERS_SAFARI_IPHONE["User-Agent"],
        )
        page = ctx.new_page()
        status_codes: list[tuple[str, int]] = []
        page.on("response", lambda r: (
            status_codes.append((r.url, r.status))
            if "/api/" in r.url and BASE in r.url else None
        ))
        line("  goto homepage…")
        page.goto(BASE + "/", wait_until="domcontentloaded", timeout=30000)

        # accept cookies
        page.evaluate("""() => {
            for (const b of document.querySelectorAll('button')) {
                if (/accept|agree|allow/i.test((b.innerText||'').trim())) b.click();
            }
        }""")
        line("  accepted cookie banner")

        # tap Retry repeatedly + wait for clearance
        cleared = False
        for tick in range(1, 31):
            page.wait_for_timeout(1000)
            page.evaluate("""() => {
                for (const b of document.querySelectorAll('button')) {
                    if (/retry/i.test((b.innerText||'').trim())) b.click();
                }
            }""")
            try:
                status = page.evaluate("""async () => {
                    try {
                        const r = await fetch('/api/token', { credentials: 'include' });
                        return r.status;
                    } catch (e) { return -1; }
                }""")
            except Exception as e:
                status = -1
            cookie_names = sorted(c["name"] for c in ctx.cookies())
            line(f"  t+{tick:2d}s /api/token→{status}  cookies={cookie_names}")
            if status == 200:
                cleared = True
                break

        cookies = ctx.cookies()
        ua = ctx._impl_obj._initializer.get("userAgent")  # type: ignore
        line(f"  final cleared={cleared}  cookies={[c['name'] for c in cookies]}")
        browser.close()

        if cleared:
            # Build a plain requests session pre-loaded with the cookies and
            # test whether /api/token survives outside the browser context.
            import requests
            s = requests.Session()
            s.headers.update(HEADERS_SAFARI_IPHONE)
            for c in cookies:
                s.cookies.set(c["name"], c["value"], domain=c["domain"], path=c["path"])
            r = s.get(f"{BASE}/api/token", timeout=15)
            line(f"  REPLAY via requests with captured cookies → HTTP {r.status_code}")
            line(f"  body: {r.text[:200]}")
            if r.status_code == 200:
                token = r.json().get("token")
                line(f"  TOKEN MINTED: {token[:24]}…")
                return s, token
        return None


def test_undetected_chromedriver():
    section("4b. undetected-chromedriver (Cloudflare-aware Selenium)")
    try:
        import undetected_chromedriver as uc  # type: ignore
    except ImportError:
        line("  undetected-chromedriver NOT installed.")
        return None
    opts = uc.ChromeOptions()
    opts.add_argument("--headless=new")
    driver = uc.Chrome(options=opts, version_main=None)
    try:
        driver.get(BASE + "/")
        time.sleep(2)
        # accept cookies
        driver.execute_script("""
            for (const b of document.querySelectorAll('button')) {
                if (/accept|agree|allow/i.test((b.innerText||'').trim())) b.click();
            }
        """)
        cleared = False
        for tick in range(1, 31):
            time.sleep(1)
            try:
                status = driver.execute_script("""
                    return new Promise((res) => {
                        fetch('/api/token', { credentials: 'include' })
                            .then(r => res(r.status))
                            .catch(() => res(-1));
                    });
                """)
            except Exception:
                status = -1
            cookies = sorted(c["name"] for c in driver.get_cookies())
            line(f"  t+{tick:2d}s /api/token→{status}  cookies={cookies}")
            if status == 200:
                cleared = True
                break
        result = None
        if cleared:
            import requests
            s = requests.Session()
            s.headers.update(HEADERS_SAFARI_IPHONE)
            for c in driver.get_cookies():
                s.cookies.set(c["name"], c["value"], domain=c["domain"], path=c["path"])
            r = s.get(f"{BASE}/api/token", timeout=15)
            line(f"  REPLAY via requests → HTTP {r.status_code}  body: {r.text[:200]}")
            if r.status_code == 200:
                result = (s, r.json().get("token"))
        return result
    finally:
        driver.quit()


def test_flight_search(session, token):
    section("5. /api/flight-search for DAL→HOU today")
    if not session or not token:
        line("  no session/token → skipped")
        return
    today = date.today().isoformat()
    body = {
        "endpoint": "/route",
        "body": {
            "json": {
                "departureAirportIata": "DAL",
                "arrivalAirportIata":   "HOU",
                "departureDates":       [today],
                "maxStops":             0,
                "limit":                50,
                "includeAppendix":      True,
            }
        }
    }
    import requests
    r = session.post(
        f"{BASE}/api/flight-search",
        headers={**HEADERS_SAFARI_IPHONE, "Content-Type": "application/json", "X-API-Token": token},
        json=body, timeout=20,
    )
    line(f"  /api/flight-search → HTTP {r.status_code}")
    if r.status_code != 200:
        line(f"  body: {r.text[:400]}")
        return
    data = r.json()
    conns = data.get("json", {}).get("connections", [])
    line(f"  → {len(conns)} connections")
    for c in conns[:8]:
        for f in c.get("flights", []):
            line(f"    {f.get('carrierIata')}{f.get('flightNumber')}  "
                 f"{f.get('departure',{}).get('airportIata')}@"
                 f"{f.get('departure',{}).get('dateTime')}  →  "
                 f"{f.get('arrival',{}).get('airportIata')}@"
                 f"{f.get('arrival',{}).get('dateTime')}  "
                 f"({f.get('equipmentIata')})")


def test_blob_catalog():
    section("6. Public Vercel blob — no auth, raw route catalog")
    import requests
    urls = [
        "/data/airports-with-routes.json",
        "/data/airlines.json",
        "/data/routes/DAL.json",
    ]
    for u in urls:
        r = requests.get(BLOB + u, timeout=15)
        line(f"  GET {u}  → HTTP {r.status_code}  size={len(r.content):,}B")
    # sample DAL→HOU from blob
    dal = requests.get(BLOB + "/data/routes/DAL.json", timeout=15).json()
    hou = [r for r in dal["routes"] if r["dest"] == "HOU"]
    line(f"  DAL→HOU in blob: {hou[0] if hou else '<not found>'}")


# ---------------------------------------------------------------------------

def main():
    sess = None
    token = None

    test_plain_requests()
    test_session_homepage_first()

    if r := test_cloudscraper():
        sess, token = r, None  # cloudscraper currently won't carry token, see below

    if not (sess and token):
        if result := test_playwright(headless=True, label="headless"):
            sess, token = result
    if not (sess and token):
        if result := test_undetected_chromedriver():
            sess, token = result
    if not (sess and token):
        line()
        line(">>> headless approaches all failed. Trying HEADED Chromium...")
        line(">>> (window will appear on your screen)")
        if result := test_playwright(headless=False, label="HEADED"):
            sess, token = result

    if sess and token:
        test_flight_search(sess, token)
    else:
        line()
        line("No path produced a token — /api/flight-search step skipped.")

    test_blob_catalog()

    section("CONCLUSION")
    if sess and token:
        line(f"  Reached /api/flight-search with status 200. The data IS reachable")
        line(f"  programmatically — Playwright-with-real-Chromium passes the gate.")
        line(f"  Path forward: small backend that mints tokens this way and serves")
        line(f"  the iOS app, or pin the captured cookie into the app's WKWebView.")
    else:
        line("  No request shape outside real Safari managed to mint a token.")
        line("  The gate categorically rejects URLSession + WKWebView + headless")
        line("  Chromium without sticky cumulative session state.")
        line()
        line("  But blob catalog data IS public — browse-style UX is achievable")
        line("  without any auth.")

if __name__ == "__main__":
    main()
