use anyhow::Result;
use clap::Args;
use colored::*;

use std::fs;
use std::path::{Path, PathBuf};

use crate::server::{models::insight, services::similarity};

// Semantic similarity threshold for meaningful results
const SEMANTIC_SIMILARITY_THRESHOLD: f32 = 0.2;

// Default terminal width for text wrapping
const DEFAULT_TERMINAL_WIDTH: usize = 80;

#[derive(Debug)]
pub struct SearchResult {
    pub topic: String,
    pub name: String,
    pub overview: String,
    pub details: String,
    pub score: f32, // number of matching terms
}

/// Search configuration options
#[derive(Args)]
pub struct SearchCommandOptions {
    /// Optional topic to restrict search to
    #[arg(short, long)]
    pub topic: Option<String>,
    /// Case-sensitive search
    #[arg(short, long)]
    pub case_sensitive: bool,
    /// Search only in overview sections
    #[arg(short, long)]
    pub overview_only: bool,
    /// Use exact term matching only
    #[arg(short, long)]
    pub exact: bool,
    /// Use semantic search (term matching + jaccard similarity, no embedding)
    #[arg(short, long)]
    pub semantic: bool,
}

pub struct SearchOptions {
    pub topic: Option<String>,
    pub case_sensitive: bool,
    pub overview_only: bool,
    pub exact: bool,
    pub semantic: bool,
}

impl SearchOptions {
    pub fn from(options: &SearchCommandOptions) -> Self {
        Self {
            topic: options.topic.clone(),
            case_sensitive: options.case_sensitive,
            overview_only: options.overview_only,
            exact: options.exact,
            semantic: options.semantic,
        }
    }
}

pub fn search(terms: &[String], options: &SearchOptions) -> Result<Vec<SearchResult>> {
    let mut results = Vec::new();

    // Include exact term matching if not in semantic-only mode
    if !options.semantic {
        results.extend(search_topic(terms, get_exact_match, 0.0, options)?);
    }

    // Include semantic search if not in exact-only mode
    if !options.exact {
        results.extend(search_topic(
            terms,
            get_semantic_match,
            SEMANTIC_SIMILARITY_THRESHOLD,
            options,
        )?);
    }

    // Note: Embedding search is handled asynchronously in the server handler
    // and merged with these results there

    results.sort_by(|a, b| {
        b.score
            .partial_cmp(&a.score)
            .unwrap_or(std::cmp::Ordering::Equal)
            .then_with(|| a.topic.cmp(&b.topic).then_with(|| a.name.cmp(&b.name)))
    });

    // Deduplicate by keeping only the first occurrence of each (topic, name) pair
    // Since we sorted by score descending, the first occurrence will be the highest scoring
    let mut seen = std::collections::HashSet::new();
    results.retain(|result| {
        let key = (result.topic.clone(), result.name.clone());
        seen.insert(key)
    });

    Ok(results)
}

/// Search a topic for matches based on a search strategy
fn search_topic(
    terms: &[String],
    search_strategy: fn(&insight::Insight, &[String], &SearchOptions) -> f32,
    threshold: f32,
    options: &SearchOptions,
) -> Result<Vec<SearchResult>> {
    let mut results = Vec::new();

    let insights_dir = insight::get_valid_insights_dir()?;
    let search_paths = get_search_paths(&insights_dir, options.topic.as_deref())?;

    for topic_path in search_paths {
        for entry in fs::read_dir(&topic_path)? {
            let entry = entry?;
            let path = entry.path();

            if insight::is_insight_file(&path) {
                let insight = insight::load_from_path(&path)?;
                if let Ok(Some(result)) =
                    search_insight(&insight, search_strategy, terms, threshold, options)
                {
                    results.push(result);
                }
            }
        }
    }

    Ok(results)
}

fn search_insight(
    insight: &insight::Insight,
    search_strategy: fn(&insight::Insight, &[String], &SearchOptions) -> f32,
    terms: &[String],
    threshold: f32,
    options: &SearchOptions,
) -> Result<Option<SearchResult>> {
    let score = search_strategy(insight, terms, options);
    if score > threshold {
        Ok(Some(SearchResult {
            topic: insight.topic.to_string(),
            name: insight.name.to_string(),
            overview: insight.overview.to_string(),
            details: insight.details.to_string(),
            score,
        }))
    } else {
        Ok(None)
    }
}

fn get_normalized_content(insight: &insight::Insight, options: &SearchOptions) -> String {
    if options.overview_only {
        format!("{} {} {}", insight.topic, insight.name, insight.overview)
    } else {
        format!(
            "{} {} {} {}",
            insight.topic, insight.name, insight.overview, insight.details
        )
    }
}

