/// The browser bridge — implements jsdet_core::Bridge with a full
/// fake browser environment for URL detonation.
///
use std::sync::Mutex;

use jsdet_core::bridge::Bridge;
use jsdet_core::observation::Value;

use crate::canvas;
use crate::css::ComputedStyle;
use crate::dom::{Dom, NodeId};
use crate::navigator;
use crate::network::{self, ResponseRegistry};
use crate::persona::Persona;
use crate::storage::{CookieJar, WebStorage};
use crate::window::{History, Location};

/// Full browser bridge for URL detonation.
pub struct BrowserBridge {
    /// The DOM tree — shared mutable state.
    dom: Mutex<Dom>,
    /// Current page URL.
    location: Mutex<Location>,
    /// Browser history.
    #[allow(dead_code)]
    history: Mutex<History>,
    /// Browser persona (UA, platform, etc.).
    persona: Persona,
    /// localStorage.
    local_storage: WebStorage,
    /// sessionStorage.
    session_storage: WebStorage,
    /// Cookie jar.
    cookies: CookieJar,
    /// Fake network response registry.
    responses: ResponseRegistry,
    /// Collected redirect targets from window.location.assign / window.open.
    redirect_targets: Mutex<Vec<String>>,
    /// Collected window.open URLs.
    popup_urls: Mutex<Vec<String>>,
}

impl BrowserBridge {
    /// Create a bridge for detonating JS from the given URL.
    pub fn new(url: &str, html: &str, persona: Persona) -> Self {
        Self {
            dom: Mutex::new(Dom::parse(html)),
            location: Mutex::new(Location::from_url(url)),
            history: Mutex::new(History::new(url)),
            persona,
            local_storage: WebStorage::new(5000),
            session_storage: WebStorage::new(5000),
            cookies: CookieJar::new(),
            responses: ResponseRegistry::new(),
            redirect_targets: Mutex::new(Vec::new()),
            popup_urls: Mutex::new(Vec::new()),
        }
    }

    /// Set a fake response for a URL.
    pub fn add_response(&mut self, url: &str, response: network::FakeResponse) {
        self.responses.add_exact(url, response);
    }

    /// Pre-populate a cookie.
    pub fn add_cookie(&self, name: &str, value: &str, domain: &str) {
        self.cookies.add(name, value, domain);
    }

    /// Get collected redirect targets.
    pub fn redirect_targets(&self) -> Vec<String> {
        self.redirect_targets
            .lock()
            .map(|v| v.clone())
            .unwrap_or_default()
    }

    /// Get collected popup URLs.
    pub fn popup_urls(&self) -> Vec<String> {
        self.popup_urls
            .lock()
            .map(|v| v.clone())
            .unwrap_or_default()
    }

    fn handle_document(&self, method: &str, args: &[Value]) -> Result<Value, String> {
        match method {
            "createElement" => {
                let tag = args.first().and_then(|v| v.as_str()).unwrap_or("div");
                let mut dom = self.dom.lock().map_err(|e| e.to_string())?;
                let id = dom.create_element(tag);
                Ok(Value::Int(id.0 as i64))
            }
            "getElementById" => {
                let id = args.first().and_then(|v| v.as_str()).unwrap_or("");
                let dom = self.dom.lock().map_err(|e| e.to_string())?;
                match dom.get_element_by_id(id) {
                    Some(node_id) => Ok(Value::Int(node_id.0 as i64)),
                    None => Ok(Value::Null),
                }
            }
            "querySelector" => {
                let selector = args.first().and_then(|v| v.as_str()).unwrap_or("");
                let dom = self.dom.lock().map_err(|e| e.to_string())?;
                match dom.query_selector(selector) {
                    Some(node_id) => Ok(Value::Int(node_id.0 as i64)),
                    None => Ok(Value::Null),
                }
            }
            "querySelectorAll" => {
                let selector = args.first().and_then(|v| v.as_str()).unwrap_or("");
                let dom = self.dom.lock().map_err(|e| e.to_string())?;
                let results = dom.query_selector_all(selector);
                let ids: Vec<i64> = results.iter().map(|n| n.0 as i64).collect();
                Ok(Value::json(serde_json::to_string(&ids).unwrap_or_default()))
            }
            "write" | "writeln" => {
                // document.write — content is recorded as an ApiCall observation
                // by the sandbox bridge_call wrapper. The bridge handler just
                // needs to return success so JS doesn't throw.
                Ok(Value::Undefined)
            }
            _ => Err(format!("document.{method} is not defined")),
        }
    }

