import logging

from gsearch_mcp_cli.exceptions import ExtractionError, SearchBlockedError
from gsearch_mcp_cli.models import (
    AIOverview,
    AIResult,
    OrganicResult,
    SearchResult,
    Section,
    Source,
)

logger = logging.getLogger(__name__)

BLOCKED_CHECK_JS = """
(() => {
    const text = document.body ? document.body.innerText : '';
    return text.includes('unusual traffic') || text.includes('not a robot');
})()
"""

SEARCH_EXTRACT_JS = """
(() => {
    const data = {
        ai_overview: null,
        results: [],
        people_also_ask: [],
        related_searches: [],
    };

    // AI Overview
    const aiSection = document.body.innerText;
    const aiMatch = aiSection.match(/AI Overview[\\s\\S]*?(?=Search Results|$)/);
    // Try to find AI overview content between "AI Overview" heading and first result
    const allText = document.body.innerText;
    const aiIdx = allText.indexOf('AI Overview');
    const searchIdx = allText.indexOf('Search Results');
    if (aiIdx !== -1 && searchIdx !== -1 && searchIdx > aiIdx) {
        const aiText = allText.substring(aiIdx + 'AI Overview'.length, searchIdx).trim();
        if (aiText.length > 20) {
            data.ai_overview = {text: aiText.substring(0, 2000), sources: []};
        }
    }

    // Organic results
    const h3s = document.querySelectorAll('h3');
    let position = 0;
    h3s.forEach(h3 => {
        const link = h3.closest('a');
        if (!link || !link.href || link.href.includes('google.com/search')) return;
        const url = link.href;
        position++;

        // Find snippet - look for nearby text content
        const container = h3.closest('[data-hveid]') || h3.parentElement?.parentElement;
        let snippet = '';
        if (container) {
            const spans = container.querySelectorAll('span, div');
            for (const s of spans) {
                const t = s.innerText;
                if (t.length > 40 && t !== h3.innerText && !t.includes(url)) {
                    snippet = t.substring(0, 300);
                    break;
                }
            }
        }

        const domain = new URL(url).hostname.replace('www.', '');
        data.results.push({
            position,
            title: h3.innerText,
            url,
            snippet,
            source: domain,
        });
    });

    // People also ask
    document.querySelectorAll('[data-q]').forEach(el => {
        const q = el.getAttribute('data-q');
        if (q) data.people_also_ask.push(q);
    });
    // Fallback: look for expandable buttons with question text
    if (data.people_also_ask.length === 0) {
        document.querySelectorAll('[role="button"][aria-expanded]').forEach(btn => {
            const text = btn.innerText;
            if (text.endsWith('?') && text.length > 10 && text.length < 200) {
                data.people_also_ask.push(text);
            }
        });
    }

    // Related searches
    const relatedSection = document.querySelector('[data-ssp]');
    if (relatedSection) {
        relatedSection.querySelectorAll('a').forEach(a => {
            if (a.innerText.length > 3) data.related_searches.push(a.innerText);
        });
    }

    return data;
})()
"""

NEWS_EXTRACT_JS = """
(() => {
    const data = {
        ai_overview: null,
        results: [],
        people_also_ask: [],
        related_searches: [],
    };
    const seen = new Set();
    let position = 0;

    // Strategy 1: role="heading" elements (Google News tab layout uses these instead of h3)
    document.querySelectorAll('[role="heading"]').forEach(heading => {
        const title = heading.innerText?.trim();
        if (!title || title.length < 10 || title.length > 300) return;

        let link = heading.closest('a');
        if (!link) {
            let el = heading.parentElement;
            for (let i = 0; i < 5 && el; i++) {
                if (el.tagName === 'A' && el.href) { link = el; break; }
                const a = el.querySelector('a[href]');
                if (a && a.href && !a.href.includes('google.com/search')) { link = a; break; }
                el = el.parentElement;
            }
        }

        if (!link || !link.href) return;
        const url = link.href;
        if (url.includes('google.com/search') || url.includes('accounts.google.com')) return;
        if (seen.has(url)) return;
        seen.add(url);

        position++;
        let snippet = '';
        let source = '';

        const container = heading.closest('[data-hveid]') ||
                          heading.parentElement?.parentElement?.parentElement ||
                          heading.parentElement?.parentElement;

        if (container) {
            const lines = container.innerText.split('\\n').map(l => l.trim()).filter(l => l && l !== title);
            for (const line of lines) {
                if (!source && line.length > 2 && line.length < 80 && !line.includes('http')) {
                    source = line;
                }
                if (!snippet && line.length > 40) {
                    snippet = line.substring(0, 300);
                }
            }
        }

        if (!source) {
            try { source = new URL(url).hostname.replace('www.', ''); } catch { source = ''; }
        }

        data.results.push({ position, title, url, snippet, source });
    });

    // Strategy 2: fallback — external links with substantial title text
    if (data.results.length === 0) {
        document.querySelectorAll('a[href]').forEach(a => {
            const url = a.href;
            if (!url || !url.startsWith('http')) return;
            if (url.includes('google.com') && !url.includes('google.com/url')) return;
            if (seen.has(url)) return;

            const heading = a.querySelector('[role="heading"], h3, h4');
            const title = heading ? heading.innerText?.trim() : a.innerText?.trim();
            if (!title || title.length < 15 || title.length > 300) return;
            if (title.split(' ').length < 3) return;

            seen.add(url);
            position++;

            let source = '';
            try { source = new URL(url).hostname.replace('www.', ''); } catch { source = ''; }

            data.results.push({ position, title, url, snippet: '', source });
        });
    }

    // Deduplicate and reindex
    const urlSeen = new Set();
    data.results = data.results.filter(r => {
        if (urlSeen.has(r.url)) return false;
        urlSeen.add(r.url);
        return true;
    });
    data.results.forEach((r, i) => { r.position = i + 1; });

    return data;
})()
"""

