from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
from playwright.sync_api import sync_playwright
from urllib.parse import urlencode
from openpyxl import load_workbook
import re
import time

BASE_SEARCH_URL = "https://www.laurageller.com/search"
EXCEL_FILE = "uploaded_files/lg_prod_list2.xlsx"
HEADLESS = True


def normalize_text(value: str) -> str:
    return re.sub(r"\s+", " ", (value or "").strip()).casefold()


def is_human_verification_page(page) -> bool:
    checks = [
        "your connection needs to be verified before you can proceed",
        "verify you are human",
        "cloudflare",
        "checking your browser",
    ]
    try:
        body_text = normalize_text(page.locator("body").inner_text(timeout=1200))
    except Exception:
        body_text = ""
    if any(token in body_text for token in checks):
        return True

    try:
        frame_match = page.evaluate(
            """() => {
                return Array.from(document.querySelectorAll("iframe")).some((f) => {
                    const t = (f.getAttribute("title") || "").toLowerCase();
                    const s = (f.getAttribute("src") || "").toLowerCase();
                    return t.includes("challenge") || t.includes("captcha") || s.includes("challenges.cloudflare.com");
                });
            }"""
        )
        return bool(frame_match)
    except Exception:
        return False


def wait_for_human_verification(page, where: str, timeout_sec: int = 180) -> None:
    if not is_human_verification_page(page):
        return

    if HEADLESS:
        raise RuntimeError(
            f"Cloudflare verification detected at {where}. Run with HEADLESS=False and verify manually."
        )

    print(f'Cloudflare verification detected at {where}. Complete it manually in browser window...')
    end = time.time() + timeout_sec
    while time.time() < end:
        time.sleep(1.0)
        if not is_human_verification_page(page):
            print("Verification completed. Continuing...")
            return
    raise RuntimeError(f"Verification not completed within {timeout_sec}s at {where}.")


def close_newsletter_modal(page) -> bool:
    try:
        closed = page.evaluate(
            """() => {
                const norm = (s) => (s || "").replace(/\\s+/g, " ").trim().toLowerCase();
                const isVisible = (el) => !!el && !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
                const modalCandidates = Array.from(document.querySelectorAll("div, section, aside"));

                const isNewsletterModal = (el) => {
                    const txt = norm(el.textContent || "");
                    return txt.includes("sign up to unlock") && txt.includes("15% off");
                };

                const modal = modalCandidates.find((el) => isVisible(el) && isNewsletterModal(el));
                if (!modal) return false;

                const controls = Array.from(modal.querySelectorAll("a,button,[role='button'],input[type='button'],input[type='submit']"));
                for (const c of controls) {
                    const label = norm(c.textContent || c.value || c.getAttribute("aria-label") || "");
                    if (label.includes("no,thanks") || label.includes("no thanks")) {
                        c.click();
                        return true;
                    }
                    if (label === "x" || label === "×" || label.includes("close")) {
                        c.click();
                        return true;
                    }
                }

                // No clear close control found: remove modal and immediate fixed overlays.
                modal.remove();
                const overlays = Array.from(document.querySelectorAll("div, section, aside")).filter((el) => {
                    if (!isVisible(el)) return false;
                    const st = window.getComputedStyle(el);
                    if (st.position !== "fixed") return false;
                    const z = Number(st.zIndex || 0);
                    const r = el.getBoundingClientRect();
                    return z >= 20 && r.width > window.innerWidth * 0.6 && r.height > window.innerHeight * 0.6;
                });
                overlays.forEach((el) => {
                    const txt = norm(el.textContent || "");
                    if (!txt || txt.includes("sign up to unlock") || txt.includes("15% off")) {
                        el.remove();
                    }
                });
                return true;
            }"""
        )
        return bool(closed)
    except Exception:
        return False


