use std::{collections::HashSet, time::SystemTime};

use crate::{page::Page, protocol::types::*};

/// Per-connection CDP session state.
pub struct CdpSession {
    pub enabled_domains: HashSet<String>,
    pub scripts_on_new_document: Vec<String>,
    pub frame_id: String,
    loader_counter: u64,
    request_counter: u64,
    /// Set by Page.navigate — the server handles the actual page replacement.
    pub pending_navigate: Option<String>,
}

impl Default for CdpSession {
    fn default() -> Self {
        Self::new()
    }
}

impl CdpSession {
    pub fn new() -> Self {
        Self {
            enabled_domains: HashSet::new(),
            scripts_on_new_document: Vec::new(),
            frame_id: "main".to_string(),
            loader_counter: 0,
            request_counter: 0,
            pending_navigate: None,
        }
    }

    pub fn next_request_id(&mut self) -> String {
        self.request_counter += 1;
        format!("{}.1", self.request_counter)
    }

    pub fn next_loader_id(&mut self) -> String {
        self.loader_counter += 1;
        format!("loader-{}", self.loader_counter)
    }

    pub fn is_domain_enabled(&self, domain: &str) -> bool {
        self.enabled_domains.contains(domain)
    }

    pub fn enable_domain(&mut self, domain: &str) {
        self.enabled_domains.insert(domain.to_string());
    }

