use crate::core::error::Result;
use crate::core::path_resolver::escape_like;
use crate::documents::SourceType;
use rusqlite::Connection;

const DEFAULT_LIMIT: usize = 20;
const MAX_LIMIT: usize = 100;

#[derive(Debug, Clone)]
pub enum PathFilter {
    Exact(String),
    Prefix(String),
}

#[derive(Debug, Clone, Default)]
pub struct SearchFilters {
    pub source_type: Option<SourceType>,
    pub author: Option<String>,
    pub project_id: Option<i64>,
    pub since: Option<i64>,
    pub updated_since: Option<i64>,
    pub labels: Vec<String>,
    pub path: Option<PathFilter>,
    pub limit: usize,
}

impl SearchFilters {
    pub fn has_any_filter(&self) -> bool {
        self.source_type.is_some()
            || self.author.is_some()
            || self.project_id.is_some()
            || self.since.is_some()
            || self.updated_since.is_some()
            || !self.labels.is_empty()
            || self.path.is_some()
    }

    pub fn clamp_limit(&self) -> usize {
        if self.limit == 0 {
            DEFAULT_LIMIT
        } else {
            self.limit.min(MAX_LIMIT)
        }
    }
}

pub fn apply_filters(
    conn: &Connection,
    document_ids: &[i64],
    filters: &SearchFilters,
) -> Result<Vec<i64>> {
    if document_ids.is_empty() {
        return Ok(Vec::new());
    }

    let ids_json = serde_json::to_string(document_ids)
        .map_err(|e| crate::core::error::LoreError::Other(e.to_string()))?;

    let mut sql = String::from(
        "SELECT d.id FROM json_each(?1) AS j JOIN documents d ON d.id = j.value WHERE 1=1",
    );
    let mut params: Vec<Box<dyn rusqlite::types::ToSql>> = vec![Box::new(ids_json)];
    let mut param_idx = 2;

    if let Some(ref st) = filters.source_type {
        sql.push_str(&format!(" AND d.source_type = ?{}", param_idx));
        params.push(Box::new(st.as_str().to_string()));
        param_idx += 1;
    }

    if let Some(ref author) = filters.author {
        sql.push_str(&format!(" AND d.author_username = ?{}", param_idx));
        params.push(Box::new(author.clone()));
        param_idx += 1;
    }

    if let Some(pid) = filters.project_id {
        sql.push_str(&format!(" AND d.project_id = ?{}", param_idx));
        params.push(Box::new(pid));
        param_idx += 1;
    }

    if let Some(since) = filters.since {
        sql.push_str(&format!(" AND d.created_at >= ?{}", param_idx));
        params.push(Box::new(since));
        param_idx += 1;
    }

    if let Some(updated_since) = filters.updated_since {
        sql.push_str(&format!(" AND d.updated_at >= ?{}", param_idx));
        params.push(Box::new(updated_since));
        param_idx += 1;
    }

    if !filters.labels.is_empty() {
        // SQLite has a default limit of 999 bind parameters.
        let max_labels = 900_usize.saturating_sub(param_idx);
        let label_slice = if filters.labels.len() > max_labels {
            &filters.labels[..max_labels]
        } else {
            &filters.labels
        };
        let placeholders: Vec<String> = (0..label_slice.len())
            .map(|i| format!("?{}", param_idx + i))
            .collect();
        sql.push_str(&format!(
            " AND EXISTS (SELECT 1 FROM document_labels dl WHERE dl.document_id = d.id AND dl.label_name IN ({}) GROUP BY dl.document_id HAVING COUNT(DISTINCT dl.label_name) = {})",
            placeholders.join(","),
            label_slice.len()
        ));
        for label in label_slice {
            params.push(Box::new(label.clone()));
            param_idx += 1;
        }
    }

    if let Some(ref path_filter) = filters.path {
        match path_filter {
            PathFilter::Exact(p) => {
                sql.push_str(&format!(
                    " AND EXISTS (SELECT 1 FROM document_paths dp WHERE dp.document_id = d.id AND dp.path = ?{})",
                    param_idx
                ));
                params.push(Box::new(p.clone()));
                param_idx += 1;
            }
            PathFilter::Prefix(p) => {
                let escaped = escape_like(p);
                sql.push_str(&format!(
                    " AND EXISTS (SELECT 1 FROM document_paths dp WHERE dp.document_id = d.id AND dp.path LIKE ?{} ESCAPE '\\')",
                    param_idx
                ));
                params.push(Box::new(format!("{}%", escaped)));
                param_idx += 1;
            }
        }
    }

    let limit = filters.clamp_limit();
    sql.push_str(&format!(" ORDER BY j.key LIMIT ?{}", param_idx));
    params.push(Box::new(limit as i64));

    let param_refs: Vec<&dyn rusqlite::types::ToSql> = params.iter().map(|p| p.as_ref()).collect();

    let mut stmt = conn.prepare(&sql)?;
    let ids = stmt
        .query_map(param_refs.as_slice(), |row| row.get::<_, i64>(0))?
        .collect::<std::result::Result<Vec<_>, _>>()?;

    Ok(ids)
}

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

    #[test]
    fn test_has_any_filter_default() {
        let f = SearchFilters::default();
        assert!(!f.has_any_filter());
    }

    #[test]
    fn test_has_any_filter_with_source_type() {
        let f = SearchFilters {
            source_type: Some(SourceType::Issue),
            ..Default::default()
        };
        assert!(f.has_any_filter());
    }

    #[test]
    fn test_has_any_filter_with_labels() {
        let f = SearchFilters {
            labels: vec!["bug".to_string()],
            ..Default::default()
        };
        assert!(f.has_any_filter());
    }

    #[test]
    fn test_limit_clamping_zero() {
        let f = SearchFilters {
            limit: 0,
            ..Default::default()
        };
        assert_eq!(f.clamp_limit(), 20);
    }

    #[test]
    fn test_limit_clamping_over_max() {
        let f = SearchFilters {
            limit: 200,
            ..Default::default()
        };
        assert_eq!(f.clamp_limit(), 100);
    }

    #[test]
    fn test_limit_clamping_normal() {
        let f = SearchFilters {
            limit: 50,
            ..Default::default()
        };
        assert_eq!(f.clamp_limit(), 50);
    }

    #[test]
    fn test_escape_like() {
        assert_eq!(escape_like("src/%.rs"), "src/\\%.rs");
        assert_eq!(escape_like("file_name"), "file\\_name");
        assert_eq!(escape_like("normal"), "normal");
        assert_eq!(escape_like("a\\b"), "a\\\\b");
    }

    #[test]
    fn test_empty_ids() {
        let f = SearchFilters::default();
        assert!(!f.has_any_filter());
    }
}