def close_popups_in_iframes(page) -> bool:
    closed_any = False
    frame_selectors = [
        'text=/NO,?\\s*THANKS\\.?/i',
        'a:has-text("No Thanks")',
        'a:has-text("NO THANKS")',
        'button:has-text("No Thanks")',
        'button:has-text("NO THANKS")',
        '[aria-label="Close"]',
        'button[aria-label="Close"]',
        'button:has-text("Close")',
        'text="×"',
        'text="✕"',
        '.needsclick.klaviyo-close-form',
    ]

    for frame in page.frames:
        for selector in frame_selectors:
            try:
                items = frame.locator(selector)
                total = min(items.count(), 3)
                for idx in range(total):
                    item = items.nth(idx)
                    if item.is_visible(timeout=300):
                        item.click(timeout=1500, force=True)
                        closed_any = True
                        time.sleep(0.1)
            except Exception:
                pass

        try:
            clicked = frame.evaluate(
                """() => {
                    const norm = (s) => (s || "").replace(/\\s+/g, " ").trim().toLowerCase();
                    const isVisible = (el) => !!el && !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
                    const controls = Array.from(document.querySelectorAll("a,button,[role='button'],input[type='button'],input[type='submit']"));
                    for (const el of controls) {
                        if (!isVisible(el)) continue;
                        const label = norm(el.textContent || el.value || el.getAttribute("aria-label") || "");
                        if (label.includes("no thanks") || label.includes("no,thanks") || label === "x" || label === "×" || label.includes("close")) {
                            el.click();
                            return true;
                        }
                    }
                    return false;
                }"""
            )
            if clicked:
                closed_any = True
        except Exception:
            pass

    return closed_any


def close_common_popups(
    page, rounds: int = 3, press_escape: bool = False, preserve_quickview: bool = True
) -> None:
    safe_selectors = [
        'text=/NO,?\\s*THANKS\\.?/i',
        'a:has-text("NO, THANKS")',
        'a:has-text("NO, THANKS.")',
        'a:has-text("NO,THANKS")',
        'a:has-text("NO,THANKS.")',
        'a:has-text("NO THANKS")',
        'a:has-text("No, thanks")',
        'a:has-text("No, thanks.")',
        'a:has-text("No Thanks")',
        'a:has-text("No Thanks.")',
        'button:has-text("NO, THANKS")',
        'button:has-text("NO, THANKS.")',
        'button:has-text("NO THANKS")',
        'button:has-text("NO THANKS.")',
        'button:has-text("No Thanks")',
        'button:has-text("No Thanks.")',
        'button:has-text("No, thanks")',
        'button:has-text("No, thanks.")',
        '.needsclick.klaviyo-close-form',
        '[aria-label="Close dialog"]',
        'button:has-text("Accept")',
        'button:has-text("Accept All")',
        'button:has-text("Allow All")',
        '#onetrust-accept-btn-handler',
        '.onetrust-close-btn-handler',
        '[id*="onetrust"] button[aria-label="Close"]',
    ]
    generic_close_selectors = [
        '[aria-label="Close"]',
        'button[aria-label="Close"]',
        'button:has-text("Close")',
        'button:has-text("✕")',
        'text="×"',
    ]

    selectors = list(safe_selectors)
    if not preserve_quickview:
        selectors.extend(generic_close_selectors)
    else:
        try:
            quickview_open = page.locator(
                'text="Choose Your Shade", a:has-text("View Product Details")'
            ).first.is_visible(timeout=300)
        except Exception:
            quickview_open = False
        if not quickview_open:
            selectors.extend(generic_close_selectors)
    for _ in range(rounds):
        clicked_any = False

        if close_newsletter_modal(page):
            clicked_any = True
            time.sleep(0.15)
        if close_popups_in_iframes(page):
            clicked_any = True
            time.sleep(0.15)

        for selector in selectors:
            try:
                elements = page.locator(selector)
                total = min(elements.count(), 3)
                for idx in range(total):
                    button = elements.nth(idx)
                    if button.is_visible(timeout=600):
                        button.click(timeout=2500, force=True)
                        clicked_any = True
                        time.sleep(0.15)
            except Exception:
                pass

        if press_escape:
            try:
                page.keyboard.press("Escape")
            except Exception:
                pass

        if not clicked_any:
            break


