use serde::{Deserialize, Serialize};
use tail_fin_common::BrowserSession;
use tail_fin_common::TailFinError;

/// Result of probing a single authentication strategy against an endpoint.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProbeResult {
    pub strategy: String,
    pub success: bool,
    pub status_code: Option<u16>,
    pub has_data: Option<bool>,
    pub error: Option<String>,
}

/// Aggregated result of cascade-probing an endpoint with multiple strategies.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CascadeResult {
    pub best_strategy: String,
    pub probes: Vec<ProbeResult>,
    pub confidence: f64,
}

/// Order in which authentication strategies are tried (cheapest first).
pub const CASCADE_ORDER: &[&str] = &["public", "cookie", "header"];

/// Generate JavaScript that probes an endpoint with the given strategy.
///
/// - `"public"`: plain fetch, no credentials.
/// - `"cookie"`: fetch with `credentials: 'include'`.
/// - `"header"`: like cookie, plus extract a CSRF token from `document.cookie`
///   (looks for `ct0=`, `csrf_token=`, `_csrf=`) and attach it as
///   `X-Csrf-Token`.
///
/// All probes check `resp.ok && hasData` where hasData means the JSON response
/// has content and does not contain an API-level error code (`json.code !== 0`).
pub fn build_probe_js(url: &str, strategy: &str) -> String {
    let url_escaped = serde_json::to_string(url).unwrap_or_default();

    let fetch_options = match strategy {
        "public" => r#"{ method: 'GET' }"#.to_string(),
        "cookie" => r#"{ method: 'GET', credentials: 'include' }"#.to_string(),
        "header" => {
            // Extract CSRF token from cookies and add as header
            r#"(() => {
    const cookies = document.cookie || '';
    let csrf = '';
    for (const pattern of ['ct0=', 'csrf_token=', '_csrf=']) {
        const idx = cookies.indexOf(pattern);
        if (idx !== -1) {
            const start = idx + pattern.length;
            const end = cookies.indexOf(';', start);
            csrf = end === -1 ? cookies.substring(start) : cookies.substring(start, end);
            break;
        }
    }
    return {
        method: 'GET',
        credentials: 'include',
        headers: { 'X-Csrf-Token': csrf }
    };
})()"#
                .to_string()
        }
        _ => r#"{ method: 'GET' }"#.to_string(),
    };

    format!(
        r#"
(async () => {{
    try {{
        const opts = {fetch_options};
        const resp = await fetch({url_escaped}, opts);
        const status = resp.status;
        let hasData = false;
        let bodyText = '';
        try {{
            bodyText = await resp.clone().text();
            if (bodyText && bodyText.length > 0) {{
                const json = JSON.parse(bodyText);
                hasData = resp.ok && json !== null && json !== undefined && json.code !== 0;
            }}
        }} catch (e) {{
            // Non-JSON response
        }}
        return JSON.stringify({{ ok: resp.ok, status: status, hasData: hasData }});
    }} catch (e) {{
        return JSON.stringify({{ ok: false, status: 0, hasData: false, error: e.message }});
    }}
}})()
"#
    )
}

/// Probe a single endpoint with a specific authentication strategy.
pub async fn probe_endpoint(session: &BrowserSession, url: &str, strategy: &str) -> ProbeResult {
    let js = build_probe_js(url, strategy);

    match session.eval(&js).await {
        Ok(result) => {
            let text = match &result {
                serde_json::Value::String(s) => s.clone(),
                other => other.to_string(),
            };

            if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&text) {
                let ok = parsed.get("ok").and_then(|v| v.as_bool()).unwrap_or(false);
                let status = parsed
                    .get("status")
                    .and_then(|v| v.as_u64())
                    .map(|s| s as u16);
                let has_data = parsed.get("hasData").and_then(|v| v.as_bool());
                let error = parsed
                    .get("error")
                    .and_then(|v| v.as_str())
                    .map(|s| s.to_string());

                ProbeResult {
                    strategy: strategy.to_string(),
                    success: ok && has_data.unwrap_or(false),
                    status_code: status,
                    has_data,
                    error,
                }
            } else {
                ProbeResult {
                    strategy: strategy.to_string(),
                    success: false,
                    status_code: None,
                    has_data: None,
                    error: Some(format!("failed to parse probe result: {}", text)),
                }
            }
        }
        Err(e) => ProbeResult {
            strategy: strategy.to_string(),
            success: false,
            status_code: None,
            has_data: None,
            error: Some(e.to_string()),
        },
    }
}

