use chromiumoxide::{Browser, BrowserConfig};
use futures::StreamExt;
use scraper::Html;
use std::{collections::HashSet, time::Duration};
use thiserror::Error;
use tokio::{task::JoinHandle, time::timeout};
use url::Url;

use super::utils;

#[derive(Debug, Error)]
pub enum HeadlessError {
    #[error("Chrome not found — install Google Chrome to enable JS rendering")]
    ChromeNotFound,
    #[error("Failed to build browser config: {0}")]
    Config(String),
    #[error("Browser error: {0}")]
    Browser(#[from] chromiumoxide::error::CdpError),
    #[error("Page fetch timed out")]
    Timeout,
}

pub struct HeadlessBrowser {
    browser: Browser,
    _handler: JoinHandle<()>,
}

impl HeadlessBrowser {
    pub async fn launch() -> Result<Self, HeadlessError> {
        let config = BrowserConfig::builder()
            .no_sandbox()
            .build()
            .map_err(HeadlessError::Config)?;

        let (browser, handler) = Browser::launch(config).await.map_err(|e| {
            let msg = e.to_string().to_lowercase();
            if msg.contains("not found")
                || msg.contains("no such file")
                || msg.contains("cannot find")
                || msg.contains("executablepath")
            {
                HeadlessError::ChromeNotFound
            } else {
                HeadlessError::Browser(e)
            }
        })?;

        let handle = tokio::spawn(async move {
            let mut handler = handler;
            while handler.next().await.is_some() {}
        });

        Ok(Self {
            browser,
            _handler: handle,
        })
    }

    pub async fn close(mut self) {
        let _ = self.browser.close().await;
    }

    /// Fetch fully-rendered HTML for a single URL, waiting for JS to execute.
    pub async fn fetch_rendered_html(&self, url: &Url) -> Result<String, HeadlessError> {
        let fetch = async {
            let page = self.browser.new_page(url.as_str()).await?;
            tokio::time::sleep(Duration::from_secs(3)).await;
            let content = page.content().await?;
            page.close().await?;
            Ok(content)
        };

        timeout(Duration::from_secs(15), fetch)
            .await
            .map_err(|_| HeadlessError::Timeout)?
    }

    async fn fetch_rendered(&self, url: &Url) -> Result<Html, HeadlessError> {
        self.fetch_rendered_html(url)
            .await
            .map(|content| Html::parse_document(&content))
    }

    /// Fetch the homepage and common contact page paths for a URL.
    /// Returns a list of extracted text blocks in the same format as the regular crawler.
    pub async fn fetch_contact_pages(&self, url: &Url) -> Result<Vec<String>, HeadlessError> {
        let contact = url.join("/contact").unwrap_or_else(|_| url.clone());
        let contact_us = url.join("/contact-us").unwrap_or_else(|_| url.clone());

        let pages = [url, &contact, &contact_us];
        let mut texts = Vec::new();
        let mut seen: HashSet<String> = HashSet::new();

        for page_url in &pages {
            if !seen.insert(page_url.to_string()) {
                continue;
            }

            match self.fetch_rendered(page_url).await {
                Ok(html) => {
                    texts.push(build_page_text(page_url, &html));
                }
                Err(HeadlessError::Timeout) => {
                    log::warn!("   ⏱️  Headless timeout for {}", page_url);
                }
                Err(e) => {
                    log::warn!("   ⚠️  Headless fetch failed for {}: {}", page_url, e);
                }
            }
        }

        Ok(texts)
    }
}

fn build_page_text(url: &Url, html: &Html) -> String {
    let text = utils::extract_text_content(html);
    let (mut emails, phones) = utils::extract_contact_links(html);
    utils::merge_unique(&mut emails, utils::extract_emails_from_html(html));

    let mut parts = vec![format!("URL: {}", url)];
    if !emails.is_empty() {
        parts.push(format!("Emails: {}", emails.join(", ")));
    }
    if !phones.is_empty() {
        parts.push(format!("Phones: {}", phones.join(", ")));
    }
    parts.push(text);
    sanitize_for_json(&parts.join("\n"))
}

/// Strip null bytes and C0 control chars (except tab/newline/CR) that would
/// produce invalid JSON when the text is embedded in an API request body.
fn sanitize_for_json(s: &str) -> String {
    s.chars()
        .filter(|&c| c >= ' ' || c == '\t' || c == '\n' || c == '\r')
        .collect()
}