def guarded_click(page, locator, step_name: str, attempts: int = 4) -> None:
    last_error = None
    for _ in range(attempts):
        wait_for_human_verification(page, f"before {step_name}")
        close_common_popups(page, rounds=2)
        try:
            locator.wait_for(state="visible", timeout=6000)
            locator.scroll_into_view_if_needed()
            locator.click(timeout=12000)
            return
        except Exception as exc:
            last_error = exc
            time.sleep(0.5)
    raise RuntimeError(f'Failed to click "{step_name}" after retries: {last_error}')


def find_visible_locator(page, selectors, timeout_ms: int = 1200):
    for selector in selectors:
        try:
            items = page.locator(selector)
            total = min(items.count(), 8)
            for idx in range(total):
                item = items.nth(idx)
                if item.is_visible(timeout=timeout_ms):
                    return item
        except Exception:
            continue
    return None


def wait_page_ready(page, timeout_ms: int = 100000) -> None:
    # Some storefront scripts keep background requests open, so avoid hard-failing on networkidle.
    page.wait_for_load_state("domcontentloaded", timeout=timeout_ms)
    try:
        page.wait_for_load_state("load", timeout=15000)
    except PlaywrightTimeoutError:
        pass
    try:
        page.wait_for_load_state("networkidle", timeout=10000)
    except PlaywrightTimeoutError:
        pass


def find_exact_product_title_locator(page, exact_title: str):
    target = normalize_text(exact_title)

    title_candidates = page.locator("h1, h2, h3, h4, p, a, span")
    count = title_candidates.count()

    for i in range(count):
        node = title_candidates.nth(i)
        try:
            text = normalize_text(node.inner_text(timeout=1000))
            if text != target:
                continue

            return node
        except Exception:
            continue

    return None


def click_shop_now_for_exact_title(page, exact_title: str) -> None:
    clicked = page.evaluate(
        """(targetTitle) => {
            const norm = (s) => (s || "").replace(/\\s+/g, " ").trim().toLowerCase();
            const target = norm(targetTitle);
            const isVisible = (el) => !!el && !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length);

            const cardSelectors = [
                "article",
                "li",
                ".grid__item",
                ".card-wrapper",
                ".card",
                ".product-item",
                ".product-card"
            ];

            const cards = Array.from(document.querySelectorAll(cardSelectors.join(",")));
            for (const card of cards) {
                if (!isVisible(card)) continue;

                const titleEls = card.querySelectorAll("h1,h2,h3,h4,p,a,span");
                let hasExactTitle = false;
                for (const t of titleEls) {
                    if (norm(t.textContent) === target) {
                        hasExactTitle = true;
                        break;
                    }
                }
                if (!hasExactTitle) continue;

                const shopNow = card.querySelector(
                    'a,button,input[type="button"],input[type="submit"]'
                );
                const actions = Array.from(
                    card.querySelectorAll('a,button,input[type="button"],input[type="submit"]')
                );
                for (const action of actions) {
                    const label = norm(action.textContent || action.value || "");
                    if (label === "shop now" && isVisible(action)) {
                        action.scrollIntoView({block: "center"});
                        action.click();
                        return true;
                    }
                }
            }
            return false;
        }""",
        exact_title,
    )

    if clicked:
        return

    # Fallback with Playwright locators if DOM click path fails.
    title_node = find_exact_product_title_locator(page, exact_title)
    if not title_node:
        raise RuntimeError(f'Could not find product title: "{exact_title}"')

    shop_now = page.locator('a:has-text("SHOP NOW"), button:has-text("SHOP NOW")').filter(
        has=page.locator(f'text="{exact_title}"')
    ).first
    if shop_now.count() == 0:
        raise RuntimeError('Found title but no "SHOP NOW" in that product card.')

    guarded_click(page, shop_now, "SHOP NOW")