/// Try each authentication strategy in cascade order against an endpoint.
///
/// The first strategy that succeeds wins. Confidence levels:
/// - `"public"` → 1.0
/// - `"cookie"` → 0.9
/// - `"header"` → 0.8
///
/// If no strategy succeeds, falls back to `"cookie"` at confidence 0.3.
pub async fn cascade_probe(
    session: &BrowserSession,
    url: &str,
) -> Result<CascadeResult, TailFinError> {
    let mut probes = Vec::new();

    for &strategy in CASCADE_ORDER {
        let result = probe_endpoint(session, url, strategy).await;
        let success = result.success;
        probes.push(result);

        if success {
            let confidence = match strategy {
                "public" => 1.0,
                "cookie" => 0.9,
                "header" => 0.8,
                _ => 0.5,
            };

            return Ok(CascadeResult {
                best_strategy: strategy.to_string(),
                probes,
                confidence,
            });
        }
    }

    // No strategy succeeded — fallback to cookie with low confidence.
    Ok(CascadeResult {
        best_strategy: "cookie".to_string(),
        probes,
        confidence: 0.3,
    })
}

/// Render a human-readable summary of cascade probe results.
pub fn render_cascade_result(result: &CascadeResult) -> String {
    let mut lines = Vec::new();
    lines.push(format!(
        "Best strategy: {} (confidence: {:.0}%)",
        result.best_strategy,
        result.confidence * 100.0
    ));
    lines.push(String::new());

    for probe in &result.probes {
        let status = probe
            .status_code
            .map(|s| s.to_string())
            .unwrap_or_else(|| "-".to_string());
        let icon = if probe.success { "+" } else { "-" };
        let data = probe
            .has_data
            .map(|d| if d { "has data" } else { "no data" })
            .unwrap_or("unknown");
        lines.push(format!(
            "  [{}] {} — HTTP {} — {}",
            icon, probe.strategy, status, data
        ));
        if let Some(err) = &probe.error {
            lines.push(format!("      error: {}", err));
        }
    }

    lines.join("\n")
}

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

    #[test]
    fn test_build_probe_js_public() {
        let js = build_probe_js("https://api.example.com/v1/hot", "public");
        assert!(js.contains("fetch("));
        assert!(!js.contains("credentials"));
    }

    #[test]
    fn test_build_probe_js_cookie() {
        let js = build_probe_js("https://api.example.com/v1/hot", "cookie");
        assert!(js.contains("credentials"));
        assert!(js.contains("include"));
    }

    #[test]
    fn test_build_probe_js_header() {
        let js = build_probe_js("https://api.example.com/v1/hot", "header");
        assert!(js.contains("X-Csrf-Token"));
        assert!(js.contains("ct0="));
        assert!(js.contains("csrf_token="));
        assert!(js.contains("_csrf="));
    }

    #[test]
    fn test_cascade_order() {
        assert_eq!(CASCADE_ORDER, &["public", "cookie", "header"]);
    }

    #[test]
    fn test_render_cascade_result() {
        let result = CascadeResult {
            best_strategy: "cookie".to_string(),
            probes: vec![
                ProbeResult {
                    strategy: "public".to_string(),
                    success: false,
                    status_code: Some(401),
                    has_data: Some(false),
                    error: None,
                },
                ProbeResult {
                    strategy: "cookie".to_string(),
                    success: true,
                    status_code: Some(200),
                    has_data: Some(true),
                    error: None,
                },
            ],
            confidence: 0.9,
        };

        let output = render_cascade_result(&result);
        assert!(output.contains("Best strategy: cookie"));
        assert!(output.contains("90%"));
        assert!(output.contains("[+] cookie"));
        assert!(output.contains("[-] public"));
    }
}
