#!/usr/bin/env python3
"""
GHL OAuth Private App Creator v10 — Headed Playwright Chromium

DIAGNOSIS (v6-v9): Google reCAPTCHA detects headless Chromium via sec-ch-ua
containing "HeadlessChrome" and rejects the captcha token (RCE-02 Failed).
CDP approach (v9) failed because WSL2 can't reach Windows Chrome's localhost.

SOLUTION: Launch Playwright Chromium in HEADED mode (headless=False).
In headed mode, sec-ch-ua shows "Chromium" (no HeadlessChrome).
DISPLAY=:0 is available in this WSL2 environment.

FLOW:
  Step 1: Login (fill email/password, submit, handle reCAPTCHA naturally)
  Step 2: OTP (detect OTP page, poll for code from file, enter + verify)
  Step 3: Navigate to My Apps
  Step 4: Create App (click Create App button)
  Step 5: Fill Details (name, type=Private, distribution=Agency)
  Step 6: Set Redirect URI
  Step 7: Select 23 OAuth Scopes
  Step 8: Save/Create
  Step 9: Capture Client ID + Client Secret
"""

import json
import time
import sys
import os
from pathlib import Path
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeout

# ─── Configuration ──────────────────────────────────────────────────
LOGIN_URL = "https://marketplace.gohighlevel.com/login"
MARKETPLACE_URL = "https://marketplace.gohighlevel.com"
EMAIL = "kinan@agileadapt.com"
PASSWORD = "SystemBird505*"
APP_NAME = "Genesis CRM Bridge"
REDIRECT_URI = "https://api.sunaivadigital.com/ghl/oauth/callback"
SCREENSHOT_DIR = Path("/mnt/e/genesis-system/GHL/oauth/screenshots")
CREDENTIALS_FILE = Path("/mnt/e/genesis-system/GHL/oauth/credentials_captured.json")
OTP_FILE = Path("/mnt/e/genesis-system/GHL/oauth/otp_code.txt")

REQUIRED_SCOPES = [
    "contacts.readonly", "contacts.write",
    "locations.readonly", "locations.write",
    "opportunities.readonly", "opportunities.write",
    "calendars.readonly", "calendars.write",
    "conversations.readonly", "conversations.write",
    "workflows.readonly",
    "campaigns.readonly", "campaigns.write",
    "users.readonly", "users.write",
    "custom-fields.readonly", "custom-fields.write",
    "custom-values.readonly", "custom-values.write",
    "oauth.readonly", "oauth.write",
    "forms.readonly", "forms.write",
]

# ─── Helpers ────────────────────────────────────────────────────────
step_counter = 0

def ss(page, label):
    global step_counter
    step_counter += 1
    path = SCREENSHOT_DIR / f"{step_counter:02d}_{label}.png"
    try:
        page.screenshot(path=str(path), full_page=True)
    except Exception as e:
        print(f"  [SS ERROR] {e}")
    print(f"  [SS] {path.name}")
    sys.stdout.flush()

def dump_html(page, label):
    path = SCREENSHOT_DIR / f"DEBUG_{label}.html"
    try:
        with open(path, "w", encoding="utf-8") as f:
            f.write(page.content())
        print(f"  [HTML] {path.name} ({path.stat().st_size} bytes)")
    except Exception as e:
        print(f"  [HTML ERROR] {e}")

def dump_text(page, label):
    path = SCREENSHOT_DIR / f"DEBUG_{label}.txt"
    try:
        text = page.evaluate("document.body?.innerText || ''")
        with open(path, "w", encoding="utf-8") as f:
            f.write(text)
        print(f"  [TXT] {path.name} ({len(text)} chars)")
    except Exception as e:
        print(f"  [TXT ERROR] {e}")

def wait_idle(page, timeout=15000):
    try:
        page.wait_for_load_state("networkidle", timeout=timeout)
    except:
        pass