def click_add_to_bag(page) -> None:
    selectors = [
        'button:has-text("ADD TO BAG")',
        'button:has-text("Add to Bag")',
        'button:has-text("ADD TO CART")',
        'button:has-text("Add to Cart")',
        'button[name="add"]',
        'form[action*="/cart/add"] button[type="submit"]',
        '[data-add-to-cart]',
        '[data-atc]',
    ]

    deadline = time.time() + 30
    while time.time() < deadline:
        close_common_popups(page, rounds=2, press_escape=False)

        button = find_visible_locator(page, selectors, timeout_ms=1200)
        if button:
            try:
                if button.is_enabled(timeout=2000):
                    guarded_click(page, button, "ADD TO BAG")
                    return
            except Exception:
                pass

        # Fallback: click any visible action whose label contains add-to-bag/cart.
        clicked = page.evaluate(
            """() => {
                const norm = (s) => (s || "").replace(/\\s+/g, " ").trim().toLowerCase();
                const isVisible = (el) => !!el && !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
                const controls = Array.from(
                    document.querySelectorAll('button, input[type="button"], input[type="submit"], a')
                );
                for (const el of controls) {
                    const label = norm(el.textContent || el.value || el.getAttribute("aria-label") || "");
                    if (!isVisible(el)) continue;
                    if (el.disabled || el.getAttribute("aria-disabled") === "true") continue;
                    if (label.includes("add to bag") || label.includes("add to cart")) {
                        el.scrollIntoView({block: "center"});
                        el.click();
                        return true;
                    }
                }
                return false;
            }"""
        )
        if clicked:
            return

        time.sleep(0.4)

    raise RuntimeError('Could not find clickable "ADD TO BAG" button.')


def wait_for_product_context(page, timeout_ms: int = 30000) -> None:
    start = time.time()
    while (time.time() - start) * 1000 < timeout_ms:
        close_common_popups(page, rounds=2, press_escape=False)
        current = page.url
        if "/products/" in current:
            return
        add_btn = find_visible_locator(
            page,
            [
                'button:has-text("ADD TO BAG")',
                'button:has-text("Add to Bag")',
                'button[name="add"]',
                'form[action*="/cart/add"] button[type="submit"]',
                '[data-add-to-cart]',
                '[data-atc]',
            ],
            timeout_ms=600,
        )
        if add_btn:
            return
        quickview_marker = find_visible_locator(
            page,
            [
                'text="Choose Your Shade"',
                'a:has-text("View Product Details")',
                'text="View Product Details"',
            ],
            timeout_ms=600,
        )
        if quickview_marker:
            return
        time.sleep(0.4)
    raise RuntimeError("SHOP NOW did not open product page or quick-view with ADD TO BAG.")


