"""JavaScript interaction discovery: click buttons, cards, open modals, capture content."""

from __future__ import annotations

import hashlib
import logging
import time
from pathlib import Path
from urllib.parse import urljoin, urlparse

from bs4 import BeautifulSoup

log = logging.getLogger("crawler.interactions")

# ---------------------------------------------------------------------------
# Safety selectors — skip these by default
# ---------------------------------------------------------------------------
_SAFE_SKIP_TEXTS = {
    "login", "log in", "logout", "sign in", "sign out", "register",
    "account", "cart", "checkout", "basket", "delete", "remove",
    "subscribe", "newsletter", "share", "print", "cookie", "language",
    "english", "deutsch", "francais", "italiano", "espanol",
}

_EXCLUDE_SELECTORS = [
    ".cookie", ".newsletter", ".login", ".logout", ".cart", ".checkout",
    ".account", ".language", ".share", ".social", ".print",
    "[aria-label*='cookie']", "[aria-label*='login']", "[aria-label*='language']",
    "[aria-label*='share']", "[aria-label*='print']",
]

# ---------------------------------------------------------------------------
# Candidate extraction via JS
# ---------------------------------------------------------------------------
_INTERACTION_DISCOVERY_JS = r"""
(() => {
    const candidates = [];
    const selectors = [
        'a[href^="javascript:"]', 'a[href="#"]',
        '[onclick]', 'button', '[role="button"]', '[role="link"]',
        '[data-href]', '[data-url]', '[data-link]', '[data-target]',
        '.card', '.product-card', '.product-tile', '.product-item',
        '[class*="product"]', '[class*="card"]', '[class*="tile"]',
    ];
    const seen = new Set();
    for (const sel of selectors) {
        try {
            for (const el of document.querySelectorAll(sel)) {
                const rect = el.getBoundingClientRect();
                if (rect.width < 5 || rect.height < 5) continue;
                if (rect.bottom < 0 || rect.top > window.innerHeight) continue;
                const text = (el.textContent || el.innerText || '').trim().slice(0, 200);
                const href = el.getAttribute('href') || '';
                const onclick = el.getAttribute('onclick') || '';
                const onclickHash = onclick ? onclick.split('').reduce((a,b)=>{a=((a<<5)-a)+b.charCodeAt(0);return a|0},0).toString(16) : '';
                const dataHref = el.getAttribute('data-href') || el.getAttribute('data-url') || '';
                const tag = el.tagName.toLowerCase();
                const id = el.getAttribute('id') || '';
                const className = (el.className || '').toString().slice(0, 200);
                const key = `${tag}|${href}|${text}|${Math.round(rect.x)},${Math.round(rect.y)}`;
                if (seen.has(key)) continue;
                seen.add(key);
                const sigParts = [tag, text, href, onclickHash, dataHref, String(Math.round(rect.x/10)), String(Math.round(rect.y/10))];
                candidates.push({
                    tag, id, className: className.slice(0,100), text,
                    href, onclick: onclick.slice(0,200), onclick_hash: onclickHash,
                    data_href: dataHref,
                    x: Math.round(rect.x), y: Math.round(rect.y),
                    w: Math.round(rect.width), h: Math.round(rect.height),
                    cx: Math.round(rect.x + rect.width/2), cy: Math.round(rect.y + rect.height/2),
                    signature: sigParts.join('|'),
                });
            }
        } catch(e) {}
    }
    return candidates;
})()
"""

_EXCLUDE_TEXT_JS = r"""
(() => {
    const exclude = new Set(["login","log in","logout","sign in","sign out","register",
        "account","cart","checkout","basket","delete","remove","subscribe",
        "newsletter","share","print","cookie","language","english","deutsch",
        "francais","italiano","espanol","search","close","menu"]);
    for (const sel of ['.cookie','.cookie-banner','.consent','.newsletter',
        '.login','.logout','.cart','[aria-label*="cookie"]','[aria-label*="login"]',
        '[aria-label*="language"]','[aria-label*="share"]','[aria-label*="print"]',
        '.social']) {
        try { for (const el of document.querySelectorAll(sel)) { el.setAttribute('data-crawler-skip','1'); } } catch(e){}
    }
    for (const btn of document.querySelectorAll('button, a, [role="button"], [role="link"]')) {
        const text = (btn.textContent || '').toLowerCase().trim();
        for (const word of exclude) {
            if (text.includes(word)) { btn.setAttribute('data-crawler-skip','1'); break; }
        }
    }
})()
"""


async def discover_interactive_targets(tab, page_url: str) -> list[dict]:
    """Return visible JS-interactive elements as candidate dicts."""
    try:
        await tab.evaluate(_EXCLUDE_TEXT_JS, return_by_value=True)
        await tab.sleep(0.3)
        candidates = await tab.evaluate(_INTERACTION_DISCOVERY_JS, return_by_value=True, await_promise=False)
        if not isinstance(candidates, list):
            return []
        results = []
        for c in candidates:
            if not isinstance(c, dict):
                continue
            text_lower = str(c.get("text", "")).lower().strip()
            skip = False
            for w in _SAFE_SKIP_TEXTS:
                if w in text_lower:
                    skip = True
                    break
            if skip:
                continue
            sig = c.get("signature", "")
            action_key = hashlib.sha256(sig.encode()).hexdigest()[:16]
            c["action_key"] = action_key
            results.append(c)
        log.info("Interactive discovery: %d candidates found (%d after safety filter)", len(candidates), len(results))
        return results
    except Exception as exc:
        log.debug("Interactive discovery failed: %s", exc)
        return []


