#!/usr/bin/env python3
"""
GHL OAuth Private App Creator — Final Version
Two-phase: login+OTP, then app creation.
Usage:
  Phase 1 (trigger OTP): python3 create_ghl_app_final.py --login
  Phase 2 (enter OTP):   python3 create_ghl_app_final.py --otp 123456
  Full flow:             python3 create_ghl_app_final.py --otp 123456 --create
  Resume after login:    python3 create_ghl_app_final.py --create
"""

import json
import time
import re
import sys
import os
import argparse
import random
from pathlib import Path
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeout
from playwright_stealth import Stealth

# 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/api/ghl/oauth/callback"
SCREENSHOT_DIR = Path("/mnt/e/genesis-system/GHL/oauth/screenshots")
CREDENTIALS_FILE = Path("/mnt/e/genesis-system/GHL/oauth/app_credentials.json")
BROWSER_PROFILE = "/mnt/e/genesis-system/.browser-data/ghl-marketplace-profile"

REQUIRED_SCOPES = [
    "contacts.readonly", "contacts.write",
    "conversations.readonly", "conversations.write",
    "conversations/message.readonly", "conversations/message.write",
    "calendars.readonly", "calendars.write",
    "calendars/events.readonly", "calendars/events.write",
    "opportunities.readonly", "opportunities.write",
    "locations.readonly", "locations.write",
    "locations/customFields.readonly", "locations/customFields.write",
    "locations/customValues.readonly", "locations/customValues.write",
    "locations/tags.readonly", "locations/tags.write",
    "workflows.readonly",
    "oauth.readonly",
    "oauth.write",
]

step_num = [0]

def ss(page, name):
    step_num[0] += 1
    path = SCREENSHOT_DIR / f"{step_num[0]:02d}_{name}.png"
    page.screenshot(path=str(path), full_page=False)
    print(f"  [SS] {path.name}")
    return path

def human_type(page, text):
    for char in text:
        page.keyboard.type(char, delay=random.randint(30, 80))
        time.sleep(random.uniform(0.01, 0.05))

def mouse_jitter(page):
    for _ in range(3):
        page.mouse.move(random.randint(100, 1800), random.randint(100, 900))
        time.sleep(random.uniform(0.1, 0.3))

def is_logged_in(page):
    """Check if logged into GHL marketplace."""
    page.goto(MARKETPLACE_URL, wait_until="domcontentloaded", timeout=30000)
    page.wait_for_timeout(3000)
    try:
        header = page.locator('header, nav').first
        if header.is_visible(timeout=3000):
            text = header.text_content()
            if "My Apps" in text or "Logout" in text or "Account" in text:
                return True
            if "Sign In" in text:
                return False
    except:
        pass
    return False

def do_login(page):
    """Login and reach OTP page."""
    print("\n[LOGIN] Navigating to login page...")
    page.goto(LOGIN_URL, wait_until="domcontentloaded", timeout=60000)
    page.wait_for_timeout(3000)
    mouse_jitter(page)

    if "login" not in page.url.lower():
        print("  Not on login page — may already be authenticated")
        return True

    print("  Filling credentials...")
    email_input = page.locator('input[placeholder="Enter your email"]').first
    try:
        if not email_input.is_visible(timeout=5000):
            email_input = page.locator('input[type="email"]').first
    except:
        email_input = page.locator('input[type="email"]').first

    email_input.click()
    page.wait_for_timeout(500)
    human_type(page, EMAIL)

    page.keyboard.press("Tab")
    page.wait_for_timeout(500)

    pwd_input = page.locator('input[type="password"]').first
    pwd_input.click()
    page.wait_for_timeout(300)
    human_type(page, PASSWORD)
    page.wait_for_timeout(1000)
    mouse_jitter(page)

    print("  Clicking Sign in...")
    btn = page.locator('button:has-text("Sign in")').first
    btn.hover()
    page.wait_for_timeout(500)
    btn.click()

    print("  Waiting for response...")
    page.wait_for_timeout(10000)
    ss(page, "login_response")
    print(f"  URL: {page.url}")

    # Check for OTP page
    page_text = page.evaluate("document.body.innerText")
    if "Verify Security Code" in page_text or "OTP" in page_text:
        print("  OTP verification page detected!")
        print("  An OTP code was sent to kinan@agileadapt.com")
        ss(page, "otp_page")
        return "OTP_NEEDED"

    if "login" not in page.url.lower():
        print("  Login succeeded (no OTP needed)")
        return True

    print("  Login may have failed")
    ss(page, "login_result")
    return False