    fn handle_element(&self, method: &str, args: &[Value]) -> Result<Value, String> {
        let node_id = args
            .first()
            .and_then(|v| match v {
                Value::Int(n) => Some(NodeId(*n as u32)),
                _ => None,
            })
            .ok_or("missing node ID")?;

        match method {
            "getAttribute" => {
                let name = args.get(1).and_then(|v| v.as_str()).unwrap_or("");
                let dom = self.dom.lock().map_err(|e| e.to_string())?;
                match dom.get_attribute(node_id, name) {
                    Some(v) => Ok(Value::string(v.to_string())),
                    None => Ok(Value::Null),
                }
            }
            "setAttribute" => {
                let name = args.get(1).and_then(|v| v.as_str()).unwrap_or("");
                let value = args.get(2).and_then(|v| v.as_str()).unwrap_or("");
                let mut dom = self.dom.lock().map_err(|e| e.to_string())?;
                dom.set_attribute(node_id, name, value);
                Ok(Value::Undefined)
            }
            "removeAttribute" => {
                let name = args.get(1).and_then(|v| v.as_str()).unwrap_or("");
                let mut dom = self.dom.lock().map_err(|e| e.to_string())?;
                dom.remove_attribute(node_id, name);
                Ok(Value::Undefined)
            }
            "appendChild" => {
                let child_id = args
                    .get(1)
                    .and_then(|v| match v {
                        Value::Int(n) => Some(NodeId(*n as u32)),
                        _ => None,
                    })
                    .ok_or("missing child ID")?;
                let mut dom = self.dom.lock().map_err(|e| e.to_string())?;
                dom.append_child(node_id, child_id);
                Ok(Value::Int(child_id.0 as i64))
            }
            "innerHTML" => {
                let dom = self.dom.lock().map_err(|e| e.to_string())?;
                Ok(Value::string(dom.inner_html(node_id)))
            }
            "setInnerHTML" => {
                let html = args.get(1).and_then(|v| v.as_str()).unwrap_or("");
                let mut dom = self.dom.lock().map_err(|e| e.to_string())?;
                dom.set_inner_html(node_id, html);
                Ok(Value::Undefined)
            }
            _ => Err(format!("element.{method} is not defined")),
        }
    }

    fn handle_window(&self, method: &str, args: &[Value]) -> Result<Value, String> {
        match method {
            "open" => {
                let url = args
                    .first()
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string();
                if !url.is_empty()
                    && let Ok(mut urls) = self.popup_urls.lock()
                {
                    urls.push(url);
                }
                Ok(Value::Null) // window reference
            }
            "alert" | "confirm" | "prompt" => Ok(Value::Undefined),
            "postMessage" => Ok(Value::Undefined),
            _ => Err(format!("window.{method} is not defined")),
        }
    }

    fn handle_fetch(&self, args: &[Value]) -> Result<Value, String> {
        let url = args.first().and_then(|v| v.as_str()).unwrap_or("");
        let method = args.get(1).and_then(|v| v.as_str()).unwrap_or("GET");
        Ok(network::handle_fetch(url, method, &self.responses))
    }

    fn handle_storage(&self, area: &str, method: &str, args: &[Value]) -> Result<Value, String> {
        let storage = match area {
            "localStorage" => &self.local_storage,
            "sessionStorage" => &self.session_storage,
            _ => return Err(format!("{area} is not defined")),
        };

        match method {
            "getItem" => {
                let key = args.first().and_then(|v| v.as_str()).unwrap_or("");
                match storage.get_item(key) {
                    Some(v) => Ok(Value::string(v)),
                    None => Ok(Value::Null),
                }
            }
            "setItem" => {
                let key = args.first().and_then(|v| v.as_str()).unwrap_or("");
                let value = args.get(1).and_then(|v| v.as_str()).unwrap_or("");
                storage.set_item(key, value)?;
                Ok(Value::Undefined)
            }
            "removeItem" => {
                let key = args.first().and_then(|v| v.as_str()).unwrap_or("");
                storage.remove_item(key);
                Ok(Value::Undefined)
            }
            "clear" => {
                storage.clear();
                Ok(Value::Undefined)
            }
            "length" => Ok(Value::Int(storage.length() as i64)),
            "key" => {
                let index = args
                    .first()
                    .and_then(|v| match v {
                        Value::Int(n) => Some(*n as usize),
                        _ => None,
                    })
                    .unwrap_or(0);
                match storage.key(index) {
                    Some(k) => Ok(Value::string(k)),
                    None => Ok(Value::Null),
                }
            }
            _ => Err(format!("{area}.{method} is not defined")),
        }
    }
}

