use std::collections::HashMap;

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

mod infer;
mod js;

use infer::{analyze_endpoints, epoch_seconds, infer_capabilities, NetworkEntry};
use js::{
    AUTO_SCROLL_JS, DISCOVER_APIS_FROM_PERFORMANCE_JS, INJECT_INTERCEPTOR_JS, READ_METADATA_JS,
};

pub use infer::detect_site_name;

// ---------------------------------------------------------------------------
// Data types
// ---------------------------------------------------------------------------

/// An API endpoint discovered and analyzed from network traffic.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnalyzedEndpoint {
    pub pattern: String,
    pub method: String,
    pub url: String,
    pub status: Option<u16>,
    pub content_type: String,
    pub query_params: Vec<String>,
    pub has_search_param: bool,
    pub has_pagination_param: bool,
    pub has_limit_param: bool,
    pub auth_indicators: Vec<String>,
    pub item_path: Option<String>,
    pub item_count: usize,
    pub detected_fields: HashMap<String, String>,
}

/// A recommended argument for an inferred capability.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecommendedArg {
    pub name: String,
    #[serde(rename = "type")]
    pub arg_type: String,
    pub required: bool,
    pub default: Option<Value>,
}

/// A capability inferred from endpoint analysis.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InferredCapability {
    pub name: String,
    pub description: String,
    pub strategy: String,
    pub endpoint: String,
    pub item_path: Option<String>,
    pub recommended_columns: Vec<String>,
    pub recommended_args: Vec<RecommendedArg>,
}

/// The full result of exploring a URL.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExploreResult {
    pub site: String,
    pub target_url: String,
    pub final_url: String,
    pub title: String,
    pub top_strategy: String,
    pub endpoint_count: usize,
    pub api_endpoint_count: usize,
    pub capabilities: Vec<InferredCapability>,
    pub auth_indicators: Vec<String>,
    pub out_dir: String,
}

// ---------------------------------------------------------------------------
// Main explore function
// ---------------------------------------------------------------------------

