use std::collections::HashMap;

use serde::{Deserialize, Serialize};
use serde_json::Value;
use url::Url;

use crate::constants::{
    FIELD_ROLES, LIMIT_PARAMS, PAGINATION_PARAMS, SEARCH_PARAMS, VOLATILE_PARAMS,
};

/// Classification of query parameters found in a URL.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ParamClassification {
    /// All non-volatile query parameter names.
    pub params: Vec<String>,
    /// Whether any search-related parameter was found.
    pub has_search: bool,
    /// Whether any pagination-related parameter was found.
    pub has_pagination: bool,
    /// Whether any limit-related parameter was found.
    pub has_limit: bool,
}

/// Normalize a URL into a pattern: replace numeric path segments with `{id}`,
/// hex strings (8+ chars) with `{hex}`, and strip volatile query parameters.
pub fn url_to_pattern(raw_url: &str) -> String {
    let parsed = match Url::parse(raw_url) {
        Ok(u) => u,
        Err(_) => return raw_url.to_string(),
    };

    let scheme = parsed.scheme();
    let host = parsed.host_str().unwrap_or("");
    let port_suffix = parsed.port().map(|p| format!(":{}", p)).unwrap_or_default();

    // Normalize path segments.
    let path = parsed.path();
    let normalized_path: String = path
        .split('/')
        .map(|seg| {
            if seg.is_empty() {
                return seg.to_string();
            }
            // Pure numeric segments become {id}.
            if seg.chars().all(|c| c.is_ascii_digit()) && !seg.is_empty() {
                return "{id}".to_string();
            }
            // Hex strings of 8+ characters become {hex}.
            if seg.len() >= 8 && seg.chars().all(|c| c.is_ascii_hexdigit()) {
                return "{hex}".to_string();
            }
            seg.to_string()
        })
        .collect::<Vec<_>>()
        .join("/");

    // Filter out volatile query parameters.
    let kept_params: Vec<(String, String)> = parsed
        .query_pairs()
        .filter(|(k, _)| {
            let key = k.to_lowercase();
            !VOLATILE_PARAMS.contains(&key.as_str())
        })
        .map(|(k, v)| (k.into_owned(), v.into_owned()))
        .collect();

    let query_suffix = if kept_params.is_empty() {
        String::new()
    } else {
        let pairs: Vec<String> = kept_params
            .iter()
            .map(|(k, v)| format!("{}={}", k, v))
            .collect();
        format!("?{}", pairs.join("&"))
    };

    format!(
        "{}://{}{}{}{}",
        scheme, host, port_suffix, normalized_path, query_suffix
    )
}