impl Bridge for BrowserBridge {
    fn call(&self, api: &str, args: &[Value]) -> Result<Value, String> {
        let parts: Vec<&str> = api.splitn(3, '.').collect();

        match parts.as_slice() {
            ["document", method] => self.handle_document(method, args),
            ["element", method] => self.handle_element(method, args),
            ["window", method] => self.handle_window(method, args),
            ["fetch"] => self.handle_fetch(args),
            ["localStorage", method] => self.handle_storage("localStorage", method, args),
            ["sessionStorage", method] => self.handle_storage("sessionStorage", method, args),
            ["getComputedStyle"] => {
                let node_id = args
                    .first()
                    .and_then(|v| match v {
                        Value::Int(n) => Some(NodeId(*n as u32)),
                        _ => None,
                    })
                    .unwrap_or(NodeId::ROOT);
                let dom = self.dom.lock().map_err(|e| e.to_string())?;
                let style = ComputedStyle::for_node(&dom, node_id);
                Ok(Value::json(style.to_json()))
            }
            ["canvas", "toDataURL"] => Ok(Value::string(canvas::handle_to_data_url(
                &self.persona.user_agent,
            ))),
            ["webgl", "getParameter"] => {
                let param = args.first().and_then(|v| v.as_str()).unwrap_or("");
                match canvas::handle_webgl_parameter(&self.persona.user_agent, param) {
                    Some(v) => Ok(Value::string(v)),
                    None => Ok(Value::Null),
                }
            }
            // Form probing — analyze the original HTML for credential forms.
            // This catches static HTML forms that weren't created by JavaScript.
            // The observation is emitted as a credential_form_detected call which
            // the analyzer recognizes.
            ["probe_html_forms"] | ["probe_static_forms"] => {
                let dom = self.dom.lock().map_err(|e| e.to_string())?;
                let password_inputs = dom.extract_password_inputs();

                if !password_inputs.is_empty() {
                    let form_actions = dom.extract_form_actions();
                    let action = form_actions.first().cloned().unwrap_or_default();
                    let field_count = password_inputs.len();
                    // Return as credential_form_detected format so the same analyzer
                    // handler processes it. The value encodes action + fields + has_password.
                    return Ok(Value::string(format!(
                        "CREDENTIAL_FORM:action={};fields=password({})",
                        action, field_count
                    )));
                }
                Ok(Value::Null)
            }
            _ => Err(format!("{api} is not defined")),
        }
    }

    fn get_property(&self, object: &str, property: &str) -> Result<Value, String> {
        match object {
            "navigator" => Ok(navigator::get_navigator_property(&self.persona, property)),
            "location" | "window.location" => {
                let loc = self.location.lock().map_err(|e| e.to_string())?;
                Ok(loc.get_property(property))
            }
            "document" => match property {
                "cookie" => Ok(Value::string(self.cookies.to_cookie_string())),
                "referrer" => Ok(Value::string(self.persona.referrer.clone())),
                "title" => Ok(Value::string(String::new())),
                "readyState" => Ok(Value::string("complete")),
                "domain" => {
                    let loc = self.location.lock().map_err(|e| e.to_string())?;
                    Ok(Value::string(loc.hostname.clone()))
                }
                _ => Err(format!("document.{property} is not defined")),
            },
            "screen" => match property {
                "width" => Ok(Value::Int(self.persona.screen_width as i64)),
                "height" => Ok(Value::Int(self.persona.screen_height as i64)),
                "colorDepth" | "pixelDepth" => Ok(Value::Int(self.persona.color_depth as i64)),
                "availWidth" => Ok(Value::Int(self.persona.screen_width as i64)),
                "availHeight" => Ok(Value::Int((self.persona.screen_height - 40) as i64)),
                _ => Err(format!("screen.{property} is not defined")),
            },
            _ => Err(format!("{object}.{property} is not defined")),
        }
    }

    fn set_property(&self, object: &str, property: &str, value: &Value) -> Result<(), String> {
        match (object, property) {
            ("document", "cookie") => {
                if let Some(cookie_str) = value.as_str() {
                    self.cookies.set_from_string(cookie_str);
                }
                Ok(())
            }
            ("location" | "window.location", "href") => {
                if let Some(url) = value.as_str() {
                    if let Ok(mut targets) = self.redirect_targets.lock() {
                        targets.push(url.to_string());
                    }
                    if let Ok(mut loc) = self.location.lock() {
                        *loc = Location::from_url(url);
                    }
                }
                Ok(())
            }
            ("document", "title") => Ok(()),
            _ => Ok(()),
        }
    }

    fn provided_globals(&self) -> Vec<String> {
        vec![
            "document".into(),
            "window".into(),
            "navigator".into(),
            "location".into(),
            "screen".into(),
            "localStorage".into(),
            "sessionStorage".into(),
            "fetch".into(),
            "XMLHttpRequest".into(),
            "getComputedStyle".into(),
        ]
    }

