use serde::Serialize;
use tail_fin_common::BrowserSession;
use tail_fin_common::TailFinError;

use crate::constants::normalize_goal;
use crate::explore::{explore_url, ExploreResult};
use crate::synthesize::{synthesize, CandidateSummary};

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

/// Result of the full generate pipeline (explore + synthesize + select).
#[derive(Debug, Clone, Serialize)]
pub struct GenerateResult {
    pub ok: bool,
    pub site: String,
    pub goal: Option<String>,
    pub selected_command: Option<String>,
    pub explore: ExploreResult,
    pub candidates: Vec<CandidateSummary>,
}

// ---------------------------------------------------------------------------
// Candidate selection
// ---------------------------------------------------------------------------

/// Select the best candidate matching the given goal.
///
/// 1. Exact match on normalized goal.
/// 2. Partial match (candidate name contains normalized goal).
/// 3. Fallback to first candidate.
pub fn select_candidate<'a>(
    candidates: &'a [CandidateSummary],
    goal: Option<&str>,
) -> Option<&'a CandidateSummary> {
    if candidates.is_empty() {
        return None;
    }

    if let Some(g) = goal {
        let normalized = normalize_goal(g).unwrap_or(g);
        let lower = normalized.to_lowercase();

        // Exact match.
        if let Some(c) = candidates.iter().find(|c| c.name.to_lowercase() == lower) {
            return Some(c);
        }

        // Partial match.
        if let Some(c) = candidates
            .iter()
            .find(|c| c.name.to_lowercase().contains(&lower))
        {
            return Some(c);
        }
    }

    // Fallback to first.
    candidates.first()
}

// ---------------------------------------------------------------------------
// Main generate function
// ---------------------------------------------------------------------------

/// Run the full generation pipeline: explore a URL, synthesize command configs,
/// and select the best candidate matching the user's goal.
pub async fn generate_cli(
    session: &BrowserSession,
    url: &str,
    goal: Option<&str>,
    site_override: Option<&str>,
) -> Result<GenerateResult, TailFinError> {
    // Normalize goal for matching.
    let normalized_goal = goal.and_then(normalize_goal);

    // Step 1: Explore the URL.
    let explore_result = explore_url(session, url, site_override, goal, 10).await?;
    let site = explore_result.site.clone();

    // Step 2: Synthesize command configs.
    let synth_result = synthesize(&site)?;

    // Step 3: Select best candidate.
    let selected = select_candidate(&synth_result.candidates, goal);
    let selected_name = selected.map(|c| c.name.clone());

    Ok(GenerateResult {
        ok: !synth_result.candidates.is_empty(),
        site,
        goal: normalized_goal.map(|s| s.to_string()),
        selected_command: selected_name,
        explore: explore_result,
        candidates: synth_result.candidates,
    })
}

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

/// Render a human-readable summary of the generate result.
pub fn render_generate_summary(result: &GenerateResult) -> String {
    let mut lines = Vec::new();

    if result.ok {
        lines.push(format!("Generated commands for site: {}", result.site));
    } else {
        lines.push(format!(
            "No commands could be generated for site: {}",
            result.site
        ));
    }

    if let Some(goal) = &result.goal {
        lines.push(format!("Goal: {}", goal));
    }

    if let Some(cmd) = &result.selected_command {
        lines.push(format!("Selected command: {}", cmd));
    }

    lines.push(String::new());
    lines.push(format!(
        "Explore: {} endpoints, {} API",
        result.explore.endpoint_count, result.explore.api_endpoint_count
    ));

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

    lines.join("\n")
}

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

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

    fn sample_candidates() -> Vec<CandidateSummary> {
        vec![
            CandidateSummary {
                name: "hot".to_string(),
                path: "/tmp/hot.json".to_string(),
                strategy: "public".to_string(),
            },
            CandidateSummary {
                name: "search".to_string(),
                path: "/tmp/search.json".to_string(),
                strategy: "header".to_string(),
            },
            CandidateSummary {
                name: "feed".to_string(),
                path: "/tmp/feed.json".to_string(),
                strategy: "cookie".to_string(),
            },
        ]
    }

    #[test]
    fn test_select_candidate_exact_match() {
        let candidates = sample_candidates();
        let selected = select_candidate(&candidates, Some("hot"));
        assert!(selected.is_some());
        assert_eq!(selected.unwrap().name, "hot");
    }

    #[test]
    fn test_select_candidate_normalized() {
        let candidates = sample_candidates();
        // "trending" normalizes to "hot".
        let selected = select_candidate(&candidates, Some("trending"));
        assert!(selected.is_some());
        assert_eq!(selected.unwrap().name, "hot");
    }

    #[test]
    fn test_select_candidate_partial_match() {
        let candidates = sample_candidates();
        // "searc" partially matches "search".
        let selected = select_candidate(&candidates, Some("searc"));
        assert!(selected.is_some());
        assert_eq!(selected.unwrap().name, "search");
    }

    #[test]
    fn test_select_candidate_fallback() {
        let candidates = sample_candidates();
        let selected = select_candidate(&candidates, Some("nonexistent"));
        assert!(selected.is_some());
        assert_eq!(selected.unwrap().name, "hot"); // first candidate
    }

    #[test]
    fn test_select_candidate_no_goal() {
        let candidates = sample_candidates();
        let selected = select_candidate(&candidates, None);
        assert!(selected.is_some());
        assert_eq!(selected.unwrap().name, "hot");
    }

    #[test]
    fn test_select_candidate_empty() {
        let candidates: Vec<CandidateSummary> = vec![];
        let selected = select_candidate(&candidates, Some("hot"));
        assert!(selected.is_none());
    }

    #[test]
    fn test_render_generate_summary_ok() {
        use crate::explore::ExploreResult;

        let result = GenerateResult {
            ok: true,
            site: "example".to_string(),
            goal: Some("hot".to_string()),
            selected_command: Some("hot".to_string()),
            explore: ExploreResult {
                site: "example".to_string(),
                target_url: "https://example.com".to_string(),
                final_url: "https://example.com/".to_string(),
                title: "Example".to_string(),
                top_strategy: "public".to_string(),
                endpoint_count: 15,
                api_endpoint_count: 3,
                capabilities: vec![],
                auth_indicators: vec![],
                out_dir: "/tmp/explore".to_string(),
            },
            candidates: sample_candidates(),
        };

        let summary = render_generate_summary(&result);
        assert!(summary.contains("Generated commands for site: example"));
        assert!(summary.contains("Goal: hot"));
        assert!(summary.contains("Selected command: hot"));
        assert!(summary.contains("Candidates (3)"));
    }

    #[test]
    fn test_render_generate_summary_no_candidates() {
        use crate::explore::ExploreResult;

        let result = GenerateResult {
            ok: false,
            site: "example".to_string(),
            goal: None,
            selected_command: None,
            explore: ExploreResult {
                site: "example".to_string(),
                target_url: "https://example.com".to_string(),
                final_url: "https://example.com/".to_string(),
                title: "Example".to_string(),
                top_strategy: "public".to_string(),
                endpoint_count: 0,
                api_endpoint_count: 0,
                capabilities: vec![],
                auth_indicators: vec![],
                out_dir: "/tmp/explore".to_string(),
            },
            candidates: vec![],
        };

        let summary = render_generate_summary(&result);
        assert!(summary.contains("No commands could be generated"));
        assert!(summary.contains("No candidates"));
    }
}