/// Recursively find the largest array of objects in a JSON value.
/// Returns the dotted path and a reference to the array items.
/// Searches up to `max_depth` levels deep (capped at 5).
pub fn find_array_path<'a>(value: &'a Value, max_depth: usize) -> Option<(String, &'a Vec<Value>)> {
    let depth = max_depth.min(5);
    let mut best: Option<(String, &'a Vec<Value>)> = None;

    fn walk<'a>(
        val: &'a Value,
        prefix: &str,
        remaining: usize,
        best: &mut Option<(String, &'a Vec<Value>)>,
    ) {
        if remaining == 0 {
            return;
        }
        match val {
            Value::Array(arr) => {
                // Only consider arrays of objects.
                let obj_count = arr.iter().filter(|v| v.is_object()).count();
                if obj_count > 0 {
                    let dominated = best
                        .as_ref()
                        .map(|(_, b)| arr.len() > b.len())
                        .unwrap_or(true);
                    if dominated {
                        *best = Some((prefix.to_string(), arr));
                    }
                }
            }
            Value::Object(map) => {
                for (key, child) in map {
                    let path = if prefix.is_empty() {
                        key.clone()
                    } else {
                        format!("{}.{}", prefix, key)
                    };
                    walk(child, &path, remaining - 1, best);
                }
            }
            _ => {}
        }
    }

    walk(value, "", depth, &mut best);
    best
}

/// Flatten nested object keys to dotted notation up to `max_depth`.
pub fn flatten_fields(value: &Value, prefix: &str, max_depth: usize) -> Vec<String> {
    let mut result = Vec::new();

    fn walk(val: &Value, prefix: &str, remaining: usize, out: &mut Vec<String>) {
        match val {
            Value::Object(map) if remaining > 0 => {
                for (key, child) in map {
                    let path = if prefix.is_empty() {
                        key.clone()
                    } else {
                        format!("{}.{}", prefix, key)
                    };
                    walk(child, &path, remaining - 1, out);
                }
            }
            _ => {
                if !prefix.is_empty() {
                    out.push(prefix.to_string());
                }
            }
        }
    }

    walk(value, prefix, max_depth, &mut result);
    result
}

/// Match leaf field names against `FIELD_ROLES` to detect semantic roles.
/// Returns a map from field path to role name.
pub fn detect_field_roles(sample_fields: &[String]) -> HashMap<String, String> {
    let mut roles = HashMap::new();

    for field in sample_fields {
        // Use the last segment (leaf name) for matching.
        let leaf = field.rsplit('.').next().unwrap_or(field).to_lowercase();
        for (role, keywords) in FIELD_ROLES {
            if keywords.iter().any(|kw| leaf.contains(kw)) {
                roles.insert(field.clone(), role.to_string());
                break;
            }
        }
    }

    roles
}

/// Infer a capability name from a URL pattern and optional user goal.
pub fn infer_capability_name(url: &str, goal: Option<&str>) -> String {
    // If a goal is provided, try normalizing it first.
    if let Some(g) = goal {
        if let Some(canonical) = crate::constants::normalize_goal(g) {
            return canonical.to_string();
        }
    }

    let parsed = Url::parse(url).ok();
    let path = parsed.as_ref().map(|u| u.path()).unwrap_or("");

    // Check for well-known path patterns.
    let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
    let known_patterns = [
        ("hot", "hot"),
        ("trending", "hot"),
        ("ranking", "hot"),
        ("rank", "hot"),
        ("search", "search"),
        ("query", "search"),
        ("feed", "feed"),
        ("timeline", "feed"),
        ("detail", "detail"),
        ("info", "detail"),
        ("recommend", "recommend"),
        ("comment", "comment"),
        ("comments", "comment"),
        ("download", "download"),
    ];

    for seg in segments.iter().rev() {
        let lower = seg.to_lowercase();
        for (pattern, name) in &known_patterns {
            if lower.contains(pattern) {
                return name.to_string();
            }
        }
    }

    // Fallback: last meaningful path segment (not numeric, not hex placeholder).
    for seg in segments.iter().rev() {
        if seg.chars().all(|c| c.is_ascii_digit()) {
            continue;
        }
        if seg.len() >= 8 && seg.chars().all(|c| c.is_ascii_hexdigit()) {
            continue;
        }
        return seg.to_lowercase();
    }

    "list".to_string()
}

/// Infer an authentication/fetch strategy from auth indicators.
///
/// Returns one of: `"intercept"`, `"header"`, `"public"`, `"cookie"`.
pub fn infer_strategy(auth_indicators: &[String]) -> String {
    if auth_indicators.is_empty() {
        return "public".to_string();
    }

    for indicator in auth_indicators {
        let lower = indicator.to_lowercase();
        if lower.contains("signature") {
            return "intercept".to_string();
        }
    }

    for indicator in auth_indicators {
        let lower = indicator.to_lowercase();
        if lower.contains("bearer") || lower.contains("csrf") {
            return "header".to_string();
        }
    }

    "cookie".to_string()
}

/// Detect auth indicators from HTTP headers.
pub fn detect_auth_from_headers(headers: &HashMap<String, String>) -> Vec<String> {
    let mut indicators = Vec::new();

    for key in headers.keys() {
        let lower = key.to_lowercase();
        if lower == "authorization" {
            indicators.push("bearer".to_string());
        } else if lower.starts_with("x-csrf") {
            indicators.push("csrf".to_string());
        } else if lower.starts_with("x-s") {
            indicators.push("signature".to_string());
        }
    }

    indicators
}

/// Classify query parameters in a URL.
pub fn classify_query_params(url: &str) -> ParamClassification {
    let parsed = match Url::parse(url) {
        Ok(u) => u,
        Err(_) => return ParamClassification::default(),
    };

    let mut classification = ParamClassification::default();

    for (key, _) in parsed.query_pairs() {
        let key_lower = key.to_lowercase();

        // Skip volatile params.
        if VOLATILE_PARAMS.contains(&key_lower.as_str()) {
            continue;
        }

        classification.params.push(key.into_owned());

        if SEARCH_PARAMS.contains(&key_lower.as_str()) {
            classification.has_search = true;
        }
        if PAGINATION_PARAMS.contains(&key_lower.as_str()) {
            classification.has_pagination = true;
        }
        if LIMIT_PARAMS.contains(&key_lower.as_str()) {
            classification.has_limit = true;
        }
    }

    classification
}

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

    #[test]
    fn test_url_to_pattern_basic() {
        let url = "https://api.example.com/v1/posts?wts=123&page=1";
        let pattern = url_to_pattern(url);
        assert_eq!(pattern, "https://api.example.com/v1/posts?page=1");
    }

    #[test]
    fn test_url_to_pattern_numeric_id() {
        let url = "https://api.example.com/v1/users/12345/posts";
        let pattern = url_to_pattern(url);
        assert_eq!(pattern, "https://api.example.com/v1/users/{id}/posts");
    }

    #[test]
    fn test_url_to_pattern_hex() {
        let url = "https://api.example.com/v1/items/abcdef1234567890/detail";
        let pattern = url_to_pattern(url);
        assert_eq!(pattern, "https://api.example.com/v1/items/{hex}/detail");
    }

    #[test]
    fn test_find_array_path() {
        let data = json!({
            "code": 0,
            "data": {
                "items": [
                    {"id": 1, "title": "hello"},
                    {"id": 2, "title": "world"}
                ],
                "total": 100
            }
        });

        let result = find_array_path(&data, 5);
        assert!(result.is_some());
        let (path, items) = result.unwrap();
        assert_eq!(path, "data.items");
        assert_eq!(items.len(), 2);
    }

    #[test]
    fn test_find_array_path_picks_largest() {
        let data = json!({
            "small": [{"a": 1}],
            "large": [{"b": 1}, {"b": 2}, {"b": 3}]
        });

        let result = find_array_path(&data, 5);
        assert!(result.is_some());
        let (path, items) = result.unwrap();
        assert_eq!(path, "large");
        assert_eq!(items.len(), 3);
    }

    #[test]
    fn test_flatten_fields() {
        let val = json!({
            "user": {
                "name": "alice",
                "profile": {
                    "avatar": "url"
                }
            },
            "title": "hello"
        });

        let fields = flatten_fields(&val, "", 3);
        assert!(fields.contains(&"title".to_string()));
        assert!(fields.contains(&"user.name".to_string()));
        assert!(fields.contains(&"user.profile.avatar".to_string()));
    }

    #[test]
    fn test_detect_field_roles() {
        let fields = vec![
            "title".to_string(),
            "author".to_string(),
            "cover_image".to_string(),
            "created_time".to_string(),
            "detail_url".to_string(),
            "unknown_field".to_string(),
        ];

        let roles = detect_field_roles(&fields);
        assert_eq!(roles.get("title"), Some(&"title".to_string()));
        assert_eq!(roles.get("author"), Some(&"author".to_string()));
        assert_eq!(roles.get("cover_image"), Some(&"cover".to_string()));
        assert_eq!(roles.get("created_time"), Some(&"time".to_string()));
        assert_eq!(roles.get("detail_url"), Some(&"url".to_string()));
        assert!(!roles.contains_key("unknown_field"));
    }

    #[test]
    fn test_infer_capability_name() {
        assert_eq!(
            infer_capability_name("https://api.example.com/v1/hot", None),
            "hot"
        );
        assert_eq!(
            infer_capability_name("https://api.example.com/v1/search?q=rust", None),
            "search"
        );
        assert_eq!(
            infer_capability_name("https://api.example.com/v1/ranking/list", None),
            "hot"
        );
        assert_eq!(
            infer_capability_name("https://api.example.com/v1/feed", None),
            "feed"
        );
        assert_eq!(
            infer_capability_name("https://api.example.com/v1/articles", None),
            "articles"
        );
    }

    #[test]
    fn test_infer_capability_name_with_goal() {
        assert_eq!(
            infer_capability_name("https://api.example.com/v1/xyz", Some("trending")),
            "hot"
        );
    }

    #[test]
    fn test_classify_query_params() {
        let url = "https://api.example.com/v1/search?q=rust&page=1&limit=20&wts=12345";
        let cls = classify_query_params(url);
        assert!(cls.has_search);
        assert!(cls.has_pagination);
        assert!(cls.has_limit);
        // wts should be filtered out
        assert!(!cls.params.contains(&"wts".to_string()));
        assert!(cls.params.contains(&"q".to_string()));
    }

    #[test]
    fn test_classify_query_params_empty() {
        let url = "https://api.example.com/v1/hot";
        let cls = classify_query_params(url);
        assert!(cls.params.is_empty());
        assert!(!cls.has_search);
        assert!(!cls.has_pagination);
        assert!(!cls.has_limit);
    }

    #[test]
    fn test_infer_strategy() {
        assert_eq!(infer_strategy(&[]), "public");
        assert_eq!(infer_strategy(&["signature".to_string()]), "intercept");
        assert_eq!(infer_strategy(&["bearer".to_string()]), "header");
        assert_eq!(infer_strategy(&["csrf".to_string()]), "header");
        assert_eq!(infer_strategy(&["some_cookie".to_string()]), "cookie");
    }

    #[test]
    fn test_detect_auth_from_headers() {
        let mut headers = HashMap::new();
        headers.insert("Authorization".to_string(), "Bearer token123".to_string());
        headers.insert("X-CSRF-Token".to_string(), "abc".to_string());

        let indicators = detect_auth_from_headers(&headers);
        assert!(indicators.contains(&"bearer".to_string()));
        assert!(indicators.contains(&"csrf".to_string()));
    }
}