def enter_otp(page, otp_code):
    """Enter the OTP code on the verification page."""
    print(f"\n[OTP] Entering code: {otp_code}")

    # Check if we're on the OTP page
    page_text = page.evaluate("document.body.innerText")
    if "Verify Security Code" not in page_text and "OTP" not in page_text:
        # Maybe need to navigate to it or login first
        print("  Not on OTP page. Checking state...")
        if "login" in page.url.lower():
            # Need to login first to get to OTP page
            result = do_login(page)
            if result != "OTP_NEEDED":
                return result
        else:
            print("  May already be past OTP")
            return True

    # Find OTP input fields (6 digit boxes)
    otp_inputs = page.locator('input[maxlength="1"], input[type="tel"], input.otp-input').all()
    if otp_inputs and len(otp_inputs) >= 6:
        print(f"  Found {len(otp_inputs)} OTP input boxes")
        for i, digit in enumerate(otp_code[:6]):
            if i < len(otp_inputs):
                otp_inputs[i].click()
                page.wait_for_timeout(100)
                otp_inputs[i].fill(digit)
                page.wait_for_timeout(200)
        print("  Filled all 6 digits")
    else:
        # Try a single OTP input
        otp_input = page.locator('input[name*="otp" i], input[name*="code" i], input[placeholder*="code" i]').first
        try:
            if otp_input.is_visible(timeout=3000):
                otp_input.fill(otp_code)
                print("  Filled single OTP input")
        except:
            # Type the code digit by digit into focused input
            first_input = page.locator('input:visible').first
            first_input.click()
            page.wait_for_timeout(300)
            for digit in otp_code:
                page.keyboard.type(digit, delay=100)
                page.wait_for_timeout(200)
            print("  Typed OTP via keyboard")

    page.wait_for_timeout(1000)
    ss(page, "otp_entered")

    # Click Verify OTP button
    verify_btn = page.locator('button:has-text("Verify OTP"), button:has-text("Verify"), button[type="submit"]').first
    try:
        if verify_btn.is_visible(timeout=3000):
            verify_btn.click()
            print("  Clicked Verify OTP")
    except:
        page.keyboard.press("Enter")
        print("  Pressed Enter")

    print("  Waiting for verification...")
    page.wait_for_timeout(10000)
    ss(page, "otp_verified")
    print(f"  URL: {page.url}")

    if "login" not in page.url.lower():
        print("  OTP verified — logged in!")
        return True
    else:
        print("  Still on login page after OTP")
        return False