/// Explore a URL by intercepting network traffic to discover API endpoints
/// and infer capabilities.
///
/// Steps:
/// 1. Inject interceptor JS
/// 2. Navigate to URL (ignore timeout), wait for network idle
/// 3. Auto-scroll to trigger lazy loading, wait 5s
/// 4. Read page metadata
/// 5. Read captured network entries
/// 6. Analyze endpoints
/// 7. Infer capabilities
/// 8. Write artifacts to `~/.tail-fin/explore/<site>/`
/// 9. Return result
pub async fn explore_url(
    session: &BrowserSession,
    url: &str,
    site_override: Option<&str>,
    goal: Option<&str>,
    wait_seconds: u64,
) -> Result<ExploreResult, TailFinError> {
    // 1. Navigate to an empty page first so we can inject interceptor
    let _ = session.eval("window.location.href = 'about:blank'").await;
    tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;

    // 2. Inject interceptor on blank page
    let _ = session.eval(INJECT_INTERCEPTOR_JS).await;

    // 3. Navigate to target — interceptor survives on same-origin transitions
    //    but NOT cross-origin. So we use location.href from the page context.
    let url_json = serde_json::to_string(url).unwrap_or_default();
    let _ = session
        .eval(&format!("window.location.href = {}", url_json))
        .await;
    tokio::time::sleep(tokio::time::Duration::from_secs(wait_seconds + 3)).await;

    // 4. Re-inject interceptor (navigation likely cleared it)
    let _ = session.eval(INJECT_INTERCEPTOR_JS).await;

    // 5. Auto-scroll to trigger lazy-loaded content
    let _ = session.eval(AUTO_SCROLL_JS).await;
    tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;

    // 6. Also try to re-fetch the page's own API by looking at Performance entries
    let _ = session.eval(DISCOVER_APIS_FROM_PERFORMANCE_JS).await;

    // 4. Read metadata
    let meta_raw = session.eval(READ_METADATA_JS).await?;
    let meta_text = match &meta_raw {
        Value::String(s) => s.clone(),
        other => other.to_string(),
    };
    let meta: Value = serde_json::from_str(&meta_text).unwrap_or_else(|_| {
        serde_json::json!({
            "url": url,
            "title": ""
        })
    });
    let final_url = meta
        .get("url")
        .and_then(|v| v.as_str())
        .unwrap_or(url)
        .to_string();
    let title = meta
        .get("title")
        .and_then(|v| v.as_str())
        .unwrap_or("")
        .to_string();

    // 5. Read network entries
    let net_raw = session
        .eval("JSON.stringify(window.__tailfin_net || [])")
        .await?;
    let net_text = match &net_raw {
        Value::String(s) => s.clone(),
        other => other.to_string(),
    };
    let entries: Vec<NetworkEntry> = serde_json::from_str(&net_text).unwrap_or_default();

    // 6. Analyze endpoints
    let analyzed = analyze_endpoints(&entries);
    let endpoint_count = entries.len();
    let api_endpoint_count = analyzed.len();

    // Detect site name
    let site = site_override
        .map(|s| s.to_string())
        .unwrap_or_else(|| detect_site_name(url));

    // 7. Infer capabilities
    let (capabilities, top_strategy, auth_indicators) = infer_capabilities(&analyzed, &site, goal);

    // 8. Write artifacts
    let base_dir = tail_fin_common::tail_fin_dir();
    let out_dir = base_dir.join("explore").join(&site);
    std::fs::create_dir_all(&out_dir).map_err(|e| TailFinError::Io(e.to_string()))?;

    let ts = epoch_seconds();

    // manifest.json
    let manifest = serde_json::json!({
        "site": site,
        "target_url": url,
        "final_url": final_url,
        "title": title,
        "explored_at": ts,
        "endpoint_count": endpoint_count,
        "api_endpoint_count": api_endpoint_count,
        "top_strategy": top_strategy,
    });
    std::fs::write(
        out_dir.join("manifest.json"),
        serde_json::to_string_pretty(&manifest).unwrap_or_default(),
    )
    .map_err(|e| TailFinError::Io(e.to_string()))?;

    // endpoints.json
    std::fs::write(
        out_dir.join("endpoints.json"),
        serde_json::to_string_pretty(&analyzed).unwrap_or_default(),
    )
    .map_err(|e| TailFinError::Io(e.to_string()))?;

    // capabilities.json
    std::fs::write(
        out_dir.join("capabilities.json"),
        serde_json::to_string_pretty(&capabilities).unwrap_or_default(),
    )
    .map_err(|e| TailFinError::Io(e.to_string()))?;

    // auth.json
    let auth_data = serde_json::json!({
        "indicators": auth_indicators,
        "top_strategy": top_strategy,
        "detected_at": ts,
    });
    std::fs::write(
        out_dir.join("auth.json"),
        serde_json::to_string_pretty(&auth_data).unwrap_or_default(),
    )
    .map_err(|e| TailFinError::Io(e.to_string()))?;

    Ok(ExploreResult {
        site,
        target_url: url.to_string(),
        final_url,
        title,
        top_strategy,
        endpoint_count,
        api_endpoint_count,
        capabilities,
        auth_indicators,
        out_dir: out_dir.to_string_lossy().to_string(),
    })
}

/// Render a human-readable summary of an explore result.
pub fn render_explore_summary(result: &ExploreResult) -> String {
    let mut lines = Vec::new();
    lines.push(format!("Site: {} ({})", result.site, result.target_url));
    lines.push(format!("Title: {}", result.title));
    lines.push(format!("Final URL: {}", result.final_url));
    lines.push(format!("Strategy: {}", result.top_strategy));
    lines.push(format!(
        "Endpoints: {} total, {} API",
        result.endpoint_count, result.api_endpoint_count
    ));
    lines.push(String::new());

    if result.capabilities.is_empty() {
        lines.push("No capabilities inferred.".to_string());
    } else {
        lines.push(format!("Capabilities ({}):", result.capabilities.len()));
        for cap in &result.capabilities {
            lines.push(format!("  - {} [{}]", cap.name, cap.strategy));
            lines.push(format!("    endpoint: {}", cap.endpoint));
            if let Some(path) = &cap.item_path {
                lines.push(format!("    item_path: {}", path));
            }
            if !cap.recommended_columns.is_empty() {
                lines.push(format!(
                    "    columns: {}",
                    cap.recommended_columns.join(", ")
                ));
            }
            if !cap.recommended_args.is_empty() {
                let args: Vec<String> = cap
                    .recommended_args
                    .iter()
                    .map(|a| {
                        if a.required {
                            format!("{}*", a.name)
                        } else {
                            a.name.clone()
                        }
                    })
                    .collect();
                lines.push(format!("    args: {}", args.join(", ")));
            }
        }
    }

    if !result.auth_indicators.is_empty() {
        lines.push(String::new());
        lines.push(format!(
            "Auth indicators: {}",
            result.auth_indicators.join(", ")
        ));
    }

    lines.push(String::new());
    lines.push(format!("Artifacts: {}", result.out_dir));

    lines.join("\n")
}

