use std::collections::HashMap;

use serde_json::Value;
use tail_fin_common::BrowserSession;
use tail_fin_common::TailFinError;

use crate::synthesize::CommandConfig;

// ---------------------------------------------------------------------------
// Load helpers
// ---------------------------------------------------------------------------

/// Load a command config from `~/.tail-fin/sites/<site>/<command>.json`.
pub fn load_command_config(site: &str, command: &str) -> Result<CommandConfig, TailFinError> {
    let path = tail_fin_common::tail_fin_dir()
        .join("sites")
        .join(site)
        .join(format!("{}.json", command));
    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()))
}

/// Scan `~/.tail-fin/sites/` and return a list of `(site, [command_names])`.
pub fn list_dynamic_sites() -> Vec<(String, Vec<String>)> {
    let sites_dir = tail_fin_common::tail_fin_dir().join("sites");
    let mut result = Vec::new();

    let entries = match std::fs::read_dir(&sites_dir) {
        Ok(e) => e,
        Err(_) => return result,
    };

    for entry in entries.flatten() {
        let path = entry.path();
        if !path.is_dir() {
            continue;
        }
        let site_name = match path.file_name().and_then(|n| n.to_str()) {
            Some(n) => n.to_string(),
            None => continue,
        };

        let mut commands = Vec::new();
        if let Ok(files) = std::fs::read_dir(&path) {
            for file in files.flatten() {
                let fname = file.file_name();
                let fname_str = fname.to_string_lossy();
                if fname_str.ends_with(".json") {
                    commands.push(fname_str.trim_end_matches(".json").to_string());
                }
            }
        }
        commands.sort();
        result.push((site_name, commands));
    }

    result.sort_by(|a, b| a.0.cmp(&b.0));
    result
}

// ---------------------------------------------------------------------------
// Execute
// ---------------------------------------------------------------------------