def create_app(page):
    """Create the OAuth Private App after login."""
    print("\n" + "=" * 70)
    print("CREATING APP: Genesis CRM Bridge")
    print("=" * 70)

    # ===== MY APPS =====
    print("\n[1] Navigating to My Apps...")
    page.goto(MARKETPLACE_URL, wait_until="domcontentloaded", timeout=30000)
    page.wait_for_timeout(3000)
    ss(page, "marketplace_home")

    # Look for My Apps in nav
    my_apps = page.locator('a:has-text("My Apps")').first
    try:
        if my_apps.is_visible(timeout=5000):
            my_apps.click()
            page.wait_for_timeout(5000)
            ss(page, "my_apps_clicked")
        else:
            page.goto(f"{MARKETPLACE_URL}/apps", wait_until="domcontentloaded", timeout=30000)
            page.wait_for_timeout(5000)
            ss(page, "my_apps_direct")
    except:
        page.goto(f"{MARKETPLACE_URL}/apps", wait_until="domcontentloaded", timeout=30000)
        page.wait_for_timeout(5000)
        ss(page, "my_apps_fallback")

    print(f"  URL: {page.url}")

    # ===== CREATE APP =====
    print("\n[2] Looking for Create App...")

    # Dump page text to understand the UI
    page_text = page.evaluate("document.body.innerText")
    print(f"  Page text (300 chars): {page_text[:300]}")

    found_create = False
    for txt in ["Create App", "Create New App", "New App", "Create"]:
        for tag in ["button", "a"]:
            try:
                elem = page.locator(f'{tag}:has-text("{txt}")').first
                if elem.is_visible(timeout=2000):
                    elem.click()
                    print(f"  Clicked '{txt}' ({tag})")
                    page.wait_for_timeout(5000)
                    found_create = True
                    break
            except:
                continue
        if found_create:
            break

    if not found_create:
        print("  WARNING: No Create App button found")
        # Take detailed screenshot and list all buttons
        all_btns = page.locator('button:visible, a:visible').all()
        for i, btn in enumerate(all_btns[:20]):
            try:
                txt = btn.text_content().strip()[:50]
                href = btn.get_attribute("href") or ""
                print(f"    [{i}] {txt} (href={href})")
            except:
                pass

    ss(page, "create_form")
    print(f"  URL: {page.url}")

    # ===== FILL APP DETAILS =====
    print("\n[3] Filling app details...")

    # Dump the form to understand structure
    form_text = page.evaluate("document.body.innerText")
    print(f"  Form text (500 chars): {form_text[:500]}")

    # List all input fields
    all_inputs = page.locator('input:visible, select:visible, textarea:visible').all()
    print(f"  Found {len(all_inputs)} visible form elements:")
    for i, inp in enumerate(all_inputs):
        try:
            tag = inp.evaluate("el => el.tagName")
            t = inp.get_attribute("type") or "?"
            n = inp.get_attribute("name") or "?"
            ph = inp.get_attribute("placeholder") or "?"
            v = inp.input_value()[:30] if tag == "INPUT" else ""
            print(f"    [{i}] <{tag}> type={t} name={n} placeholder={ph} value={v}")
        except Exception as e:
            print(f"    [{i}] (error: {e})")

    # Fill App Name - try first text input
    name_filled = False
    for sel in ['input[placeholder*="name" i]', 'input[placeholder*="Name"]', 'input[name="name"]', 'input[type="text"]']:
        try:
            inp = page.locator(sel).first
            if inp.is_visible(timeout=2000):
                inp.fill(APP_NAME)
                print(f"  Filled name: {APP_NAME} (via {sel})")
                name_filled = True
                break
        except:
            continue

    if not name_filled and all_inputs:
        # Use first input
        all_inputs[0].fill(APP_NAME)
        print(f"  Filled name via first input")

    page.wait_for_timeout(1000)

    # Select "Private" (radio/button/div)
    print("  Selecting Private...")
    for sel in [
        'label:has-text("Private")',
        'div[role="radio"]:has-text("Private")',
        'input[value="private" i]',
        'button:has-text("Private")',
        'span:has-text("Private")',
    ]:
        try:
            elem = page.locator(sel).first
            if elem.is_visible(timeout=1000):
                elem.click()
                print(f"    Selected via: {sel}")
                break
        except:
            continue

    page.wait_for_timeout(500)

    # Select "Agency"
    print("  Selecting Agency...")
    for sel in [
        'label:has-text("Agency")',
        'div[role="radio"]:has-text("Agency")',
        'input[value="agency" i]',
        'button:has-text("Agency")',
        'span:has-text("Agency")',
    ]:
        try:
            elem = page.locator(sel).first
            if elem.is_visible(timeout=1000):
                elem.click()
                print(f"    Selected via: {sel}")
                break
        except:
            continue

    page.wait_for_timeout(500)
    ss(page, "details_filled")

    # ===== REDIRECT URI =====
    print("\n[4] Setting redirect URI...")
    for sel in [
        'input[placeholder*="redirect" i]',
        'input[placeholder*="URI" i]',
        'input[placeholder*="url" i]',
        'input[placeholder*="callback" i]',
        'input[type="url"]',
        'input[name*="redirect" i]',
    ]:
        try:
            inp = page.locator(sel).first
            if inp.is_visible(timeout=1000):
                inp.fill(REDIRECT_URI)
                print(f"  Set: {REDIRECT_URI}")
                break
        except:
            continue

    # Also look for "Add" button near redirect URI
    for txt in ["Add", "Add URI", "Add Redirect"]:
        try:
            btn = page.locator(f'button:has-text("{txt}")').first
            if btn.is_visible(timeout=1000):
                btn.click()
                page.wait_for_timeout(1000)
                print(f"  Clicked '{txt}'")
                break
        except:
            continue

    ss(page, "redirect_uri")

    # ===== SELECT SCOPES =====
    print("\n[5] Selecting scopes...")

    # Look for scopes section
    for tab in ["Scopes", "Permissions", "Access", "OAuth Scopes"]:
        try:
            t = page.locator(f'button:has-text("{tab}"), a:has-text("{tab}"), [role="tab"]:has-text("{tab}")').first
            if t.is_visible(timeout=2000):
                t.click()
                print(f"  Clicked tab: {tab}")
                page.wait_for_timeout(3000)
                break
        except:
            continue

    ss(page, "scopes_section")

    # List all checkboxes / toggles for diagnostics
    checkboxes = page.locator('input[type="checkbox"]').all()
    print(f"  Found {len(checkboxes)} checkboxes")

    # Try to find and select each scope
    selected = 0
    missing = []

    for scope in REQUIRED_SCOPES:
        found = False

        # Strategy 1: checkbox with matching value
        try:
            cb = page.locator(f'input[value="{scope}"]').first
            if cb.is_visible(timeout=300):
                if not cb.is_checked():
                    cb.check()
                found = True
                selected += 1
        except:
            pass

        if not found:
            # Strategy 2: label containing scope text
            try:
                label = page.locator(f'label:has-text("{scope}")').first
                if label.is_visible(timeout=300):
                    label.click()
                    found = True
                    selected += 1
            except:
                pass

        if not found:
            # Strategy 3: any element with exact text
            try:
                elem = page.locator(f'text="{scope}"').first
                if elem.is_visible(timeout=300):
                    elem.click()
                    found = True
                    selected += 1
            except:
                pass

        if not found:
            # Strategy 4: Look for the scope name with . replaced by period in the text
            # Some UIs show "contacts readonly" or "Contacts Read" instead
            scope_parts = scope.replace("/", " ").replace(".", " ").split()
            search_text = " ".join(scope_parts)
            try:
                elem = page.locator(f'text=/{re.escape(search_text)}/i').first
                if elem.is_visible(timeout=300):
                    elem.click()
                    found = True
                    selected += 1
            except:
                pass

        if not found:
            missing.append(scope)

        page.wait_for_timeout(50)

    print(f"  Selected: {selected}/{len(REQUIRED_SCOPES)}")
    if missing:
        print(f"  Missing ({len(missing)}): {missing}")

    # Scroll through page to check for more scopes
    page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
    page.wait_for_timeout(1000)
    ss(page, "scopes_selected")

    # ===== SAVE APP =====
    print("\n[6] Saving app...")
    for txt in ["Save", "Create", "Submit", "Create App"]:
        try:
            btn = page.locator(f'button:has-text("{txt}")').first
            if btn.is_visible(timeout=2000):
                btn.click()
                print(f"  Clicked '{txt}'")
                page.wait_for_timeout(10000)
                break
        except:
            continue

    ss(page, "after_save")
    print(f"  URL: {page.url}")

    # ===== CAPTURE CREDENTIALS =====
    print("\n[7] Capturing credentials...")

    page_text = page.evaluate("document.body.innerText")
    print(f"  Page text after save (1000 chars): {page_text[:1000]}")

    client_id = None
    client_secret = None

    # Try regex on page text
    id_match = re.search(r'Client\s*ID[:\s]*([a-f0-9-]{20,})', page_text, re.I)
    if id_match:
        client_id = id_match.group(1).strip()

    secret_match = re.search(r'Client\s*Secret[:\s]*([a-zA-Z0-9_-]{20,})', page_text, re.I)
    if secret_match:
        client_secret = secret_match.group(1).strip()

    # Try input fields with copy buttons
    if not client_id:
        for sel in ['input[name*="clientId" i]', 'input[name*="client_id" i]', 'input[readonly]']:
            try:
                inputs = page.locator(sel).all()
                for inp in inputs:
                    if inp.is_visible():
                        val = inp.input_value()
                        if val and len(val) > 15:
                            if not client_id:
                                client_id = val
                            elif not client_secret:
                                client_secret = val
            except:
                pass

    # Try looking for copy-to-clipboard text spans
    if not client_id or not client_secret:
        # Look for all code/monospace elements
        code_elems = page.locator('code, .copy-text, [class*="secret"], [class*="client"]').all()
        for elem in code_elems:
            try:
                if elem.is_visible():
                    val = elem.text_content().strip()
                    if val and len(val) > 15:
                        if not client_id:
                            client_id = val
                        elif not client_secret:
                            client_secret = val
            except:
                pass

    # Also check clipboard
    try:
        # Look for copy buttons and click them
        copy_btns = page.locator('button:has-text("Copy"), [class*="copy"]').all()
        for btn in copy_btns:
            if btn.is_visible():
                btn.click()
                page.wait_for_timeout(500)
                try:
                    clip = page.evaluate("navigator.clipboard.readText()")
                    if clip and len(clip) > 15:
                        if not client_id:
                            client_id = clip
                        elif not client_secret:
                            client_secret = clip
                except:
                    pass
    except:
        pass

    ss(page, "credentials_captured")

    # Save results
    creds = {
        "app_name": APP_NAME,
        "client_id": client_id,
        "client_secret": client_secret,
        "redirect_uri": REDIRECT_URI,
        "scopes_requested": REQUIRED_SCOPES,
        "scopes_selected": selected,
        "scopes_missing": missing,
        "created_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
        "page_url": page.url,
        "page_text": page_text[:5000],
    }
    with open(CREDENTIALS_FILE, "w") as f:
        json.dump(creds, f, indent=2)

    print(f"\n{'='*70}")
    print("CREDENTIALS CAPTURED")
    print(f"{'='*70}")
    print(f"  Client ID:     {client_id or 'NOT FOUND - check screenshots'}")
    print(f"  Client Secret: {client_secret or 'NOT FOUND - check screenshots'}")
    print(f"  Scopes:        {selected}/{len(REQUIRED_SCOPES)}")
    print(f"  Saved to:      {CREDENTIALS_FILE}")
    print(f"  Screenshots:   {SCREENSHOT_DIR}")

    return client_id, client_secret