    /// Handle a CDP request and return response + events to emit.
    pub async fn handle_request(
        &mut self,
        page: &mut Page,
        req: &CdpRequest,
    ) -> (String, Vec<CdpEvent>) {
        // Fast path: Runtime.evaluate
        if req.method == "Runtime.evaluate" {
            let expression = req
                .params
                .get("expression")
                .and_then(|v| v.as_str())
                .unwrap_or("");

            return match page.evaluate(expression) {
                Ok(result_str) => {
                    let ty = js_type(&result_str);
                    let return_by_value = req
                        .params
                        .get("returnByValue")
                        .and_then(|v| v.as_bool())
                        .unwrap_or(false);

                    let resp_json = if return_by_value {
                        let raw_value = match ty {
                            "number" | "boolean" => result_str.clone(),
                            "undefined" => "null".to_string(),
                            _ => {
                                if serde_json::from_str::<serde_json::Value>(&result_str).is_ok() {
                                    result_str.clone()
                                } else {
                                    json_escape_string(&result_str)
                                }
                            }
                        };
                        format!(
                            r#"{{"id":{},"result":{{"result":{{"type":"{}","value":{}}}}}}}"#,
                            req.id, ty, raw_value
                        )
                    } else {
                        format!(
                            r#"{{"id":{},"result":{{"type":"{}","value":{}}}}}"#,
                            req.id,
                            ty,
                            json_escape_string(&result_str)
                        )
                    };
                    (resp_json, Vec::new())
                }
                Err(e) => {
                    let resp = to_json(&serde_json::json!({
                        "id": req.id,
                        "result": {
                            "exceptionDetails": {
                                "text": e.to_string(),
                                "lineNumber": 0,
                                "columnNumber": 0,
                            }
                        }
                    }));
                    (resp, Vec::new())
                }
            };
        }

        let mut events = Vec::new();

        let result: Result<serde_json::Value, String> = match req.method.as_str() {
            // --- Target domain ---
            "Target.getTargets" => Ok(serde_json::json!({
                "targetInfos": [{
                    "targetId": "page-1",
                    "type": "page",
                    "title": page.title(),
                    "url": page.url(),
                    "attached": true,
                }]
            })),

            // --- Page domain ---
            "Page.enable" => {
                self.enable_domain("Page");
                Ok(serde_json::json!({}))
            }
            "Page.navigate" => {
                let url = req
                    .params
                    .get("url")
                    .and_then(|v| v.as_str())
                    .unwrap_or("about:blank");
                let loader_id = self.next_loader_id();

                if url != "about:blank" {
                    self.pending_navigate = Some(url.to_string());
                }

                if self.is_domain_enabled("Page") {
                    events.push(CdpEvent::new(
                        "Page.frameNavigated",
                        serde_json::json!({
                            "frame": {
                                "id": self.frame_id,
                                "loaderId": loader_id,
                                "url": url,
                                "securityOrigin": url,
                                "mimeType": "text/html",
                            }
                        }),
                    ));
                    events.push(CdpEvent::new(
                        "Page.domContentEventFired",
                        serde_json::json!({ "timestamp": timestamp() }),
                    ));
                    events.push(CdpEvent::new(
                        "Page.loadEventFired",
                        serde_json::json!({ "timestamp": timestamp() }),
                    ));
                }

                Ok(serde_json::json!({
                    "frameId": self.frame_id,
                    "loaderId": loader_id,
                }))
            }
            "Page.getFrameTree" => Ok(serde_json::json!({
                "frameTree": {
                    "frame": {
                        "id": self.frame_id,
                        "loaderId": format!("loader-{}", self.loader_counter),
                        "url": page.url(),
                        "securityOrigin": page.url(),
                        "mimeType": "text/html",
                    },
                    "childFrames": []
                }
            })),
            "Page.addScriptToEvaluateOnNewDocument" => {
                let source = req
                    .params
                    .get("source")
                    .and_then(|v| v.as_str())
                    .unwrap_or("");
                self.scripts_on_new_document.push(source.to_string());
                Ok(serde_json::json!({
                    "identifier": format!("script-{}", self.scripts_on_new_document.len())
                }))
            }
            "Page.setLifecycleEventsEnabled" => Ok(serde_json::json!({})),
            "Page.createIsolatedWorld" => Ok(serde_json::json!({ "executionContextId": 2 })),

            // --- Runtime domain ---
            "Runtime.enable" => {
                self.enable_domain("Runtime");
                events.push(CdpEvent::new(
                    "Runtime.executionContextCreated",
                    serde_json::json!({
                        "context": {
                            "id": 1,
                            "origin": page.url(),
                            "name": "",
                            "auxData": { "isDefault": true, "type": "default", "frameId": self.frame_id }
                        }
                    }),
                ));
                Ok(serde_json::json!({}))
            }
            "Runtime.callFunctionOn" => {
                let decl = req
                    .params
                    .get("functionDeclaration")
                    .and_then(|v| v.as_str())
                    .unwrap_or("() => undefined");
                let code = format!("({})()", decl);
                match page.evaluate(&code) {
                    Ok(result_str) => Ok(serde_json::json!({
                        "result": { "type": js_type(&result_str), "value": result_str }
                    })),
                    Err(e) => Ok(serde_json::json!({
                        "exceptionDetails": { "text": e.to_string() }
                    })),
                }
            }
            "Runtime.disable" => Ok(serde_json::json!({})),
            "Runtime.runIfWaitingForDebugger" => Ok(serde_json::json!({})),

            // --- DOM domain ---
            "DOM.enable" => {
                self.enable_domain("DOM");
                Ok(serde_json::json!({}))
            }
            "DOM.getDocument" => {
                let _depth = req
                    .params
                    .get("depth")
                    .and_then(|v| v.as_i64())
                    .unwrap_or(1);
                Ok(serde_json::json!({
                    "root": {
                        "nodeId": 1,
                        "backendNodeId": 1,
                        "nodeType": 9,
                        "nodeName": "#document",
                        "localName": "",
                        "nodeValue": "",
                        "childNodeCount": 1,
                        "documentURL": page.url(),
                        "baseURL": page.url(),
                    }
                }))
            }
            "DOM.querySelector" => {
                let selector = req
                    .params
                    .get("selector")
                    .and_then(|v| v.as_str())
                    .unwrap_or("");
                let has = page.has_element(selector);
                Ok(serde_json::json!({ "nodeId": if has { 2 } else { 0 } }))
            }
            "DOM.getOuterHTML" => {
                let result = page
                    .evaluate("document.documentElement.outerHTML")
                    .unwrap_or_default();
                Ok(serde_json::json!({ "outerHTML": result }))
            }
            "DOM.disable" => Ok(serde_json::json!({})),

            // --- Network domain ---
            "Network.enable" => {
                self.enable_domain("Network");
                Ok(serde_json::json!({}))
            }
            "Network.getCookies" => Ok(serde_json::json!({ "cookies": [] })),
            "Network.setCookies" => Ok(serde_json::json!({})),
            "Network.disable" => {
                self.enabled_domains.remove("Network");
                Ok(serde_json::json!({}))
            }
            "Network.setExtraHTTPHeaders" => Ok(serde_json::json!({})),
            "Network.getResponseBody" => Ok(serde_json::json!({
                "body": "",
                "base64Encoded": false,
            })),
            "Network.clearBrowserCache" => Ok(serde_json::json!({})),
            "Network.clearBrowserCookies" => Ok(serde_json::json!({})),
            "Network.setCacheDisabled" => Ok(serde_json::json!({})),

            // --- Emulation domain ---
            "Emulation.setDeviceMetricsOverride" => Ok(serde_json::json!({})),
            "Emulation.setUserAgentOverride" => Ok(serde_json::json!({})),
            "Emulation.setTouchEmulationEnabled" => Ok(serde_json::json!({})),

            // --- Browser domain ---
            "Browser.getVersion" => Ok(serde_json::json!({
                "protocolVersion": "1.3",
                "product": "hpx-browser/0.1.0",
                "revision": "0",
                "userAgent": "Mozilla/5.0 hpx-browser/0.1.0",
                "jsVersion": "V8",
            })),

            // --- Log domain ---
            "Log.enable" => Ok(serde_json::json!({})),
            "Log.disable" => Ok(serde_json::json!({})),

            // --- Inspector domain ---
            "Inspector.enable" => Ok(serde_json::json!({})),

            // --- Performance domain ---
            "Performance.enable" => Ok(serde_json::json!({})),

            // --- Security domain ---
            "Security.enable" => Ok(serde_json::json!({})),

            // --- Input domain ---
            "Input.dispatchMouseEvent" => Ok(serde_json::json!({})),
            "Input.dispatchKeyEvent" => Ok(serde_json::json!({})),
            "Input.dispatchTouchEvent" => Ok(serde_json::json!({})),
            "Input.insertText" => Ok(serde_json::json!({})),
            "Input.setIgnoreInputEvents" => Ok(serde_json::json!({})),

            // Unknown method
            _ => {
                let err = CdpError::method_not_found(req.id, &req.method);
                return (to_json(&err), events);
            }
        };

        match result {
            Ok(value) => (to_json(&CdpResponse::ok(req.id, value)), events),
            Err(msg) => (to_json(&CdpError::internal(req.id, &msg)), events),
        }
    }
}