/// Execute a dynamically generated command by fetching data from the configured
/// endpoint using the browser session, navigating the response via `items_path`,
/// mapping fields, and slicing to the requested count.
pub async fn execute_command(
    session: &BrowserSession,
    config: &CommandConfig,
    args: &HashMap<String, String>,
) -> Result<Value, TailFinError> {
    // Determine requested count from args (limit or count key, default 20).
    let count: usize = args
        .get("limit")
        .or_else(|| args.get("count"))
        .and_then(|v| v.parse().ok())
        .unwrap_or(20);

    // Build URL from endpoint template: substitute {keyword}, {limit}, {page}.
    let mut url = config.endpoint.clone();
    for (key, value) in args {
        let placeholder = format!("{{{}}}", key);
        url = url.replace(&placeholder, value);
    }
    // Fill defaults for unsubstituted placeholders.
    if url.contains("{limit}") {
        url = url.replace("{limit}", &count.to_string());
    }
    if url.contains("{page}") {
        url = url.replace("{page}", "1");
    }
    if url.contains("{keyword}") {
        url = url.replace("{keyword}", "");
    }

    // Build the JavaScript fetch call.
    let url_escaped = serde_json::to_string(&url).unwrap_or_default();
    let credentials = match config.strategy.as_str() {
        "public" => "omit",
        _ => "include",
    };

    // Build CSRF setup for "header" strategy.
    let csrf_setup = if config.strategy == "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;
            }
        }
        if (csrf) headers['X-Csrf-Token'] = csrf;
        "#
    } else {
        ""
    };

    // Build items_path navigation chain.
    let path_chain = config
        .items_path
        .as_deref()
        .unwrap_or("")
        .split('.')
        .filter(|s| !s.is_empty())
        .map(|seg| format!("?.{}", seg))
        .collect::<Vec<_>>()
        .join("");

    // Build field mapping.
    let field_entries: Vec<String> = config
        .fields
        .iter()
        .map(|(field_path, role)| {
            let role_escaped = serde_json::to_string(role).unwrap_or_default();
            let path_parts: Vec<&str> = field_path.split('.').collect();
            let accessor = path_parts
                .iter()
                .map(|p| format!("?.{}", p))
                .collect::<Vec<_>>()
                .join("");
            format!("[{}]: item{}", role_escaped, accessor)
        })
        .collect();

    let field_map_js = if field_entries.is_empty() {
        "item".to_string()
    } else {
        format!("({{ {} }})", field_entries.join(", "))
    };

    let js = format!(
        r#"
(async () => {{
    try {{
        const headers = {{}};
        {csrf_setup}
        const resp = await fetch({url_escaped}, {{
            method: 'GET',
            credentials: '{credentials}',
            headers: headers
        }});
        if (!resp.ok) {{
            return JSON.stringify({{ __error: 'HTTP ' + resp.status }});
        }}
        const json = await resp.json();
        let items = json{path_chain};
        if (!Array.isArray(items)) {{
            return JSON.stringify({{ __error: 'items not found at path' }});
        }}
        const mapped = items.slice(0, {count}).map(item => {field_map_js});
        return JSON.stringify(mapped);
    }} catch (e) {{
        return JSON.stringify({{ __error: e.message }});
    }}
}})()
"#,
        csrf_setup = csrf_setup,
        url_escaped = url_escaped,
        credentials = credentials,
        path_chain = path_chain,
        count = count,
        field_map_js = field_map_js,
    );

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

    let result: Value = serde_json::from_str(&text)
        .map_err(|e| TailFinError::Parse(format!("failed to parse fetch result: {}", e)))?;

    // Check for __error in result.
    if let Some(err) = result.get("__error").and_then(|v| v.as_str()) {
        return Err(TailFinError::Api(err.to_string()));
    }

    Ok(result)
}

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

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

    fn sample_config(strategy: &str) -> CommandConfig {
        let mut args = HashMap::new();
        args.insert(
            "keyword".to_string(),
            ArgDef {
                arg_type: "String".to_string(),
                required: Some(true),
                default: None,
                description: Some("Search keyword".to_string()),
            },
        );
        args.insert(
            "limit".to_string(),
            ArgDef {
                arg_type: "u32".to_string(),
                required: Some(false),
                default: Some(Value::Number(20.into())),
                description: Some("Max items".to_string()),
            },
        );

        CommandConfig {
            site: "example".to_string(),
            name: "search".to_string(),
            description: "search data from example".to_string(),
            domain: "api.example.com".to_string(),
            strategy: strategy.to_string(),
            endpoint: "https://api.example.com/v1/search?q={keyword}&limit={limit}".to_string(),
            method: "GET".to_string(),
            items_path: Some("data.list".to_string()),
            fields: HashMap::from([
                ("title".to_string(), "title".to_string()),
                ("author".to_string(), "author".to_string()),
            ]),
            args,
        }
    }

    #[test]
    fn test_list_dynamic_sites_no_dir() {
        // When the sites directory doesn't exist, should return empty.
        let result = list_dynamic_sites();
        // This is environment-dependent, just verify it doesn't panic.
        let _ = result;
    }

    #[test]
    fn test_url_template_substitution() {
        let config = sample_config("public");
        let mut args = HashMap::new();
        args.insert("keyword".to_string(), "rust".to_string());
        args.insert("limit".to_string(), "10".to_string());

        let mut url = config.endpoint.clone();
        for (key, value) in &args {
            let placeholder = format!("{{{}}}", key);
            url = url.replace(&placeholder, value);
        }
        assert_eq!(url, "https://api.example.com/v1/search?q=rust&limit=10");
    }

    #[test]
    fn test_url_template_defaults() {
        let config = sample_config("public");
        let args: HashMap<String, String> = HashMap::new();

        let mut url = config.endpoint.clone();
        for (key, value) in &args {
            let placeholder = format!("{{{}}}", key);
            url = url.replace(&placeholder, value);
        }
        // Fill defaults.
        if url.contains("{limit}") {
            url = url.replace("{limit}", "20");
        }
        if url.contains("{keyword}") {
            url = url.replace("{keyword}", "");
        }
        assert_eq!(url, "https://api.example.com/v1/search?q=&limit=20");
    }

    #[test]
    fn test_sample_config_structure() {
        let config = sample_config("header");
        assert_eq!(config.strategy, "header");
        assert_eq!(config.site, "example");
        assert!(config.fields.contains_key("title"));
        assert!(config.args.contains_key("keyword"));
    }
}