def main():
    parser = argparse.ArgumentParser(description="GHL OAuth App Creator")
    parser.add_argument("--login", action="store_true", help="Trigger login (sends OTP)")
    parser.add_argument("--otp", type=str, help="Enter OTP code (6 digits)")
    parser.add_argument("--create", action="store_true", help="Create the app (after login)")
    parser.add_argument("--headless", action="store_true", help="Run headless (default: headed)")
    args = parser.parse_args()

    if not args.login and not args.otp and not args.create:
        parser.print_help()
        print("\nExamples:")
        print("  python3 create_ghl_app_final.py --login          # Trigger login + OTP")
        print("  python3 create_ghl_app_final.py --otp 123456     # Enter OTP")
        print("  python3 create_ghl_app_final.py --create         # Create app (if already logged in)")
        print("  python3 create_ghl_app_final.py --otp 123456 --create  # OTP + create in one shot")
        return

    SCREENSHOT_DIR.mkdir(parents=True, exist_ok=True)
    Path(BROWSER_PROFILE).mkdir(parents=True, exist_ok=True)

    # Ensure display
    if not os.environ.get("DISPLAY"):
        os.environ["DISPLAY"] = ":0"

    print(f"Display: {os.environ.get('DISPLAY')}")
    print(f"Headless: {args.headless}")

    with sync_playwright() as p:
        context = p.chromium.launch_persistent_context(
            user_data_dir=BROWSER_PROFILE,
            headless=args.headless,
            args=[
                "--no-sandbox",
                "--disable-blink-features=AutomationControlled",
                "--disable-features=IsolateOrigins,site-per-process",
                "--disable-dev-shm-usage",
            ],
            viewport={"width": 1920, "height": 1080},
            user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.108 Safari/537.36",
            ignore_https_errors=True,
            locale="en-US",
            timezone_id="Australia/Brisbane",
        )

        page = context.pages[0] if context.pages else context.new_page()
        page.set_default_timeout(30000)

        stealth = Stealth()
        stealth.apply_stealth_sync(page)

        try:
            if args.login:
                result = do_login(page)
                if result == "OTP_NEEDED":
                    print("\n*** OTP REQUIRED ***")
                    print("Check kinan@agileadapt.com email for the 6-digit code.")
                    print(f"Then run: python3 create_ghl_app_final.py --otp <CODE> --create")
                elif result:
                    print("Logged in successfully!")
                    if args.create:
                        create_app(page)
                else:
                    print("Login failed!")

            elif args.otp:
                # Navigate to login page first to trigger OTP flow
                page.goto(LOGIN_URL, wait_until="domcontentloaded", timeout=60000)
                page.wait_for_timeout(3000)

                # Check if we're on OTP page or need to login
                page_text = page.evaluate("document.body.innerText")
                if "Verify Security Code" in page_text:
                    print("Already on OTP page")
                elif "login" in page.url.lower():
                    print("Need to login first...")
                    result = do_login(page)
                    if result != "OTP_NEEDED":
                        if result:
                            print("Logged in without OTP!")
                            if args.create:
                                create_app(page)
                            context.close()
                            return
                        else:
                            print("Login failed")
                            context.close()
                            return

                result = enter_otp(page, args.otp)
                if result:
                    print("OTP verified!")
                    if args.create:
                        create_app(page)
                else:
                    print("OTP verification failed")
                    ss(page, "otp_failed")

            elif args.create:
                # Check if already logged in
                if is_logged_in(page):
                    print("Already logged in!")
                    create_app(page)
                else:
                    print("Not logged in. Run --login first, then --otp <code>")

        except Exception as e:
            print(f"\nFATAL: {e}")
            import traceback
            traceback.print_exc()
            ss(page, "FATAL")
        finally:
            context.close()


if __name__ == "__main__":
    main()