def click_checkout(page) -> None:
    selectors = [
        'a:has-text("CHECKOUT")',
        'button:has-text("CHECKOUT")',
        'a:has-text("Checkout")',
        'button:has-text("Checkout")',
        'a[href*="/checkout"]',
        'form[action*="/checkout"] button[type="submit"]',
    ]

    for selector in selectors:
        btn = page.locator(selector).first
        try:
            if btn.is_visible(timeout=5000):
                guarded_click(page, btn, "CHECKOUT")
                return
        except Exception:
            continue

    # Fallback: open cart drawer/page first, then click checkout.
    cart_openers = [
        '[href*="/cart"]',
        'a[aria-label*="Bag"]',
        'button[aria-label*="Bag"]',
        'a[aria-label*="Cart"]',
        'button[aria-label*="Cart"]',
        'button:has-text("View Cart")',
        'a:has-text("View Cart")',
    ]

    deadline = time.time() + 40
    cart_open_attempted = False
    while time.time() < deadline:
        close_common_popups(page, rounds=3, preserve_quickview=True)

        btn = find_visible_locator(page, selectors, timeout_ms=1000)
        if btn:
            try:
                guarded_click(page, btn, "CHECKOUT")
                return
            except Exception:
                pass

        if not cart_open_attempted:
            opener = find_visible_locator(page, cart_openers, timeout_ms=800)
            if opener:
                try:
                    guarded_click(page, opener, "OPEN CART")
                    close_common_popups(page, rounds=3, preserve_quickview=True)
                    cart_open_attempted = True
                except Exception:
                    pass

        clicked = page.evaluate(
            """() => {
                const norm = (s) => (s || "").replace(/\\s+/g, " ").trim().toLowerCase();
                const isVisible = (el) => !!el && !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
                const controls = Array.from(document.querySelectorAll("a,button,input[type='button'],input[type='submit']"));
                for (const el of controls) {
                    if (!isVisible(el)) continue;
                    const label = norm(el.textContent || el.value || el.getAttribute("aria-label") || "");
                    if (label.includes("checkout")) {
                        el.scrollIntoView({ block: "center" });
                        el.click();
                        return true;
                    }
                }
                return false;
            }"""
        )
        if clicked:
            return

        time.sleep(0.4)

    raise RuntimeError('Could not find clickable "CHECKOUT" button/link.')


def run_one_product(page, product_title: str) -> str:
    params = urlencode({"q": product_title, "options[prefix]": "last"})
    search_url = f"{BASE_SEARCH_URL}?{params}"
    print(f"Opening search URL: {search_url}")
    page.goto(search_url, wait_until="domcontentloaded", timeout=100000)
    wait_for_human_verification(page, "after search open")
    wait_page_ready(page)
    wait_for_human_verification(page, "after page ready")
    close_common_popups(page, rounds=4)

    print(f'Clicking SHOP NOW for exact title: "{product_title}"')
    click_shop_now_for_exact_title(page, product_title)
    wait_for_human_verification(page, "after SHOP NOW")

    wait_for_product_context(page, timeout_ms=30000)
    wait_for_human_verification(page, "after product context")
    if "/products/" in page.url:
        wait_page_ready(page, timeout_ms=30000)
    close_common_popups(page, rounds=4)

    print("Clicking ADD TO BAG")
    click_add_to_bag(page)
    wait_for_human_verification(page, "after ADD TO BAG")
    close_common_popups(page, rounds=4)
    return "ADDED_TO_BAG"


def main() -> None:
    wb = load_workbook(EXCEL_FILE)
    ws = wb.active

    header_map = {}
    for cell in ws[1]:
        if cell.value:
            header_map[str(cell.value).strip()] = cell.column

    product_col = header_map.get("Product_Title")
    url_col = header_map.get("URL")
    if not product_col or not url_col:
        raise RuntimeError('Excel must contain "Product_Title" and "URL" columns.')

    with sync_playwright() as p:
        browser = p.chromium.launch(headless=HEADLESS, slow_mo=150)
        context = browser.new_context(
            viewport={"width": 1440, "height": 900},
            locale="en-US",
            user_agent=(
                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                "AppleWebKit/537.36 (KHTML, like Gecko) "
                "Chrome/132.0.0.0 Safari/537.36"
            ),
        )

        try:
            for row in range(2, ws.max_row + 1):
                product_title = ws.cell(row=row, column=product_col).value
                if not product_title:
                    continue
                product_title = str(product_title).strip()
                if not product_title:
                    continue

                page = context.new_page()
                try:
                    print(f"\nProcessing row {row}: {product_title}")
                    result = run_one_product(page, product_title)
                    ws.cell(row=row, column=url_col).value = result
                except Exception as exc:
                    ws.cell(row=row, column=url_col).value = f"ERROR: {exc}"
                    print(f"Row {row} failed: {exc}")
                finally:
                    page.close()

            wb.save(EXCEL_FILE)
            print(f"Updated {EXCEL_FILE} with add-to-bag status.")
        finally:
            context.close()
            browser.close()
            wb.close()


if __name__ == "__main__":
    main()