#[cfg(test)]
mod tests {
    use super::*;
    use infer::{analyze_endpoints, infer_capabilities, NetworkEntry};

    #[test]
    fn test_detect_site_name_known() {
        assert_eq!(detect_site_name("https://x.com/home"), "twitter");
        assert_eq!(detect_site_name("https://twitter.com/search"), "twitter");
        assert_eq!(
            detect_site_name("https://news.ycombinator.com/"),
            "hackernews"
        );
        assert_eq!(detect_site_name("https://www.zhihu.com/hot"), "zhihu");
        assert_eq!(
            detect_site_name("https://www.bilibili.com/ranking"),
            "bilibili"
        );
        assert_eq!(
            detect_site_name("https://search.bilibili.com/all?keyword=rust"),
            "bilibili"
        );
        assert_eq!(detect_site_name("https://www.reddit.com/r/rust"), "reddit");
    }

    #[test]
    fn test_detect_site_name_fallback() {
        assert_eq!(
            detect_site_name("https://api.example.com/v1/hot"),
            "example"
        );
        assert_eq!(
            detect_site_name("https://news.somesite.org/feed"),
            "somesite"
        );
    }

    #[test]
    fn test_detect_site_name_invalid() {
        assert_eq!(detect_site_name("not-a-url"), "unknown");
    }