def read_otp_from_file(timeout_seconds=180):
    """Poll OTP file for a 6-digit code."""
    if OTP_FILE.exists():
        OTP_FILE.unlink()
    OTP_FILE.write_text("")
    print(f"\n  {'='*60}")
    print(f"  OTP REQUIRED!")
    print(f"  GHL sent a 6-digit code to ki***@agileadapt.com")
    print(f"  Write code to: {OTP_FILE}")
    print(f"  CMD: echo 123456 > '{OTP_FILE}'")
    print(f"  Waiting {timeout_seconds}s...")
    print(f"  {'='*60}\n")
    sys.stdout.flush()

    start = time.time()
    while time.time() - start < timeout_seconds:
        try:
            content = OTP_FILE.read_text().strip()
            if len(content) == 6 and content.isdigit():
                print(f"  OTP received: {content}")
                return content
        except:
            pass
        time.sleep(3)
        elapsed = int(time.time() - start)
        if elapsed % 15 == 0 and elapsed > 0:
            print(f"  ... waiting ({elapsed}s)")
            sys.stdout.flush()
    return None


def main():
    SCREENSHOT_DIR.mkdir(parents=True, exist_ok=True)
    # Clean old screenshots
    for f in SCREENSHOT_DIR.glob("*.png"):
        f.unlink()
    for f in SCREENSHOT_DIR.glob("DEBUG_*"):
        f.unlink()

    print("=" * 70)
    print("  GHL OAuth Private App Creator v10")
    print("  Headed Playwright Chromium (reCAPTCHA bypass via headed mode)")
    print("=" * 70)
    print(f"  Login:        {LOGIN_URL}")
    print(f"  App Name:     {APP_NAME}")
    print(f"  Redirect URI: {REDIRECT_URI}")
    print(f"  Scopes:       {len(REQUIRED_SCOPES)}")
    print(f"  OTP file:     {OTP_FILE}")
    print("=" * 70)
    sys.stdout.flush()

    with sync_playwright() as p:
        # Launch HEADED Chromium -- critical for reCAPTCHA bypass
        # headed mode sec-ch-ua = "Chromium" (no HeadlessChrome)
        browser = p.chromium.launch(
            headless=False,
            args=[
                "--disable-blink-features=AutomationControlled",
                "--window-size=1400,900",
            ]
        )
        context = browser.new_context(
            viewport={"width": 1400, "height": 900},
            user_agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
        )
        page = context.new_page()
        page.set_default_timeout(30000)

        # Remove webdriver flag
        page.add_init_script("""
            Object.defineProperty(navigator, 'webdriver', { get: () => false });
        """)

        # Monitor login API responses
        login_api_status = {}

        def on_response(response):
            url = response.url
            if 'login' in url and 'leadconnector' in url:
                try:
                    body = response.body().decode('utf-8')
                    login_api_status['status'] = response.status
                    login_api_status['body'] = body[:2000]
                    print(f"  [LOGIN-API] {response.status}: {body[:300]}")
                    sys.stdout.flush()
                except:
                    login_api_status['status'] = response.status
            if 'verify' in url.lower() and ('otp' in url.lower() or 'code' in url.lower()):
                try:
                    body = response.body().decode('utf-8')
                    print(f"  [OTP-API] {response.status}: {body[:300]}")
                    sys.stdout.flush()
                except:
                    pass

        page.on("response", on_response)

        try:
            # ──────── STEP 1: LOGIN ────────────────────────────────────
            print("\n===[ STEP 1: Login ]===")
            sys.stdout.flush()

            page.goto(LOGIN_URL, timeout=60000)
            wait_idle(page)
            page.wait_for_timeout(3000)
            ss(page, "login_page")
            print(f"  URL: {page.url}")
            dump_html(page, "login_page")

            # Find and fill email
            email_filled = False
            try:
                email_input = page.wait_for_selector(
                    'input[placeholder*="email" i], input[name*="email" i], input[type="email"]',
                    state="visible", timeout=15000
                )
                email_input.click()
                email_input.fill(EMAIL)
                email_filled = True
                print(f"  Email filled: {EMAIL}")
            except Exception as e:
                print(f"  Email input not found: {e}")
                dump_text(page, "no_email_input")

            # Find and fill password
            pwd_filled = False
            try:
                pwd_input = page.wait_for_selector(
                    'input[type="password"]', state="visible", timeout=5000
                )
                pwd_input.click()
                pwd_input.fill(PASSWORD)
                pwd_filled = True
                print(f"  Password filled")
            except Exception as e:
                print(f"  Password input not found: {e}")

            if not email_filled or not pwd_filled:
                print("  FATAL: Could not fill login form")
                ss(page, "login_form_fail")
                dump_html(page, "login_form_fail")
                sys.exit(1)

            ss(page, "creds_filled")

            # Click Sign In
            try:
                sign_in = page.locator('button:has-text("Sign in")').first
                sign_in.scroll_into_view_if_needed()
                page.wait_for_timeout(500)
                sign_in.click()
                print("  Clicked Sign In button")
            except Exception as e:
                print(f"  Sign In button click failed: {e}")
                # Fallback: try pressing Enter
                page.keyboard.press("Enter")
                print("  Pressed Enter as fallback")

            sys.stdout.flush()

            # Wait for login API response
            print("  Waiting for login API response...")
            for i in range(20):
                page.wait_for_timeout(500)
                if login_api_status.get('status'):
                    break
            page.wait_for_timeout(3000)

            ss(page, "after_submit")
            api_status = login_api_status.get('status', 0)
            api_body = login_api_status.get('body', '')
            print(f"  Login API status: {api_status}")

            if api_status == 400:
                if 'RCE' in api_body:
                    print("  ERROR: reCAPTCHA still failing (RCE-02)")
                    print("  This should not happen in headed mode.")
                    print(f"  API body: {api_body[:500]}")
                    dump_html(page, "recaptcha_fail")
                    sys.exit(1)
                else:
                    print(f"  Login error: {api_body[:300]}")
                    sys.exit(1)

            # ──────── STEP 2: OTP ────────────────────────────────────
            print("\n===[ STEP 2: OTP Check ]===")
            sys.stdout.flush()

            page.wait_for_timeout(3000)

            # Detect page state
            has_pwd = page.evaluate("""
                (() => {
                    const pwd = document.querySelector('input[type="password"]');
                    return pwd && pwd.offsetParent !== null;
                })()
            """)
            visible_inputs = page.evaluate("""
                [...document.querySelectorAll('input')]
                    .filter(el => el.offsetParent !== null && el.type !== 'hidden')
                    .length
            """)
            btn_texts = page.evaluate("""
                [...document.querySelectorAll('button')]
                    .filter(el => el.offsetParent !== null)
                    .map(b => b.textContent.trim())
            """)
            page_text_snippet = page.evaluate("(document.body?.innerText || '').substring(0, 500)")

            print(f"  Has password field: {has_pwd}")
            print(f"  Visible inputs: {visible_inputs}")
            print(f"  Buttons: {btn_texts}")
            print(f"  Page text: {page_text_snippet[:200]}")

            # OTP detection: no password field + multiple inputs OR "Verify" button
            otp_detected = (
                (not has_pwd and visible_inputs >= 4) or
                any('erify' in b for b in btn_texts) or
                'otp' in page_text_snippet.lower() or
                'security code' in page_text_snippet.lower() or
                'verification' in page_text_snippet.lower() or
                api_status == 200
            )

            if otp_detected:
                print("  OTP page detected!")
                ss(page, "otp_page")

                otp_code = read_otp_from_file(180)
                if not otp_code:
                    print("  No OTP provided within timeout")
                    ss(page, "otp_timeout")
                    sys.exit(1)

                # Focus first visible input and type OTP digits
                page.evaluate("""
                    (() => {
                        const inputs = [...document.querySelectorAll('input')]
                            .filter(el => el.offsetParent !== null && el.type !== 'hidden');
                        if (inputs.length > 0) {
                            inputs[0].focus();
                            inputs[0].click();
                        }
                    })()
                """)
                page.wait_for_timeout(500)

                # Type each digit with delay
                for digit in otp_code:
                    page.keyboard.type(digit, delay=150)
                    page.wait_for_timeout(200)

                ss(page, "otp_entered")
                print(f"  OTP entered: {otp_code}")

                # Click Verify button
                page.evaluate("""
                    (() => {
                        const btn = [...document.querySelectorAll('button')]
                            .find(b => b.textContent.toLowerCase().includes('verify'));
                        if (btn) btn.click();
                    })()
                """)
                print("  Clicked Verify")
                page.wait_for_timeout(10000)
                wait_idle(page)
                ss(page, "after_verify")
                print(f"  URL after verify: {page.url}")

            elif has_pwd and api_status == 0:
                # Still on login page, login may not have triggered
                print("  Still on login page. Login may not have submitted.")
                print("  Trying again with a direct form submit...")
                page.evaluate("""
                    (() => {
                        const form = document.querySelector('form');
                        if (form) form.submit();
                    })()
                """)
                page.wait_for_timeout(8000)
                ss(page, "retry_submit")
                api_status = login_api_status.get('status', 0)
                print(f"  Retry API status: {api_status}")

            elif not has_pwd and visible_inputs < 4 and api_status == 0:
                # Page changed but no OTP -- might have logged in directly
                print("  Login page changed, no OTP detected")
                print(f"  URL: {page.url}")
                # Might already be logged in
                if 'login' not in page.url:
                    print("  Appears to be logged in!")

            # ──────── STEP 3: MY APPS ──────────────────────────────────
            print("\n===[ STEP 3: My Apps ]===")
            sys.stdout.flush()

            # Navigate to apps page
            page.goto(f"{MARKETPLACE_URL}/apps", timeout=30000)
            wait_idle(page)
            page.wait_for_timeout(5000)
            ss(page, "apps_page")
            print(f"  URL: {page.url}")

            if "login" in page.url:
                print("  FATAL: Redirected to login - not authenticated")
                dump_html(page, "not_authenticated")
                dump_text(page, "not_authenticated")
                sys.exit(1)

            dump_html(page, "apps_page")
            dump_text(page, "apps_page")

            # Try to click "My Apps" if it's a nav item
            page.evaluate("""
                (() => {
                    const items = [...document.querySelectorAll('a, button, span, li, div, [role="tab"]')];
                    const myApps = items.find(i => {
                        const t = i.textContent.trim();
                        return t === 'My Apps' || t === 'My apps';
                    });
                    if (myApps) myApps.click();
                })()
            """)
            page.wait_for_timeout(3000)
            ss(page, "my_apps")

            # Debug: list all clickable items
            nav_items = page.evaluate("""
                [...document.querySelectorAll('a, button')]
                    .filter(el => el.offsetParent !== null)
                    .map(el => ({
                        text: el.textContent.trim().substring(0, 50),
                        href: el.href || '',
                        tag: el.tagName
                    }))
                    .filter(e => e.text.length > 0 && e.text.length < 50)
            """)
            print(f"  Clickable items ({len(nav_items)}):")
            for item in nav_items[:20]:
                print(f"    <{item['tag']}> \"{item['text']}\" -> {item['href'][:60] if item['href'] else '-'}")

            # ──────── STEP 4: CREATE APP ────────────────────────────────
            print("\n===[ STEP 4: Create App ]===")
            sys.stdout.flush()

            # Try multiple selectors for Create App button
            create_clicked = page.evaluate("""
                (() => {
                    // Try exact text match first
                    const items = [...document.querySelectorAll('a, button, span')];
                    for (const text of ['Create App', 'Create app', 'Create New App', 'New App', '+ Create App']) {
                        const el = items.find(i => i.textContent.trim() === text);
                        if (el && el.offsetParent !== null) {
                            el.click();
                            return text;
                        }
                    }
                    // Try partial match
                    const create = items.find(i => {
                        const t = i.textContent.trim().toLowerCase();
                        return (t.includes('create') && t.includes('app')) || t === 'create';
                    });
                    if (create && create.offsetParent !== null) {
                        create.click();
                        return 'partial: ' + create.textContent.trim();
                    }
                    return null;
                })()
            """)
            print(f"  Create App clicked: {create_clicked}")
            page.wait_for_timeout(5000)
            ss(page, "create_form")
            dump_html(page, "create_form")

            if not create_clicked:
                print("  Create App button not found. Dumping page state...")
                dump_text(page, "no_create_btn")

            # ──────── STEP 5: FILL DETAILS ──────────────────────────────
            print("\n===[ STEP 5: Fill Details ]===")
            sys.stdout.flush()

            # Enumerate form elements
            form_elements = page.evaluate("""
                [...document.querySelectorAll('input, select, textarea')]
                    .filter(el => el.offsetParent !== null)
                    .map(el => ({
                        tag: el.tagName,
                        type: el.type || '',
                        name: el.name || '',
                        placeholder: el.placeholder || '',
                        id: el.id || '',
                        value: el.value || ''
                    }))
            """)
            print(f"  Form elements: {len(form_elements)}")
            for el in form_elements:
                print(f"    <{el['tag']} type=\"{el['type']}\" name=\"{el['name']}\" placeholder=\"{el['placeholder']}\" id=\"{el['id']}\">")

            # Fill App Name using multiple strategies
            name_filled = page.evaluate(f"""
                (() => {{
                    // Strategy 1: Find input by name/placeholder/id containing 'name'
                    const inputs = [...document.querySelectorAll('input[type="text"], input:not([type])')];
                    for (const inp of inputs) {{
                        if (inp.offsetParent === null) continue;
                        const n = (inp.name || '').toLowerCase();
                        const p = (inp.placeholder || '').toLowerCase();
                        const id = (inp.id || '').toLowerCase();
                        if (n.includes('name') || p.includes('name') || id.includes('name') || p.includes('app')) {{
                            inp.focus();
                            inp.value = '';
                            const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
                                window.HTMLInputElement.prototype, 'value').set;
                            nativeInputValueSetter.call(inp, '{APP_NAME}');
                            inp.dispatchEvent(new Event('input', {{ bubbles: true }}));
                            inp.dispatchEvent(new Event('change', {{ bubbles: true }}));
                            return 'name-match';
                        }}
                    }}
                    // Strategy 2: Find label containing 'App Name' and get associated input
                    const labels = [...document.querySelectorAll('label')];
                    for (const label of labels) {{
                        if (label.textContent.toLowerCase().includes('app name') ||
                            label.textContent.toLowerCase().includes('application name')) {{
                            const inp = label.querySelector('input') ||
                                        document.getElementById(label.htmlFor) ||
                                        label.parentElement?.querySelector('input');
                            if (inp) {{
                                inp.focus();
                                const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
                                    window.HTMLInputElement.prototype, 'value').set;
                                nativeInputValueSetter.call(inp, '{APP_NAME}');
                                inp.dispatchEvent(new Event('input', {{ bubbles: true }}));
                                inp.dispatchEvent(new Event('change', {{ bubbles: true }}));
                                return 'label-match';
                            }}
                        }}
                    }}
                    // Strategy 3: First visible text input as fallback
                    for (const inp of inputs) {{
                        if (inp.offsetParent !== null) {{
                            inp.focus();
                            const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
                                window.HTMLInputElement.prototype, 'value').set;
                            nativeInputValueSetter.call(inp, '{APP_NAME}');
                            inp.dispatchEvent(new Event('input', {{ bubbles: true }}));
                            inp.dispatchEvent(new Event('change', {{ bubbles: true }}));
                            return 'fallback';
                        }}
                    }}
                    return null;
                }})()
            """)
            print(f"  App name set via: {name_filled}")

            # Select Private type
            private_selected = page.evaluate("""
                (() => {
                    // Try radio/checkbox labeled "Private"
                    const labels = [...document.querySelectorAll('label')];
                    for (const l of labels) {
                        if (l.textContent.trim().toLowerCase().includes('private')) {
                            l.click();
                            return 'label';
                        }
                    }
                    // Try any element with text "Private"
                    const els = [...document.querySelectorAll('span, div, [role="option"], [role="radio"]')];
                    for (const el of els) {
                        if (el.textContent.trim() === 'Private') {
                            el.click();
                            return 'element';
                        }
                    }
                    return null;
                })()
            """)
            print(f"  Private type: {private_selected}")
            page.wait_for_timeout(500)

            # Select Agency distribution
            agency_selected = page.evaluate("""
                (() => {
                    const labels = [...document.querySelectorAll('label')];
                    for (const l of labels) {
                        if (l.textContent.trim().toLowerCase().includes('agency')) {
                            l.click();
                            return 'label';
                        }
                    }
                    const els = [...document.querySelectorAll('span, div, [role="option"], [role="radio"]')];
                    for (const el of els) {
                        if (el.textContent.trim() === 'Agency') {
                            el.click();
                            return 'element';
                        }
                    }
                    return null;
                })()
            """)
            print(f"  Agency distribution: {agency_selected}")
            page.wait_for_timeout(500)
            ss(page, "details_filled")

            # ──────── STEP 6: REDIRECT URI ──────────────────────────────
            print("\n===[ STEP 6: Redirect URI ]===")
            sys.stdout.flush()

            redirect_set = page.evaluate(f"""
                (() => {{
                    const inputs = [...document.querySelectorAll('input')];
                    for (const inp of inputs) {{
                        if (inp.offsetParent === null) continue;
                        const n = (inp.name || '').toLowerCase();
                        const p = (inp.placeholder || '').toLowerCase();
                        const id = (inp.id || '').toLowerCase();
                        if (n.includes('redirect') || p.includes('redirect') ||
                            p.includes('callback') || p.includes('url') ||
                            id.includes('redirect')) {{
                            inp.focus();
                            const setter = Object.getOwnPropertyDescriptor(
                                window.HTMLInputElement.prototype, 'value').set;
                            setter.call(inp, '{REDIRECT_URI}');
                            inp.dispatchEvent(new Event('input', {{ bubbles: true }}));
                            inp.dispatchEvent(new Event('change', {{ bubbles: true }}));
                            return 'set';
                        }}
                    }}
                    // Check labels
                    const labels = [...document.querySelectorAll('label')];
                    for (const label of labels) {{
                        if (label.textContent.toLowerCase().includes('redirect')) {{
                            const inp = label.querySelector('input') ||
                                        document.getElementById(label.htmlFor) ||
                                        label.parentElement?.querySelector('input');
                            if (inp) {{
                                inp.focus();
                                const setter = Object.getOwnPropertyDescriptor(
                                    window.HTMLInputElement.prototype, 'value').set;
                                setter.call(inp, '{REDIRECT_URI}');
                                inp.dispatchEvent(new Event('input', {{ bubbles: true }}));
                                inp.dispatchEvent(new Event('change', {{ bubbles: true }}));
                                return 'label-set';
                            }}
                        }}
                    }}
                    return 'not-found';
                }})()
            """)
            print(f"  Redirect URI: {redirect_set}")
            ss(page, "redirect_uri")

            # ──────── STEP 7: SCOPES ────────────────────────────────────
            print("\n===[ STEP 7: Scopes ]===")
            sys.stdout.flush()

            # Try to find and click Scopes tab/section
            scopes_tab = page.evaluate("""
                (() => {
                    const items = [...document.querySelectorAll('a, button, span, li, [role="tab"], div')];
                    const s = items.find(i => {
                        const t = i.textContent.trim();
                        return t === 'Scopes' || t === 'OAuth Scopes' || t === 'Permissions';
                    });
                    if (s) { s.click(); return s.textContent.trim(); }
                    return null;
                })()
            """)
            print(f"  Scopes tab: {scopes_tab}")
            page.wait_for_timeout(3000)
            ss(page, "scopes_section")

            # Select each scope
            selected = []
            for scope in REQUIRED_SCOPES:
                result = page.evaluate(f"""
                    (() => {{
                        const scope = '{scope}';

                        // Method 1: TreeWalker to find exact text match
                        const walker = document.createTreeWalker(
                            document.body, NodeFilter.SHOW_TEXT);
                        while (walker.nextNode()) {{
                            const text = walker.currentNode.textContent.trim();
                            if (text === scope) {{
                                const el = walker.currentNode.parentElement;
                                // Look up for a clickable container with checkbox/switch
                                let container = el;
                                for (let i = 0; i < 5; i++) {{
                                    if (!container) break;
                                    const cb = container.querySelector(
                                        'input[type="checkbox"], [role="switch"], [role="checkbox"]');
                                    if (cb) {{
                                        cb.click();
                                        return 'checkbox';
                                    }}
                                    container = container.parentElement;
                                }}
                                // Click the text element itself
                                el.click();
                                return 'text-click';
                            }}
                        }}

                        // Method 2: Find by value attribute
                        const cb = document.querySelector(
                            'input[value="' + scope + '"], input[data-scope="' + scope + '"]');
                        if (cb) {{ cb.click(); return 'value'; }}

                        // Method 3: Find in labels
                        const labels = [...document.querySelectorAll('label')];
                        const label = labels.find(l => l.textContent.trim().includes(scope));
                        if (label) {{ label.click(); return 'label'; }}

                        return null;
                    }})()
                """)
                if result:
                    selected.append(scope)
                    print(f"    [+] {scope} ({result})")
                else:
                    print(f"    [-] {scope}")
                # Small delay between selections
                page.wait_for_timeout(200)

            print(f"\n  Scopes selected: {len(selected)}/{len(REQUIRED_SCOPES)}")
            if len(selected) < len(REQUIRED_SCOPES):
                missing = [s for s in REQUIRED_SCOPES if s not in selected]
                print(f"  Missing: {missing}")
            ss(page, "scopes_done")

            # ──────── STEP 8: SAVE ──────────────────────────────────────
            print("\n===[ STEP 8: Save / Create ]===")
            sys.stdout.flush()

            save_clicked = page.evaluate("""
                (() => {
                    const btns = [...document.querySelectorAll('button')];
                    // Priority order for button text
                    for (const text of ['Create App', 'Create', 'Save', 'Submit', 'Create app']) {
                        const btn = btns.find(b => {
                            return b.textContent.trim() === text && b.offsetParent !== null;
                        });
                        if (btn) { btn.click(); return text; }
                    }
                    // Partial match
                    const btn = btns.find(b => {
                        const t = b.textContent.trim().toLowerCase();
                        return (t.includes('create') || t === 'save') && b.offsetParent !== null;
                    });
                    if (btn) { btn.click(); return 'partial: ' + btn.textContent.trim(); }
                    return null;
                })()
            """)
            print(f"  Save clicked: {save_clicked}")
            page.wait_for_timeout(8000)
            wait_idle(page)
            ss(page, "after_save")
            print(f"  URL after save: {page.url}")

            # ──────── STEP 9: CAPTURE CREDENTIALS ───────────────────────
            print("\n===[ STEP 9: Capture Credentials ]===")
            sys.stdout.flush()

            page.wait_for_timeout(3000)
            ss(page, "creds_page")
            dump_html(page, "creds_page")
            dump_text(page, "creds_page")

            # Extract Client ID and Client Secret
            extracted = page.evaluate("""
                (() => {
                    const result = { client_id: null, client_secret: null };

                    // Method 1: From input fields
                    document.querySelectorAll('input, textarea').forEach(inp => {
                        const name = (inp.name || inp.id || '').toLowerCase();
                        const label = inp.closest('div')?.querySelector('label')?.textContent?.toLowerCase() || '';
                        const prev = inp.previousElementSibling?.textContent?.toLowerCase() || '';
                        const all = name + ' ' + label + ' ' + prev;

                        if (all.includes('client') && all.includes('id') && !all.includes('secret'))
                            result.client_id = inp.value;
                        if (all.includes('secret'))
                            result.client_secret = inp.value;
                    });

                    // Method 2: From page text via regex
                    const text = document.body?.innerText || '';
                    if (!result.client_id) {
                        const m = text.match(/Client\\s*ID[:\\s]*([a-zA-Z0-9_.\\-]{10,})/i);
                        if (m) result.client_id = m[1];
                    }
                    if (!result.client_secret) {
                        const m = text.match(/Client\\s*Secret[:\\s]*([a-zA-Z0-9_.\\-]{10,})/i);
                        if (m) result.client_secret = m[1];
                    }

                    // Method 3: Look for copy buttons next to credential values
                    document.querySelectorAll('[class*="copy"], [data-clipboard]').forEach(el => {
                        const nearby = el.parentElement?.textContent || '';
                        if (nearby.toLowerCase().includes('client id')) {
                            const val = el.getAttribute('data-clipboard-text') ||
                                        el.parentElement?.querySelector('input, code, span')?.textContent;
                            if (val && val.length > 10) result.client_id = val.trim();
                        }
                        if (nearby.toLowerCase().includes('secret')) {
                            const val = el.getAttribute('data-clipboard-text') ||
                                        el.parentElement?.querySelector('input, code, span')?.textContent;
                            if (val && val.length > 10) result.client_secret = val.trim();
                        }
                    });

                    result.page_url = window.location.href;
                    result.page_text = (text || '').substring(0, 3000);
                    return result;
                })()
            """)

            client_id = extracted.get('client_id')
            client_secret = extracted.get('client_secret')

            creds = {
                "app_name": APP_NAME,
                "client_id": client_id,
                "client_secret": client_secret,
                "redirect_uri": REDIRECT_URI,
                "scopes": REQUIRED_SCOPES,
                "scopes_selected": len(selected),
                "created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
                "page_url": extracted.get('page_url', page.url),
            }
            with open(CREDENTIALS_FILE, "w") as f:
                json.dump(creds, f, indent=2)

            ss(page, "final")

            # ──────── RESULT SUMMARY ────────────────────────────────────
            print("\n" + "=" * 70)
            print("  RESULT SUMMARY")
            print("=" * 70)
            print(f"  Client ID:     {client_id or 'NOT CAPTURED'}")
            print(f"  Client Secret: {client_secret or 'NOT CAPTURED'}")
            print(f"  Scopes:        {len(selected)}/{len(REQUIRED_SCOPES)}")
            if len(selected) < len(REQUIRED_SCOPES):
                missing = [s for s in REQUIRED_SCOPES if s not in selected]
                print(f"  Missing:       {missing}")
            print(f"  Saved to:      {CREDENTIALS_FILE}")
            print("=" * 70)

        except Exception as e:
            print(f"\n  FATAL ERROR: {e}")
            import traceback
            traceback.print_exc()
            try:
                ss(page, "FATAL")
                dump_html(page, "FATAL")
                dump_text(page, "FATAL")
            except:
                pass
        finally:
            try:
                page.close()
            except:
                pass
            try:
                browser.close()
            except:
                pass
            sys.stdout.flush()


if __name__ == "__main__":
    main()