/// Fast JSON string escaping — avoids serde_json::to_string overhead.
pub fn json_escape_string(s: &str) -> String {
    if s.bytes().all(|b| b > 31 && b != b'"' && b != b'\\') {
        let mut out = String::with_capacity(s.len() + 2);
        out.push('"');
        out.push_str(s);
        out.push('"');
        return out;
    }
    let mut out = String::with_capacity(s.len() + 8);
    out.push('"');
    for ch in s.chars() {
        match ch {
            '"' => out.push_str("\\\""),
            '\\' => out.push_str("\\\\"),
            '\n' => out.push_str("\\n"),
            '\r' => out.push_str("\\r"),
            '\t' => out.push_str("\\t"),
            c if (c as u32) < 32 => {
                out.push_str(&format!("\\u{:04x}", c as u32));
            }
            c => out.push(c),
        }
    }
    out.push('"');
    out
}

fn timestamp() -> f64 {
    SystemTime::now()
        .duration_since(SystemTime::UNIX_EPOCH)
        .map(|d| d.as_secs_f64())
        .unwrap_or(0.0)
}

fn js_type(value: &str) -> &'static str {
    match value {
        "undefined" => "undefined",
        "null" => "object",
        "true" | "false" => "boolean",
        s if s.parse::<f64>().is_ok() => "number",
        _ => "string",
    }
}

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

    #[tokio::test]
    async fn handle_page_enable() {
        let mut session = CdpSession::new();
        let mut page = Page::from_html("<html><head></head><body></body></html>", false)
            .await
            .unwrap();
        let req = CdpRequest {
            id: 1,
            method: "Page.enable".to_string(),
            params: serde_json::Value::Null,
        };
        let (resp, _events) = session.handle_request(&mut page, &req).await;
        assert!(resp.contains("\"id\":1"));
        assert!(session.is_domain_enabled("Page"));
    }

    #[tokio::test]
    async fn handle_runtime_evaluate() {
        let mut session = CdpSession::new();
        let mut page = Page::from_html("<html><head></head><body></body></html>", false)
            .await
            .unwrap();
        let req = CdpRequest {
            id: 2,
            method: "Runtime.evaluate".to_string(),
            params: serde_json::json!({"expression": "1 + 2"}),
        };
        let (resp, _) = session.handle_request(&mut page, &req).await;
        // Page.evaluate is a stub returning "undefined" — that's fine for structure test
        assert!(resp.contains("\"id\":2"), "response: {}", resp);
    }

    #[tokio::test]
    async fn handle_runtime_enable_emits_context() {
        let mut session = CdpSession::new();
        let mut page = Page::from_html("<html><head></head><body></body></html>", false)
            .await
            .unwrap();
        let req = CdpRequest {
            id: 3,
            method: "Runtime.enable".to_string(),
            params: serde_json::Value::Null,
        };
        let (_, events) = session.handle_request(&mut page, &req).await;
        assert_eq!(events.len(), 1);
        assert_eq!(events[0].method, "Runtime.executionContextCreated");
    }

    #[tokio::test]
    async fn handle_page_navigate() {
        let mut session = CdpSession::new();
        session.enable_domain("Page");
        let mut page = Page::from_html("<html><head></head><body></body></html>", false)
            .await
            .unwrap();
        let req = CdpRequest {
            id: 4,
            method: "Page.navigate".to_string(),
            params: serde_json::json!({"url": "about:blank"}),
        };
        let (resp, events) = session.handle_request(&mut page, &req).await;
        assert!(resp.contains("frameId"));
        assert!(resp.contains("loaderId"));
        assert_eq!(events.len(), 3);
        assert_eq!(events[0].method, "Page.frameNavigated");
        assert_eq!(events[1].method, "Page.domContentEventFired");
        assert_eq!(events[2].method, "Page.loadEventFired");
    }

    #[tokio::test]
    async fn handle_dom_get_document() {
        let mut session = CdpSession::new();
        let mut page = Page::from_html("<html><head></head><body></body></html>", false)
            .await
            .unwrap();
        let req = CdpRequest {
            id: 5,
            method: "DOM.getDocument".to_string(),
            params: serde_json::json!({}),
        };
        let (resp, _) = session.handle_request(&mut page, &req).await;
        assert!(resp.contains("\"nodeType\":9"));
        assert!(resp.contains("#document"));
    }

    #[tokio::test]
    async fn handle_unknown_method() {
        let mut session = CdpSession::new();
        let mut page = Page::from_html("<html><head></head><body></body></html>", false)
            .await
            .unwrap();
        let req = CdpRequest {
            id: 99,
            method: "Unknown.method".to_string(),
            params: serde_json::Value::Null,
        };
        let (resp, _) = session.handle_request(&mut page, &req).await;
        assert!(resp.contains("-32601"), "response: {}", resp);
        assert!(resp.contains("Unknown.method"));
    }

    #[tokio::test]
    async fn handle_browser_get_version() {
        let mut session = CdpSession::new();
        let mut page = Page::from_html("<html><head></head><body></body></html>", false)
            .await
            .unwrap();
        let req = CdpRequest {
            id: 7,
            method: "Browser.getVersion".to_string(),
            params: serde_json::Value::Null,
        };
        let (resp, _) = session.handle_request(&mut page, &req).await;
        assert!(resp.contains("hpx-browser"));
    }

    #[tokio::test]
    async fn handle_add_script_on_new_document() {
        let mut session = CdpSession::new();
        let mut page = Page::from_html("<html><head></head><body></body></html>", false)
            .await
            .unwrap();
        let req = CdpRequest {
            id: 8,
            method: "Page.addScriptToEvaluateOnNewDocument".to_string(),
            params: serde_json::json!({"source": "window.__test = true;"}),
        };
        let (resp, _) = session.handle_request(&mut page, &req).await;
        assert!(resp.contains("identifier"));
        assert_eq!(session.scripts_on_new_document.len(), 1);
    }
}
