use std::collections::HashMap;

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

use crate::constants::{LIMIT_PARAMS, PAGINATION_PARAMS, SEARCH_PARAMS, VOLATILE_PARAMS};
use crate::explore::{AnalyzedEndpoint, InferredCapability};

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

/// Configuration for a generated command, written as JSON to disk.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandConfig {
    pub site: String,
    pub name: String,
    pub description: String,
    pub domain: String,
    pub strategy: String,
    pub endpoint: String,
    pub method: String,
    pub items_path: Option<String>,
    pub fields: HashMap<String, String>,
    pub args: HashMap<String, ArgDef>,
}

/// Definition of a single argument accepted by a command.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArgDef {
    #[serde(rename = "type")]
    pub arg_type: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub required: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub default: Option<Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
}

/// Summary of the synthesize operation.
#[derive(Debug, Clone, Serialize)]
pub struct SynthesizeResult {
    pub site: String,
    pub out_dir: String,
    pub candidates: Vec<CandidateSummary>,
}

/// A brief description of one generated command candidate.
#[derive(Debug, Clone, Serialize)]
pub struct CandidateSummary {
    pub name: String,
    pub path: String,
    pub strategy: String,
}

// ---------------------------------------------------------------------------
// Helper: load JSON
// ---------------------------------------------------------------------------

fn load_json<T: serde::de::DeserializeOwned>(path: &std::path::Path) -> Result<T, TailFinError> {
    let data = std::fs::read_to_string(path).map_err(|e| TailFinError::Io(e.to_string()))?;
    serde_json::from_str(&data).map_err(|e| TailFinError::Parse(e.to_string()))
}

// ---------------------------------------------------------------------------
// URL templating
// ---------------------------------------------------------------------------

/// Replace known query-parameter values with placeholders and strip volatile
/// params. Search params become `{keyword}`, limit params become `{limit}`,
/// pagination params become `{page}`. Other params are kept as-is.
pub fn build_templated_url(raw_url: &str, cap: &InferredCapability) -> String {
    let parsed = match url::Url::parse(raw_url) {
        Ok(u) => u,
        Err(_) => return raw_url.to_string(),
    };

    let has_search = cap.recommended_args.iter().any(|a| a.name == "keyword");
    let has_limit = cap.recommended_args.iter().any(|a| a.name == "limit");
    let has_page = cap.recommended_args.iter().any(|a| a.name == "page");

    let mut kept: Vec<(String, String)> = Vec::new();

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

        // Strip volatile params entirely.
        if VOLATILE_PARAMS.iter().any(|v| *v == key_lower.as_str()) {
            continue;
        }

        if has_search && SEARCH_PARAMS.iter().any(|s| *s == key_lower.as_str()) {
            kept.push((key.into_owned(), "{keyword}".to_string()));
        } else if has_limit && LIMIT_PARAMS.iter().any(|l| *l == key_lower.as_str()) {
            kept.push((key.into_owned(), "{limit}".to_string()));
        } else if has_page && PAGINATION_PARAMS.iter().any(|p| *p == key_lower.as_str()) {
            kept.push((key.into_owned(), "{page}".to_string()));
        } else {
            kept.push((key.into_owned(), val.into_owned()));
        }
    }

    let base = format!(
        "{}://{}{}",
        parsed.scheme(),
        parsed.host_str().unwrap_or(""),
        parsed.path()
    );

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

// ---------------------------------------------------------------------------
// Endpoint matching
// ---------------------------------------------------------------------------

/// Pick the best endpoint for a capability by matching on the URL pattern.
/// Falls back to the first endpoint if no pattern match is found.
pub fn choose_endpoint<'a>(
    cap: &InferredCapability,
    endpoints: &'a [AnalyzedEndpoint],
) -> Option<&'a AnalyzedEndpoint> {
    // Try exact pattern match first.
    if let Some(ep) = endpoints.iter().find(|ep| ep.pattern == cap.endpoint) {
        return Some(ep);
    }
    // Fallback to first endpoint.
    endpoints.first()
}

// ---------------------------------------------------------------------------
// Build command config
// ---------------------------------------------------------------------------