    fn bootstrap_js(&self) -> String {
        // Bootstrap JS is in a separate file for readability and testability.
        // Template variables ($URL, $HOSTNAME, etc.) are replaced at runtime.
        static TEMPLATE: &str = include_str!("../js/bootstrap.js");

        let loc = self.location.lock().map(|l| l.clone()).unwrap_or_default();
        TEMPLATE
            .replace("{{JSDET_URL}}", &loc.href)
            .replace("{{JSDET_PROTOCOL}}", &loc.protocol)
            .replace("{{JSDET_HOSTNAME}}", &loc.hostname)
            .replace("{{JSDET_PATHNAME}}", &loc.pathname)
            .replace("{{JSDET_SEARCH}}", &loc.search)
            .replace("{{JSDET_HASH}}", &loc.hash)
            .replace("{{JSDET_ORIGIN}}", &loc.origin)
            .replace("{{JSDET_REFERRER}}", &self.persona.referrer)
            .replace("{{JSDET_UA}}", &self.persona.user_agent)
            .replace("{{JSDET_PLATFORM}}", &self.persona.platform)
            .replace("{{JSDET_LANGUAGE}}", &self.persona.language)
            .replace(
                "{{JSDET_LANGUAGES_JSON}}",
                &serde_json::to_string(&self.persona.languages).unwrap_or_default(),
            )
            .replace(
                "{{JSDET_HW_CONCURRENCY}}",
                &self.persona.hardware_concurrency.to_string(),
            )
            .replace(
                "{{JSDET_DEVICE_MEMORY}}",
                &self.persona.device_memory.to_string(),
            )
            .replace(
                "{{JSDET_MAX_TOUCH}}",
                &self.persona.max_touch_points.to_string(),
            )
            .replace("{{JSDET_VENDOR}}", &self.persona.vendor)
            .replace("{{JSDET_APP_NAME}}", &self.persona.app_name)
            .replace("{{JSDET_APP_VERSION}}", &self.persona.app_version)
            .replace("{{JSDET_SCREEN_W}}", &self.persona.screen_width.to_string())
            .replace(
                "{{JSDET_SCREEN_H_TASKBAR}}",
                &self.persona.screen_height.saturating_sub(40).to_string(),
            )
            .replace(
                "{{JSDET_SCREEN_H}}",
                &self.persona.screen_height.to_string(),
            )
            .replace(
                "{{JSDET_COLOR_DEPTH}}",
                &self.persona.color_depth.to_string(),
            )
    }
}

// Legacy format string removed — bootstrap JS is now in js/bootstrap.js
// This dramatically improves readability, testability, and maintainability.
// The JS can now be:
// 1. Syntax-checked by any JS linter
// 2. Tested independently
// 3. Edited without worrying about {{ }} escaping
// 4. Read and understood by security researchers

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn bridge_provides_navigator() {
        let bridge = BrowserBridge::new("https://example.com", "<html></html>", Persona::default());
        let ua = bridge.get_property("navigator", "userAgent").unwrap();
        assert!(matches!(ua, Value::String(s, _) if s.contains("Chrome")));
    }

    #[test]
    fn bridge_provides_location() {
        let bridge = BrowserBridge::new(
            "https://example.com/path?q=1",
            "<html></html>",
            Persona::default(),
        );
        let host = bridge.get_property("location", "hostname").unwrap();
        assert_eq!(host, Value::string("example.com"));
    }

    #[test]
    fn bridge_creates_elements() {
        let bridge = BrowserBridge::new("https://example.com", "<html></html>", Persona::default());
        let id = bridge.call("document.createElement", &[Value::string("div")]);
        assert!(matches!(id, Ok(Value::Int(_))));
    }

    #[test]
    fn bridge_handles_cookies() {
        let bridge = BrowserBridge::new("https://example.com", "<html></html>", Persona::default());
        bridge
            .set_property("document", "cookie", &Value::string("test=hello"))
            .unwrap();
        let cookies = bridge.get_property("document", "cookie").unwrap();
        assert!(matches!(cookies, Value::String(s, _) if s.contains("test=hello")));
    }

    #[test]
    fn bridge_handles_storage() {
        let bridge = BrowserBridge::new("https://example.com", "<html></html>", Persona::default());
        bridge
            .call(
                "localStorage.setItem",
                &[Value::string("key"), Value::string("val")],
            )
            .unwrap();
        let result = bridge
            .call("localStorage.getItem", &[Value::string("key")])
            .unwrap();
        assert_eq!(result, Value::string("val"));
    }
}