fn get_normalized_terms(terms: &[String], options: &SearchOptions) -> Vec<String> {
    if options.case_sensitive {
        terms.to_vec()
    } else {
        terms
            .iter()
            .map(|t| t.to_lowercase())
            .collect::<Vec<String>>()
    }
}

fn get_exact_match(insight: &insight::Insight, terms: &[String], options: &SearchOptions) -> f32 {
    let normalized_content = get_normalized_content(insight, options);
    let normalized_terms = get_normalized_terms(terms, options);

    normalized_terms
        .iter()
        .map(|term| normalized_content.matches(term).count())
        .sum::<usize>() as f32
}

fn get_semantic_match(
    insight: &insight::Insight,
    terms: &[String],
    options: &SearchOptions,
) -> f32 {
    let normalized_content = get_normalized_content(insight, options);
    let normalized_terms = get_normalized_terms(terms, options);

    similarity::semantic(&normalized_terms.into_iter().collect(), &normalized_content)
}

/// Highlight search terms
fn highlight_keywords(text: &str, terms: &[String]) -> String {
    let mut result = text.to_string();

    let mut sorted = terms.to_vec();
    sorted.sort_by_key(|b| std::cmp::Reverse(b.len()));

    for term in sorted {
        if term.is_empty() {
            continue;
        }

        let term_lower = term.to_lowercase();
        let mut highlighted = String::new();
        let mut end = 0;

        let result_lower = result.to_lowercase();
        let mut start = 0;

        while let Some(pos) = result_lower[start..].find(&term_lower) {
            let abs_pos = start + pos;

            highlighted.push_str(&result[end..abs_pos]);

            let match_text = &result[abs_pos..abs_pos + term.len()];
            highlighted.push_str(&match_text.yellow().bold().to_string());

            end = abs_pos + term.len();
            start = end;
        }

        highlighted.push_str(&result[end..]);
        result = highlighted;
    }

    result
}

/// Build search paths based on topic filter
fn get_search_paths(insights_root: &Path, topic_filter: Option<&str>) -> Result<Vec<PathBuf>> {
    if let Some(topic) = topic_filter {
        Ok(vec![insights_root.join(topic)])
    } else {
        Ok(insight::get_topics()?
            .into_iter()
            .map(|topic| insights_root.join(topic))
            .collect())
    }
}

/// Display the combined search results
pub fn display_results(results: &[SearchResult], terms: &[String], overview_only: bool) {
    if results.is_empty() {
        println!("No matches found for: {}", terms.join(" ").yellow());
    } else {
        for result in results {
            display_single_result(result, terms, overview_only);
        }
    }
}

/// Display a single search result with keyword highlighting
fn display_single_result(result: &SearchResult, terms: &[String], overview_only: bool) {
    let header = format!(
        "=== {}/{} ===",
        result.topic.blue().bold(),
        result.name.yellow().bold()
    );

    println!("{header}");

    // Wrap and display the content with proper formatting
    let wrap_with = if header.len() < DEFAULT_TERMINAL_WIDTH {
        DEFAULT_TERMINAL_WIDTH
    } else {
        header.len()
    };

    let content = if overview_only {
        result.overview.to_string()
    } else {
        format!("{}\n\n{}", result.overview, result.details)
    };

    let highlighted_content = highlight_keywords(&content, terms);
    let wrapped_lines = wrap_text(&highlighted_content, wrap_with);
    for line in wrapped_lines {
        println!("{line}");
    }
    println!();
}