    #[test]
    fn test_analyze_endpoints_filters_noise() {
        let entries = vec![
            NetworkEntry {
                method: "GET".to_string(),
                url: "https://example.com/analytics/collect".to_string(),
                status: Some(200),
                content_type: Some("application/json".to_string()),
                response_body: Some(r#"{"ok": true}"#.to_string()),
                request_headers: HashMap::new(),
            },
            NetworkEntry {
                method: "GET".to_string(),
                url: "https://example.com/style.css".to_string(),
                status: Some(200),
                content_type: Some("text/css".to_string()),
                response_body: None,
                request_headers: HashMap::new(),
            },
        ];

        let result = analyze_endpoints(&entries);
        assert!(result.is_empty());
    }

    #[test]
    fn test_analyze_endpoints_deduplicates() {
        // Both URLs differ only by a volatile param (wts), so they share the
        // same pattern after normalization and should be deduplicated.
        let entries = vec![
            NetworkEntry {
                method: "GET".to_string(),
                url: "https://api.example.com/v1/posts?wts=111".to_string(),
                status: Some(200),
                content_type: Some("application/json".to_string()),
                response_body: Some(r#"{"items": [{"id": 1}]}"#.to_string()),
                request_headers: HashMap::new(),
            },
            NetworkEntry {
                method: "GET".to_string(),
                url: "https://api.example.com/v1/posts?wts=222".to_string(),
                status: Some(200),
                content_type: Some("application/json".to_string()),
                response_body: Some(r#"{"items": [{"id": 2}]}"#.to_string()),
                request_headers: HashMap::new(),
            },
        ];

        let result = analyze_endpoints(&entries);
        assert_eq!(result.len(), 1);
    }

    #[test]
    fn test_analyze_endpoints_detects_fields() {
        let body = serde_json::json!({
            "data": {
                "list": [
                    {"title": "Post 1", "author": "Alice", "created_at": "2024-01-01"},
                    {"title": "Post 2", "author": "Bob", "created_at": "2024-01-02"}
                ]
            }
        });

        let entries = vec![NetworkEntry {
            method: "GET".to_string(),
            url: "https://api.example.com/v1/hot".to_string(),
            status: Some(200),
            content_type: Some("application/json".to_string()),
            response_body: Some(body.to_string()),
            request_headers: HashMap::new(),
        }];

        let result = analyze_endpoints(&entries);
        assert_eq!(result.len(), 1);
        let ep = &result[0];
        assert_eq!(ep.item_count, 2);
        assert_eq!(ep.item_path.as_deref(), Some("data.list"));
        assert!(ep.detected_fields.contains_key("title"));
        assert!(ep.detected_fields.contains_key("author"));
    }

    #[test]
    fn test_analyze_endpoints_sorted_by_usefulness() {
        let body_small = serde_json::json!({
            "items": [{"id": 1}]
        });
        let body_large = serde_json::json!({
            "items": [
                {"id": 1, "title": "A", "author": "X"},
                {"id": 2, "title": "B", "author": "Y"},
                {"id": 3, "title": "C", "author": "Z"}
            ]
        });

        let entries = vec![
            NetworkEntry {
                method: "GET".to_string(),
                url: "https://api.example.com/v1/small".to_string(),
                status: Some(200),
                content_type: Some("application/json".to_string()),
                response_body: Some(body_small.to_string()),
                request_headers: HashMap::new(),
            },
            NetworkEntry {
                method: "GET".to_string(),
                url: "https://api.example.com/v1/large".to_string(),
                status: Some(200),
                content_type: Some("application/json".to_string()),
                response_body: Some(body_large.to_string()),
                request_headers: HashMap::new(),
            },
        ];

        let result = analyze_endpoints(&entries);
        assert_eq!(result.len(), 2);
        // The large endpoint should come first (higher score)
        assert!(result[0].item_count > result[1].item_count);
    }

    #[test]
    fn test_infer_capabilities_basic() {
        let endpoints = vec![AnalyzedEndpoint {
            pattern: "https://api.example.com/v1/hot".to_string(),
            method: "GET".to_string(),
            url: "https://api.example.com/v1/hot".to_string(),
            status: Some(200),
            content_type: "application/json".to_string(),
            query_params: vec![],
            has_search_param: false,
            has_pagination_param: true,
            has_limit_param: false,
            auth_indicators: vec![],
            item_path: Some("data.list".to_string()),
            item_count: 10,
            detected_fields: HashMap::from([
                ("title".to_string(), "title".to_string()),
                ("author".to_string(), "author".to_string()),
            ]),
        }];

        let (caps, strategy, auth) = infer_capabilities(&endpoints, "example", None);
        assert_eq!(caps.len(), 1);
        assert_eq!(caps[0].name, "hot");
        assert_eq!(strategy, "public");
        assert!(auth.is_empty());
        // Should have limit and page args
        let arg_names: Vec<&str> = caps[0]
            .recommended_args
            .iter()
            .map(|a| a.name.as_str())
            .collect();
        assert!(arg_names.contains(&"limit"));
        assert!(arg_names.contains(&"page"));
    }

    #[test]
    fn test_infer_capabilities_search() {
        let endpoints = vec![AnalyzedEndpoint {
            pattern: "https://api.example.com/v1/search".to_string(),
            method: "GET".to_string(),
            url: "https://api.example.com/v1/search?q=rust".to_string(),
            status: Some(200),
            content_type: "application/json".to_string(),
            query_params: vec!["q".to_string()],
            has_search_param: true,
            has_pagination_param: false,
            has_limit_param: false,
            auth_indicators: vec!["bearer".to_string()],
            item_path: Some("results".to_string()),
            item_count: 5,
            detected_fields: HashMap::new(),
        }];

        let (caps, strategy, auth) = infer_capabilities(&endpoints, "example", None);
        assert_eq!(caps[0].name, "search");
        assert_eq!(strategy, "header");
        assert!(auth.contains(&"bearer".to_string()));
        // Should have keyword arg (required)
        let kw_arg = caps[0]
            .recommended_args
            .iter()
            .find(|a| a.name == "keyword");
        assert!(kw_arg.is_some());
        assert!(kw_arg.unwrap().required);
    }

    #[test]
    fn test_render_explore_summary() {
        let result = ExploreResult {
            site: "example".to_string(),
            target_url: "https://example.com".to_string(),
            final_url: "https://example.com/".to_string(),
            title: "Example Site".to_string(),
            top_strategy: "public".to_string(),
            endpoint_count: 15,
            api_endpoint_count: 3,
            capabilities: vec![InferredCapability {
                name: "hot".to_string(),
                description: "hot data from example".to_string(),
                strategy: "public".to_string(),
                endpoint: "https://api.example.com/v1/hot".to_string(),
                item_path: Some("data.list".to_string()),
                recommended_columns: vec!["title".to_string()],
                recommended_args: vec![RecommendedArg {
                    name: "limit".to_string(),
                    arg_type: "u32".to_string(),
                    required: false,
                    default: Some(Value::Number(20.into())),
                }],
            }],
            auth_indicators: vec![],
            out_dir: "/tmp/test".to_string(),
        };

        let summary = render_explore_summary(&result);
        assert!(summary.contains("Site: example"));
        assert!(summary.contains("hot [public]"));
        assert!(summary.contains("item_path: data.list"));
        assert!(summary.contains("Artifacts: /tmp/test"));
    }
}