/// Build a `CommandConfig` from a capability and its matched endpoint.
pub fn build_command_config(
    site: &str,
    domain: &str,
    cap: &InferredCapability,
    ep: &AnalyzedEndpoint,
) -> CommandConfig {
    let templated = build_templated_url(&ep.url, cap);

    // Map fields from detected_fields (role -> field_path).
    let fields: HashMap<String, String> = ep.detected_fields.clone();

    // Build args with descriptions.
    let mut args = HashMap::new();
    for ra in &cap.recommended_args {
        let description = match ra.name.as_str() {
            "keyword" => Some("Search keyword".to_string()),
            "limit" => Some("Maximum number of items to return".to_string()),
            "page" => Some("Page number for pagination".to_string()),
            _ => None,
        };
        args.insert(
            ra.name.clone(),
            ArgDef {
                arg_type: ra.arg_type.clone(),
                required: Some(ra.required),
                default: ra.default.clone(),
                description,
            },
        );
    }

    CommandConfig {
        site: site.to_string(),
        name: cap.name.clone(),
        description: cap.description.clone(),
        domain: domain.to_string(),
        strategy: cap.strategy.clone(),
        endpoint: templated,
        method: ep.method.clone(),
        items_path: cap.item_path.clone(),
        fields,
        args,
    }
}

// ---------------------------------------------------------------------------
// Main synthesize function
// ---------------------------------------------------------------------------

/// Load explore artifacts and generate JSON command configs for the top
/// capabilities discovered during exploration.
///
/// Reads from `~/.tail-fin/explore/<site>/` and writes command configs to
/// `~/.tail-fin/sites/<site>/`.
pub fn synthesize(site: &str) -> Result<SynthesizeResult, TailFinError> {
    let base = tail_fin_common::tail_fin_dir();
    let explore_dir = base.join("explore").join(site);

    // Load explore artifacts.
    let manifest: Value = load_json(&explore_dir.join("manifest.json"))?;
    let endpoints: Vec<AnalyzedEndpoint> = load_json(&explore_dir.join("endpoints.json"))?;
    let capabilities: Vec<InferredCapability> = load_json(&explore_dir.join("capabilities.json"))?;

    // Extract domain from manifest's target_url.
    let target_url = manifest
        .get("target_url")
        .and_then(|v| v.as_str())
        .unwrap_or("");
    let domain = url::Url::parse(target_url)
        .ok()
        .and_then(|u| u.host_str().map(|h| h.to_string()))
        .unwrap_or_else(|| site.to_string());

    // Output directory for command configs.
    let out_dir = base.join("sites").join(site);
    std::fs::create_dir_all(&out_dir).map_err(|e| TailFinError::Io(e.to_string()))?;

    let mut candidates = Vec::new();

    // Process top 3 capabilities.
    for cap in capabilities.iter().take(3) {
        let ep = match choose_endpoint(cap, &endpoints) {
            Some(ep) => ep,
            None => continue,
        };

        let config = build_command_config(site, &domain, cap, ep);
        let file_path = out_dir.join(format!("{}.json", config.name));

        std::fs::write(
            &file_path,
            serde_json::to_string_pretty(&config).unwrap_or_default(),
        )
        .map_err(|e| TailFinError::Io(e.to_string()))?;

        candidates.push(CandidateSummary {
            name: config.name.clone(),
            path: file_path.to_string_lossy().to_string(),
            strategy: config.strategy.clone(),
        });
    }

    Ok(SynthesizeResult {
        site: site.to_string(),
        out_dir: out_dir.to_string_lossy().to_string(),
        candidates,
    })
}

// ---------------------------------------------------------------------------
// Render summary
// ---------------------------------------------------------------------------