/// Wrap text to fit within a specified width
fn wrap_text(text: &str, width: usize) -> Vec<String> {
    let mut lines = Vec::new();

    for paragraph in text.split('\n') {
        if paragraph.trim().is_empty() {
            lines.push(String::new());
            continue;
        }

        let words: Vec<&str> = paragraph.split_whitespace().collect();
        let mut current_line = String::new();

        for word in words {
            if current_line.is_empty() {
                current_line = word.to_string();
            } else if current_line.len() + 1 + word.len() <= width {
                current_line.push(' ');
                current_line.push_str(word);
            } else {
                lines.push(current_line);
                current_line = word.to_string();
            }
        }

        if !current_line.is_empty() {
            lines.push(current_line);
        }
    }

    lines
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::server::models::insight::Insight;
    use colored::control;

    // Mock insight for testing
    fn create_test_insight() -> Insight {
        Insight::new(
            "test_topic".to_string(),
            "test_insight".to_string(),
            "This is a test overview with some content".to_string(),
            "This is detailed content with more information for testing purposes".to_string(),
        )
    }

    #[test]
    fn test_search_options_from_command_options() {
        let cmd_options = SearchCommandOptions {
            topic: Some("test_topic".to_string()),
            case_sensitive: true,
            overview_only: true,
            exact: false,
            semantic: true,
        };

        let options = SearchOptions::from(&cmd_options);

        assert_eq!(options.topic, Some("test_topic".to_string()));
        assert!(options.case_sensitive);
        assert!(options.overview_only);
        assert!(!options.exact);
        assert!(options.semantic);
    }

    #[test]
    fn test_get_normalized_content_overview_only() {
        let insight = create_test_insight();
        let options = SearchOptions {
            topic: None,
            case_sensitive: false,
            overview_only: true,
            exact: false,
            semantic: false,
        };

        let content = get_normalized_content(&insight, &options);
        assert_eq!(
            content,
            "test_topic test_insight This is a test overview with some content"
        );
    }

    #[test]
    fn test_get_normalized_content_full_content() {
        let insight = create_test_insight();
        let options = SearchOptions {
            topic: None,
            case_sensitive: false,
            overview_only: false,
            exact: false,
            semantic: false,
        };

        let content = get_normalized_content(&insight, &options);
        let expected = "test_topic test_insight This is a test overview with some content This is detailed content with more information for testing purposes";
        assert_eq!(content, expected);
    }

    #[test]
    fn test_get_normalized_terms_case_sensitive() {
        let terms = vec!["Test".to_string(), "CONTENT".to_string()];
        let options = SearchOptions {
            topic: None,
            case_sensitive: true,
            overview_only: false,
            exact: false,
            semantic: false,
        };

        let normalized = get_normalized_terms(&terms, &options);
        assert_eq!(normalized, vec!["Test".to_string(), "CONTENT".to_string()]);
    }

    #[test]
    fn test_get_normalized_terms_case_insensitive() {
        let terms = vec!["Test".to_string(), "CONTENT".to_string()];
        let options = SearchOptions {
            topic: None,
            case_sensitive: false,
            overview_only: false,
            exact: false,
            semantic: false,
        };

        let normalized = get_normalized_terms(&terms, &options);
        assert_eq!(normalized, vec!["test".to_string(), "content".to_string()]);
    }

    #[test]
    fn test_get_exact_match_single_term() {
        let insight = create_test_insight();
        let terms = vec!["test".to_string()];
        let options = SearchOptions {
            topic: None,
            case_sensitive: false,
            overview_only: false,
            exact: true,
            semantic: false,
        };

        let score = get_exact_match(&insight, &terms, &options);
        // "test" appears in topic, name, and content multiple times
        assert!(score > 2.0); // Should find multiple matches
    }

    #[test]
    fn test_get_exact_match_multiple_terms() {
        let insight = create_test_insight();
        let terms = vec!["test".to_string(), "content".to_string()];
        let options = SearchOptions {
            topic: None,
            case_sensitive: false,
            overview_only: false,
            exact: true,
            semantic: false,
        };

        let score = get_exact_match(&insight, &terms, &options);
        // Score should be sum of matches for both terms
        assert!(score >= 4.0);
    }

    #[test]
    fn test_get_exact_match_case_sensitive() {
        let insight = create_test_insight();
        let terms = vec!["Test".to_string()]; // Capital T
        let options = SearchOptions {
            topic: None,
            case_sensitive: true,
            overview_only: false,
            exact: true,
            semantic: false,
        };

        let score = get_exact_match(&insight, &terms, &options);
        // Should find fewer matches due to case sensitivity
        let insensitive_score = get_exact_match(
            &insight,
            &["test".to_string()],
            &SearchOptions {
                topic: None,
                case_sensitive: false,
                overview_only: false,
                exact: true,
                semantic: false,
            },
        );

        assert!(insensitive_score >= score); // Case insensitive should find more matches
    }

    #[test]
    fn test_get_exact_match_no_matches() {
        let insight = create_test_insight();
        let terms = vec!["nonexistent".to_string()];
        let options = SearchOptions {
            topic: None,
            case_sensitive: false,
            overview_only: false,
            exact: true,
            semantic: false,
        };

        let score = get_exact_match(&insight, &terms, &options);
        assert_eq!(score, 0.0);
    }

    #[test]
    fn test_search_insight_above_threshold() {
        let insight = create_test_insight();
        let terms = vec!["test".to_string()];
        let options = SearchOptions {
            topic: None,
            case_sensitive: false,
            overview_only: false,
            exact: true,
            semantic: false,
        };

        let result = search_insight(&insight, get_exact_match, &terms, 0.0, &options).unwrap();

        assert!(result.is_some());
        let search_result = result.unwrap();
        assert_eq!(search_result.topic, "test_topic");
        assert_eq!(search_result.name, "test_insight");
        assert_eq!(search_result.overview, insight.overview);
        assert_eq!(search_result.details, insight.details);
        assert!(search_result.score > 0.0);
    }

    #[test]
    fn test_search_insight_below_threshold() {
        let insight = create_test_insight();
        let terms = vec!["nonexistent".to_string()];
        let options = SearchOptions {
            topic: None,
            case_sensitive: false,
            overview_only: false,
            exact: true,
            semantic: false,
        };

        let result = search_insight(&insight, get_exact_match, &terms, 1.0, &options).unwrap();
        assert!(result.is_none());
    }

    #[test]
    fn test_highlight_keywords_basic() {
        // Force color output for this test
        control::set_override(true);

        let text = "This is a test string with test content";
        let terms = vec!["test".to_string()];

        let highlighted = highlight_keywords(text, &terms);
        // Should contain ANSI color codes for highlighting
        assert!(highlighted.contains("\x1b[")); // ANSI escape codes
        assert!(highlighted.contains("test")); // Original text should still be there

        // Reset to default behavior
        control::unset_override();
    }

    #[test]
    fn test_highlight_keywords_multiple_terms() {
        control::set_override(true);

        let text = "This is a test string with test content and more content";
        let terms = vec!["test".to_string(), "content".to_string()];

        let highlighted = highlight_keywords(text, &terms);
        assert!(highlighted.contains("\x1b[")); // ANSI escape codes
        assert!(highlighted.contains("test"));
        assert!(highlighted.contains("content"));

        control::unset_override();
    }

    #[test]
    fn test_highlight_keywords_empty_terms() {
        let text = "This is a test string";
        let terms = vec![];

        let highlighted = highlight_keywords(text, &terms);
        assert_eq!(highlighted, text); // Should return unchanged
    }

    #[test]
    fn test_highlight_keywords_empty_term() {
        let text = "This is a test string";
        let terms = vec!["".to_string()];

        let highlighted = highlight_keywords(text, &terms);
        assert_eq!(highlighted, text); // Should return unchanged
    }

    #[test]
    fn test_highlight_keywords_case_insensitive() {
        control::set_override(true);

        let text = "This is a TEST string";
        let terms = vec!["test".to_string()];

        let highlighted = highlight_keywords(text, &terms);
        assert!(highlighted.contains("\x1b[")); // Should still highlight despite case difference

        control::unset_override();
    }

    #[test]
    fn test_get_search_paths_with_topic_filter() {
        use std::path::Path;

        let root = Path::new("/test/root");
        let topic_filter = Some("specific_topic");

        let paths = get_search_paths(root, topic_filter).unwrap();
        assert_eq!(paths.len(), 1);
        assert_eq!(paths[0], root.join("specific_topic"));
    }

    #[test]
    fn test_get_search_paths_without_topic_filter() {

        // This test would require mocking get_topics(), which is filesystem dependent
        // For now, we'll skip this as it's more integration than unit test
        // In a real scenario, we'd inject the topic list as a dependency
    }

    #[test]
    fn test_display_single_result() {
        let result = SearchResult {
            topic: "test_topic".to_string(),
            name: "test_insight".to_string(),
            overview: "Test overview".to_string(),
            details: "Test details".to_string(),
            score: 2.5,
        };

        let terms = vec!["test".to_string()];

        // This function prints to stdout, so we can't easily test the output
        // In a real scenario, we'd modify it to accept a writer parameter
        // For now, just ensure it doesn't panic
        display_single_result(&result, &terms, false);
    }

    #[test]
    fn test_display_results_empty() {
        let results: Vec<SearchResult> = vec![];
        let terms = vec!["test".to_string()];

        // Should not panic when displaying empty results
        display_results(&results, &terms, false);
    }

    #[test]
    fn test_display_results_with_data() {
        let results = vec![
            SearchResult {
                topic: "topic1".to_string(),
                name: "insight1".to_string(),
                overview: "Overview 1".to_string(),
                details: "Details 1".to_string(),
                score: 1.0,
            },
            SearchResult {
                topic: "topic2".to_string(),
                name: "insight2".to_string(),
                overview: "Overview 2".to_string(),
                details: "Details 2".to_string(),
                score: 2.0,
            },
        ];

        let terms = vec!["test".to_string()];

        // Should not panic when displaying results
        display_results(&results, &terms, false);
    }
}