AI_MODE_EXTRACT_JS = """
(() => {
    const data = {
        synthesis: '',
        sections: [],
        sources: [],
    };

    const body = document.body.innerText;

    // Extract full AI synthesis text (everything before the source list)
    const lines = body.split('\\n').filter(l => l.trim());
    let synthesisLines = [];
    let inSources = false;

    for (const line of lines) {
        if (line.includes('Opens in new tab') || line.includes('About this result')) {
            inSources = true;
            continue;
        }
        if (!inSources && line.length > 10) {
            // Skip navigation items
            if (['AI Mode', 'All', 'Images', 'Videos', 'News', 'More', 'Sign in',
                 'Accessibility', 'Skip to', 'Filters'].some(skip => line.startsWith(skip))) continue;
            synthesisLines.push(line);
        }
    }
    data.synthesis = synthesisLines.join('\\n').substring(0, 5000);

    // Sections from h3 headings
    document.querySelectorAll('h3').forEach(h3 => {
        const heading = h3.innerText;
        if (heading.length < 3 || heading.length > 200) return;

        // Collect items (li) under this section
        let next = h3.nextElementSibling;
        const items = [];
        let content = '';
        while (next && next.tagName !== 'H3') {
            if (next.tagName === 'UL' || next.tagName === 'OL') {
                next.querySelectorAll('li').forEach(li => {
                    items.push(li.innerText.substring(0, 500));
                });
            } else if (next.innerText) {
                content += next.innerText + '\\n';
            }
            next = next.nextElementSibling;
        }

        data.sections.push({heading, content: content.substring(0, 2000), items});
    });

    // Source links
    document.querySelectorAll('a[href]').forEach(a => {
        if (!a.href.startsWith('http') || a.href.includes('google.com')) return;
        const text = a.innerText;
        if (text.length > 10 && text.length < 300) {
            try {
                const domain = new URL(a.href).hostname.replace('www.', '');
                data.sources.push({title: text, url: a.href, domain, snippet: ''});
            } catch {}
        }
    });

    // Deduplicate sources by URL
    const seen = new Set();
    data.sources = data.sources.filter(s => {
        if (seen.has(s.url)) return false;
        seen.add(s.url);
        return true;
    });

    return data;
})()
"""


async def extract_search_results(tab, query: str, search_type: str | None = None) -> SearchResult:
    """Extract search results from a Google Search results page.

    Routes to a type-specific extractor for verticals with different DOM layouts
    (e.g. News tab uses role="heading" instead of h3).
    """
    is_blocked = await tab.evaluate(BLOCKED_CHECK_JS)
    if is_blocked:
        raise SearchBlockedError("Google blocked the search — unusual traffic detected")

    js = NEWS_EXTRACT_JS if search_type == "news" else SEARCH_EXTRACT_JS
    raw = await tab.evaluate(js)
    if not raw or not isinstance(raw, dict):
        raise ExtractionError("Failed to extract search results from DOM")

    ai_overview = None
    if raw.get("ai_overview"):
        ao = raw["ai_overview"]
        ai_overview = AIOverview(
            text=ao.get("text", ""),
            sources=[Source(**s) for s in ao.get("sources", [])],
        )

    results = [OrganicResult(**r) for r in raw.get("results", [])]

    return SearchResult(
        query=query,
        ai_overview=ai_overview,
        results=results,
        people_also_ask=raw.get("people_also_ask", []),
        related_searches=raw.get("related_searches", []),
    )


async def extract_ai_mode(tab, query: str) -> AIResult:
    """Extract results from Google AI Mode page."""
    is_blocked = await tab.evaluate(BLOCKED_CHECK_JS)
    if is_blocked:
        raise SearchBlockedError("Google blocked the search — unusual traffic detected")

    raw = await tab.evaluate(AI_MODE_EXTRACT_JS)
    if not raw or not isinstance(raw, dict):
        raise ExtractionError("Failed to extract AI Mode results from DOM")

    sections = [Section(**s) for s in raw.get("sections", [])]
    sources = [Source(**s) for s in raw.get("sources", [])]

    return AIResult(
        query=query,
        synthesis=raw.get("synthesis", ""),
        sections=sections,
        sources=sources,
    )