# ---------------------------------------------------------------------------
# Click execution + outcome detection
# ---------------------------------------------------------------------------
_MODAL_CHECK_JS = r"""
(() => {
    const modals = document.querySelectorAll('[role="dialog"], .modal, .overlay, .lightbox, .popup, [aria-modal="true"]');
    for (const m of modals) {
        const style = getComputedStyle(m);
        if (style.display !== 'none' && style.visibility !== 'hidden' && m.getBoundingClientRect().width > 50) {
            return {found: true, text: (m.textContent || '').trim().slice(0, 5000)};
        }
    }
    return {found: false};
})()
"""

_DOM_DIGEST_JS = r"""
(() => {
    const main = document.querySelector('main, article, .content, #content, .main-content') || document.body;
    return (main.textContent || main.innerText || '').trim().length;
})()
"""

_CLOSE_MODAL_JS = r"""
(() => {
    try {
        const closeBtn = document.querySelector('[role="dialog"] .close, [role="dialog"] [aria-label*="close"], .modal .close, .modal [aria-label*="close"], .overlay .close, .lightbox .close, [aria-label*="Close"]');
        if (closeBtn) { closeBtn.click(); return 'closed'; }
        const escEvent = new KeyboardEvent('keydown', {key:'Escape',bubbles:true});
        document.dispatchEvent(escEvent);
        return 'escape_sent';
    } catch(e) { return 'failed: ' + e; }
})()
"""


async def _click_element(tab, candidate: dict) -> bool:
    try:
        cx = candidate.get("cx", 0)
        cy = candidate.get("cy", 0)
        if cx > 0 and cy > 0:
            await tab.mouse_click(cx, cy)
        else:
            js = f"""
            (() => {{
                const el = document.querySelector('{candidate.get("tag","*")}[id="{candidate.get("id","")}"]') ||
                           document.querySelector('{candidate.get("tag","*")}');
                if (el) {{ el.click(); return true; }}
                return false;
            }})()
            """
            await tab.evaluate(js, return_by_value=True)
        return True
    except Exception as exc:
        log.debug("Click failed for %s: %s", candidate.get("text", "")[:50], exc)
        return False


async def execute_interaction(tab, parent_url: str, candidate: dict, wait_ms: int = 800) -> dict | None:
    before_url = ""
    try:
        loc = await tab.evaluate("() => window.location.href", return_by_value=True)
        before_url = str(loc) if loc else ""
    except Exception:
        pass

    before_digest = 0
    try:
        digest = await tab.evaluate(_DOM_DIGEST_JS, return_by_value=True)
        before_digest = int(digest) if digest else 0
    except Exception:
        pass

    clicked = await _click_element(tab, candidate)
    if not clicked:
        return {"result_type": "failed", "before_url": before_url}

    await tab.sleep(max(0.5, wait_ms / 1000.0))

    # Detect outcome
    try:
        loc = await tab.evaluate("() => window.location.href", return_by_value=True)
        after_url = str(loc) if loc else ""
    except Exception:
        after_url = ""

    result = {
        "before_url": before_url,
        "after_url": after_url,
        "result_type": "no_change",
    }

    if after_url and after_url != before_url:
        clean_after = after_url.rstrip("/")
        clean_before = before_url.rstrip("/")
        if clean_after != clean_before:
            result["result_type"] = "url_navigation"
            return result

    try:
        modal = await tab.evaluate(_MODAL_CHECK_JS, return_by_value=True)
        if isinstance(modal, dict) and modal.get("found"):
            result["result_type"] = "modal"
            result["modal_text"] = modal.get("text", "")
            return result
    except Exception:
        pass

    try:
        digest = await tab.evaluate(_DOM_DIGEST_JS, return_by_value=True)
        after_digest = int(digest) if digest else 0
        if abs(after_digest - before_digest) > 200 and after_digest > 300:
            result["result_type"] = "interaction"
            result["after_digest"] = after_digest
            return result
    except Exception:
        pass

    return result


async def capture_modal_page(tab, parent_url: str, out_dir: Path, label: str, action_key: str) -> dict | None:
    try:
        html = await tab.evaluate("() => document.documentElement.outerHTML", return_by_value=True)
        if not html or len(str(html)) < 200:
            return None
    except Exception as exc:
        log.debug("Modal capture failed: %s", exc)
        return None

    safe_label = "".join(c if c.isalnum() or c in "._- " else "_" for c in label).strip().replace(" ", "-")[:60] or "modal"
    virtual_url = f"{parent_url}#modal={safe_label}&action={action_key}"

    from crawler import _url_output_stem, SITES_ROOT, write_json, write_text
    stem = _url_output_stem(parent_url) + f"__modal_{action_key[:8]}"
    page_path = out_dir / f"{stem}.html"
    text_path = page_path.with_suffix(".txt")

    html_str = str(html)
    soup = BeautifulSoup(html_str, "html.parser")
    visible_text = soup.get_text(" ", strip=True) or ""

    page_path.write_text(html_str, encoding="utf-8")
    text_path.write_text(visible_text, encoding="utf-8")

    log.info("Modal captured: %s (%d chars)", stem, len(visible_text))
    return {
        "page_type": "modal",
        "virtual_url": virtual_url,
        "label": label,
        "action_key": action_key,
        "stem": stem,
        "content_length": len(visible_text),
    }


async def restore_page_state(tab, original_url: str) -> bool:
    try:
        loc = await tab.evaluate("() => window.location.href", return_by_value=True)
        current = str(loc).rstrip("/") if loc else ""
        original = original_url.rstrip("/")
        if current != original:
            await tab.get(original_url)
            await tab.sleep(0.5)
            return True
    except Exception:
        pass

    try:
        await tab.evaluate(_CLOSE_MODAL_JS, return_by_value=True)
        await tab.sleep(0.3)
    except Exception:
        pass
    return True