/// Render a human-readable summary of synthesize results.
pub fn render_synthesize_summary(result: &SynthesizeResult) -> String {
    let mut lines = Vec::new();
    lines.push(format!("Site: {}", result.site));
    lines.push(format!("Output: {}", result.out_dir));
    lines.push(String::new());

    if result.candidates.is_empty() {
        lines.push("No command candidates generated.".to_string());
    } else {
        lines.push(format!("Commands ({}):", result.candidates.len()));
        for c in &result.candidates {
            lines.push(format!("  - {} [{}]", c.name, c.strategy));
            lines.push(format!("    {}", c.path));
        }
    }

    lines.join("\n")
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    fn sample_capability(name: &str, has_search: bool, has_page: bool) -> InferredCapability {
        let mut args = Vec::new();
        if has_search {
            args.push(RecommendedArg {
                name: "keyword".to_string(),
                arg_type: "String".to_string(),
                required: true,
                default: None,
            });
        }
        args.push(RecommendedArg {
            name: "limit".to_string(),
            arg_type: "u32".to_string(),
            required: false,
            default: Some(Value::Number(20.into())),
        });
        if has_page {
            args.push(RecommendedArg {
                name: "page".to_string(),
                arg_type: "u32".to_string(),
                required: false,
                default: Some(Value::Number(1.into())),
            });
        }
        InferredCapability {
            name: name.to_string(),
            description: format!("{} data from test", name),
            strategy: "public".to_string(),
            endpoint: format!("https://api.example.com/v1/{}", name),
            item_path: Some("data.list".to_string()),
            recommended_columns: vec!["title".to_string()],
            recommended_args: args,
        }
    }

    fn sample_endpoint(pattern: &str, url: &str) -> AnalyzedEndpoint {
        AnalyzedEndpoint {
            pattern: pattern.to_string(),
            method: "GET".to_string(),
            url: url.to_string(),
            status: Some(200),
            content_type: "application/json".to_string(),
            query_params: vec![],
            has_search_param: false,
            has_pagination_param: false,
            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()),
            ]),
        }
    }

    #[test]
    fn test_build_templated_url_search() {
        let cap = sample_capability("search", true, true);
        let result = build_templated_url(
            "https://api.example.com/v1/search?q=rust&page=1&limit=20",
            &cap,
        );
        assert!(result.contains("q={keyword}"));
        assert!(result.contains("page={page}"));
        assert!(result.contains("limit={limit}"));
    }

    #[test]
    fn test_build_templated_url_strips_volatile() {
        let cap = sample_capability("hot", false, false);
        let result = build_templated_url("https://api.example.com/v1/hot?wts=12345&foo=bar", &cap);
        assert!(!result.contains("wts"));
        assert!(result.contains("foo=bar"));
    }

    #[test]
    fn test_build_templated_url_no_params() {
        let cap = sample_capability("hot", false, false);
        let result = build_templated_url("https://api.example.com/v1/hot", &cap);
        assert_eq!(result, "https://api.example.com/v1/hot");
    }

    #[test]
    fn test_choose_endpoint_exact_match() {
        let cap = sample_capability("hot", false, false);
        let endpoints = vec![
            sample_endpoint(
                "https://api.example.com/v1/search",
                "https://api.example.com/v1/search?q=test",
            ),
            sample_endpoint(
                "https://api.example.com/v1/hot",
                "https://api.example.com/v1/hot",
            ),
        ];
        let chosen = choose_endpoint(&cap, &endpoints);
        assert!(chosen.is_some());
        assert_eq!(chosen.unwrap().pattern, "https://api.example.com/v1/hot");
    }

    #[test]
    fn test_choose_endpoint_fallback() {
        let cap = sample_capability("trending", false, false);
        let endpoints = vec![sample_endpoint(
            "https://api.example.com/v1/hot",
            "https://api.example.com/v1/hot",
        )];
        let chosen = choose_endpoint(&cap, &endpoints);
        assert!(chosen.is_some());
        assert_eq!(chosen.unwrap().pattern, "https://api.example.com/v1/hot");
    }

    #[test]
    fn test_choose_endpoint_empty() {
        let cap = sample_capability("hot", false, false);
        let endpoints: Vec<AnalyzedEndpoint> = vec![];
        let chosen = choose_endpoint(&cap, &endpoints);
        assert!(chosen.is_none());
    }

    #[test]
    fn test_build_command_config() {
        let cap = sample_capability("search", true, true);
        let ep = sample_endpoint(
            "https://api.example.com/v1/search",
            "https://api.example.com/v1/search?q=rust&page=1&limit=20",
        );
        let config = build_command_config("example", "api.example.com", &cap, &ep);

        assert_eq!(config.site, "example");
        assert_eq!(config.name, "search");
        assert_eq!(config.domain, "api.example.com");
        assert_eq!(config.strategy, "public");
        assert_eq!(config.method, "GET");
        assert!(config.endpoint.contains("{keyword}"));
        assert!(config.args.contains_key("keyword"));
        assert!(config.args.contains_key("limit"));
        assert!(config.args.contains_key("page"));
        assert!(config.fields.contains_key("title"));
    }

    #[test]
    fn test_render_synthesize_summary_empty() {
        let result = SynthesizeResult {
            site: "test".to_string(),
            out_dir: "/tmp/test".to_string(),
            candidates: vec![],
        };
        let summary = render_synthesize_summary(&result);
        assert!(summary.contains("No command candidates"));
    }

    #[test]
    fn test_render_synthesize_summary_with_candidates() {
        let result = SynthesizeResult {
            site: "example".to_string(),
            out_dir: "/tmp/example".to_string(),
            candidates: vec![
                CandidateSummary {
                    name: "hot".to_string(),
                    path: "/tmp/example/hot.json".to_string(),
                    strategy: "public".to_string(),
                },
                CandidateSummary {
                    name: "search".to_string(),
                    path: "/tmp/example/search.json".to_string(),
                    strategy: "header".to_string(),
                },
            ],
        };
        let summary = render_synthesize_summary(&result);
        assert!(summary.contains("Commands (2)"));
        assert!(summary.contains("hot [public]"));
        assert!(summary.contains("search [header]"));
    }
}
