//! Content Index Generator
//!
//! Generates pre-computed JSON indexes for fast content filtering by category,
//! tag, and other attributes. These indexes enable O(1) lookup times for
//! content queries.

use std::collections::HashMap;
use std::path::{Path, PathBuf};

use anyhow::Result;
use rustelo_core_lib::fluent::PageContent;
use serde::{Deserialize, Serialize};
use tokio::fs;
use tracing::{debug, info, warn};

use super::file_loader::FileContentLoader;

/// Index entry for a single post
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostIndexEntry {
    pub id: String,
    pub slug: String,
    pub title: String,
    pub categories: Vec<String>,
    pub tags: Vec<String>,
    pub language: String,
    pub translations: HashMap<String, String>,
    pub published_at: Option<String>,
    pub updated_at: Option<String>,
    pub excerpt: Option<String>,
}

/// Category index with post counts and metadata
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CategoryIndex {
    pub name: String,
    pub post_ids: Vec<String>,
    pub post_count: usize,
    pub css_class: Option<String>,
    pub css_style: Option<String>,
    pub description: Option<String>,
}

/// Tag index with post counts
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TagIndex {
    pub name: String,
    pub post_ids: Vec<String>,
    pub post_count: usize,
}

/// Main content index structure
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContentIndex {
    pub content_type: String,
    pub language: String,
    pub categories: HashMap<String, CategoryIndex>,
    pub tags: HashMap<String, TagIndex>,
    pub posts: HashMap<String, PostIndexEntry>,
    pub total_posts: usize,
    pub last_updated: String,
}

/// Global index containing all content types and languages
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GlobalContentIndex {
    pub indexes: HashMap<String, HashMap<String, ContentIndex>>, // type -> lang -> index
    pub available_languages: Vec<String>,
    pub available_types: Vec<String>,
    pub total_content_count: usize,
    pub last_updated: String,
}

/// Content indexer for generating JSON indexes
/// Supports both batch generation and on-demand incremental updates
pub struct ContentIndexer {
    content_root: PathBuf,
    output_dir: PathBuf,
    file_loader: FileContentLoader,
    enable_compression: bool,
    /// Cache for loaded indexes to avoid repeated disk I/O
    index_cache: tokio::sync::RwLock<HashMap<String, ContentIndex>>,
    /// Whether to enable in-memory caching for performance
    enable_caching: bool,
}

impl ContentIndexer {
    /// Create a new content indexer with default settings
    pub fn new<P: AsRef<Path>>(content_root: P, output_dir: P) -> Self {
        let content_root = content_root.as_ref().to_path_buf();
        let output_dir = output_dir.as_ref().to_path_buf();
        let file_loader = FileContentLoader::new(&content_root);

        Self {
            content_root,
            output_dir,
            file_loader,
            enable_compression: true,
            index_cache: tokio::sync::RwLock::new(HashMap::new()),
            enable_caching: true,
        }
    }

    /// Create a new content indexer with custom settings
    pub fn with_config<P: AsRef<Path>>(
        content_root: P,
        output_dir: P,
        enable_compression: bool,
        enable_caching: bool,
    ) -> Self {
        let content_root = content_root.as_ref().to_path_buf();
        let output_dir = output_dir.as_ref().to_path_buf();
        let file_loader = FileContentLoader::new(&content_root);

        Self {
            content_root,
            output_dir,
            file_loader,
            enable_compression,
            index_cache: tokio::sync::RwLock::new(HashMap::new()),
            enable_caching,
        }
    }

    /// Generate all indexes for all content types and languages
    pub async fn generate_all_indexes(&self) -> Result<GlobalContentIndex> {
        info!("Starting index generation for all content");

        let mut global_index = GlobalContentIndex {
            indexes: HashMap::new(),
            available_languages: Vec::new(),
            available_types: Vec::new(),
            total_content_count: 0,
            last_updated: chrono::Utc::now().to_rfc3339(),
        };

        // Discover available content types
        let content_types = self.discover_content_types().await?;
        global_index.available_types = content_types.clone();

        // Generate indexes for each content type
        for content_type in &content_types {
            let mut type_indexes = HashMap::new();

            // Discover languages for this content type
            let languages = self.discover_languages_for_type(content_type).await?;

            // Update global language list
            for lang in &languages {
                if !global_index.available_languages.contains(lang) {
                    global_index.available_languages.push(lang.clone());
                }
            }

            // Generate index for each language
            for language in &languages {
                match self.generate_index(content_type, language).await {
                    Ok(index) => {
                        global_index.total_content_count += index.total_posts;
                        type_indexes.insert(language.clone(), index);
                    }
                    Err(e) => {
                        warn!(
                            "Failed to generate index for {}/{}: {}",
                            content_type, language, e
                        );
                    }
                }
            }

            if !type_indexes.is_empty() {
                global_index
                    .indexes
                    .insert(content_type.clone(), type_indexes);
            }
        }

        // Sort languages for consistency
        global_index.available_languages.sort();

        // Save global index
        self.save_global_index(&global_index).await?;

        info!(
            "Index generation complete: {} types, {} languages, {} total posts",
            global_index.available_types.len(),
            global_index.available_languages.len(),
            global_index.total_content_count
        );

        Ok(global_index)
    }

    /// Generate index for a specific content type and language
    pub async fn generate_index(&self, content_type: &str, language: &str) -> Result<ContentIndex> {
        debug!("Generating index for {}/{}", content_type, language);

        let mut index = ContentIndex {
            content_type: content_type.to_string(),
            language: language.to_string(),
            categories: HashMap::new(),
            tags: HashMap::new(),
            posts: HashMap::new(),
            total_posts: 0,
            last_updated: chrono::Utc::now().to_rfc3339(),
        };

        // Load all content for this type and language
        let contents = self
            .file_loader
            .load_by_type_and_language(content_type, language)?;

        for content in contents {
            // Create post index entry
            let post_entry = self.create_post_entry(&content, language);
            let post_id = post_entry.id.clone();

            // Update category indexes
            for category in &post_entry.categories {
                index
                    .categories
                    .entry(category.clone())
                    .or_insert_with(|| CategoryIndex {
                        name: category.clone(),
                        post_ids: Vec::new(),
                        post_count: 0,
                        css_class: None,
                        css_style: None,
                        description: None,
                    })
                    .post_ids
                    .push(post_id.clone());
            }

            // Update tag indexes
            for tag in &post_entry.tags {
                index
                    .tags
                    .entry(tag.clone())
                    .or_insert_with(|| TagIndex {
                        name: tag.clone(),
                        post_ids: Vec::new(),
                        post_count: 0,
                    })
                    .post_ids
                    .push(post_id.clone());
            }

            // Add post to index
            index.posts.insert(post_id, post_entry);
            index.total_posts += 1;
        }

        // Update post counts
        for category in index.categories.values_mut() {
            category.post_count = category.post_ids.len();
        }
        for tag in index.tags.values_mut() {
            tag.post_count = tag.post_ids.len();
        }

        // Load category metadata if available
        self.load_category_metadata(&mut index, content_type, language)
            .await?;

        // Save index to file
        self.save_index(&index).await?;

        debug!(
            "Generated index for {}/{}: {} posts, {} categories, {} tags",
            content_type,
            language,
            index.total_posts,
            index.categories.len(),
            index.tags.len()
        );

        Ok(index)
    }

    /// Create a post index entry from PageContent
    fn create_post_entry(&self, content: &PageContent, language: &str) -> PostIndexEntry {
        // Discover translations for this post
        let translations = self.file_loader.discover_translations(
            &format!("{:?}", content.content_type).to_lowercase(),
            &content.slug,
        );

        PostIndexEntry {
            id: content.slug.clone(),
            slug: content.slug.clone(),
            title: content.title.clone(),
            categories: content
                .category
                .as_ref()
                .map(|c| vec![c.clone()])
                .unwrap_or_default(),
            tags: content.tags.clone(),
            language: language.to_string(),
            translations,
            published_at: content.published_at.map(|dt| dt.to_rfc3339()),
            updated_at: Some(content.updated_at.to_rfc3339()),
            excerpt: content.excerpt.clone(),
        }
    }

    /// Load category metadata from meta.toml files
    async fn load_category_metadata(
        &self,
        index: &mut ContentIndex,
        content_type: &str,
        language: &str,
    ) -> Result<()> {
        let category_names: Vec<String> = index.categories.keys().cloned().collect();
        for category_name in category_names {
            if let Ok(meta) =
                self.file_loader
                    .get_category_metadata(content_type, language, &category_name)
            {
                if let Some(category) = index.categories.get_mut(&category_name) {
                    category.css_class = Some(meta.css_class);
                    category.css_style = Some(meta.css_style);
                    category.description = Some(meta.description);
                }
            }
        }
        Ok(())
    }

    /// Save index to JSON file
    async fn save_index(&self, index: &ContentIndex) -> Result<()> {
        let file_path = self
            .output_dir
            .join(&index.content_type)
            .join(&index.language)
            .join("index.json");

        // Create directory if it doesn't exist
        if let Some(parent) = file_path.parent() {
            fs::create_dir_all(parent).await?;
        }

        let json = serde_json::to_string_pretty(index)?;

        if self.enable_compression {
            // Save compressed version
            let compressed_path = file_path.with_extension("json.gz");
            self.save_compressed(&compressed_path, json.as_bytes())
                .await?;
        }

        // Save uncompressed version
        fs::write(&file_path, json).await?;

        debug!("Saved index to {:?}", file_path);
        Ok(())
    }

    /// Save global index to JSON file
    async fn save_global_index(&self, index: &GlobalContentIndex) -> Result<()> {
        let file_path = self.output_dir.join("content-index.json");

        // Create directory if it doesn't exist
        if let Some(parent) = file_path.parent() {
            fs::create_dir_all(parent).await?;
        }

        let json = serde_json::to_string_pretty(index)?;

        if self.enable_compression {
            // Save compressed version
            let compressed_path = file_path.with_extension("json.gz");
            self.save_compressed(&compressed_path, json.as_bytes())
                .await?;
        }

        // Save uncompressed version
        fs::write(&file_path, json).await?;

        info!("Saved global index to {:?}", file_path);
        Ok(())
    }

    /// Save compressed file using gzip
    async fn save_compressed(&self, path: &Path, data: &[u8]) -> Result<()> {
        use async_compression::tokio::write::GzipEncoder;
        use tokio::io::AsyncWriteExt;

        let file = fs::File::create(path).await?;
        let mut encoder = GzipEncoder::new(file);
        encoder.write_all(data).await?;
        encoder.shutdown().await?;

        Ok(())
    }

    /// Discover available content types
    async fn discover_content_types(&self) -> Result<Vec<String>> {
        let mut types = Vec::new();

        let mut entries = fs::read_dir(&self.content_root).await?;
        while let Some(entry) = entries.next_entry().await? {
            if entry.file_type().await?.is_dir() {
                if let Some(name) = entry.file_name().to_str() {
                    // Skip special directories
                    if !name.starts_with('.')
                        && !name.starts_with('_')
                        && name != "locales"
                        && name != "themes"
                        && name != "routes"
                        && name != "footer"
                    {
                        types.push(name.to_string());
                    }
                }
            }
        }

        types.sort();
        Ok(types)
    }

    /// Discover available languages for a content type
    async fn discover_languages_for_type(&self, content_type: &str) -> Result<Vec<String>> {
        let type_dir = self.content_root.join(content_type);
        let mut languages = Vec::new();

        if type_dir.exists() {
            let mut entries = fs::read_dir(&type_dir).await?;
            while let Some(entry) = entries.next_entry().await? {
                if entry.file_type().await?.is_dir() {
                    if let Some(name) = entry.file_name().to_str() {
                        // Basic language code validation
                        if name.len() >= 2 && name.len() <= 5 && !name.starts_with('.') {
                            languages.push(name.to_string());
                        }
                    }
                }
            }
        }

        languages.sort();
        Ok(languages)
    }

    /// Generate incremental index update for a specific change
    pub async fn update_index_incremental(
        &self,
        content_type: &str,
        language: &str,
        category: Option<&str>,
        post_id: Option<&str>,
    ) -> Result<()> {
        // Load existing index
        let index_path = self
            .output_dir
            .join(content_type)
            .join(language)
            .join("index.json");

        if !index_path.exists() {
            // Generate full index if it doesn't exist
            self.generate_index(content_type, language).await?;
            return Ok(());
        }

        let json = fs::read_to_string(&index_path).await?;
        let mut index: ContentIndex = serde_json::from_str(&json)?;

        // Update specific parts based on what changed
        if let Some(post_id) = post_id {
            // Update single post
            if let Ok(Some(content)) = self.file_loader.load_by_slug(post_id) {
                let post_entry = self.create_post_entry(&content, language);
                index.posts.insert(post_id.to_string(), post_entry);
            } else {
                // Post was deleted
                index.posts.remove(post_id);
            }
        } else if let Some(category) = category {
            // Regenerate category index
            let contents =
                self.file_loader
                    .load_category_content(content_type, language, category)?;

            let mut post_ids = Vec::new();
            for content in contents {
                let post_entry = self.create_post_entry(&content, language);
                post_ids.push(post_entry.id.clone());
                index.posts.insert(post_entry.id.clone(), post_entry);
            }

            // Update category index
            if let Some(cat_index) = index.categories.get_mut(category) {
                cat_index.post_ids = post_ids;
                cat_index.post_count = cat_index.post_ids.len();
            }
        }

        // Update timestamp
        index.last_updated = chrono::Utc::now().to_rfc3339();

        // Save updated index
        self.save_index(&index).await?;

        info!(
            "Incrementally updated index for {}/{} (category: {:?}, post: {:?})",
            content_type, language, category, post_id
        );

        Ok(())
    }

    /// Get or generate index on-demand with caching
    pub async fn get_or_generate_index(
        &self,
        content_type: &str,
        language: &str,
    ) -> Result<ContentIndex> {
        let cache_key = format!("{}:{}", content_type, language);

        // Check cache first
        if self.enable_caching {
            let cache = self.index_cache.read().await;
            if let Some(cached_index) = cache.get(&cache_key) {
                // Check if cache is still valid (within last 10 minutes)
                if let Ok(last_updated) =
                    chrono::DateTime::parse_from_rfc3339(&cached_index.last_updated)
                {
                    let age = chrono::Utc::now()
                        .signed_duration_since(last_updated.with_timezone(&chrono::Utc));
                    if age.num_minutes() < 10 {
                        debug!("Serving cached index for {}/{}", content_type, language);
                        return Ok(cached_index.clone());
                    }
                }
            }
        }

        // Try to load from disk
        let index_path = self
            .output_dir
            .join(content_type)
            .join(language)
            .join("index.json");

        let _index = if index_path.exists() {
            // Check if disk index is recent enough
            if let Ok(metadata) = fs::metadata(&index_path).await {
                let modified = metadata
                    .modified()
                    .unwrap_or(std::time::SystemTime::UNIX_EPOCH);
                let age = std::time::SystemTime::now()
                    .duration_since(modified)
                    .unwrap_or_default();

                if age.as_secs() < 600 {
                    // 10 minutes
                    // Load from disk
                    let json = fs::read_to_string(&index_path).await?;
                    let index: ContentIndex = serde_json::from_str(&json)?;
                    debug!("Loaded index from disk for {}/{}", content_type, language);

                    // Update cache
                    if self.enable_caching {
                        let mut cache = self.index_cache.write().await;
                        cache.insert(cache_key, index.clone());
                    }

                    return Ok(index);
                }
            }
        };

        // Generate new index
        info!("Generating fresh index for {}/{}", content_type, language);
        let index = self.generate_index(content_type, language).await?;

        // Update cache
        if self.enable_caching {
            let mut cache = self.index_cache.write().await;
            cache.insert(cache_key, index.clone());
        }

        Ok(index)
    }

    /// Get posts by category on-demand
    pub async fn get_category_posts(
        &self,
        content_type: &str,
        language: &str,
        category: &str,
    ) -> Result<Vec<PostIndexEntry>> {
        let index = self.get_or_generate_index(content_type, language).await?;

        if let Some(category_index) = index.categories.get(category) {
            let mut posts = Vec::new();
            for post_id in &category_index.post_ids {
                if let Some(post) = index.posts.get(post_id) {
                    posts.push(post.clone());
                }
            }

            // Sort by published date (newest first)
            posts.sort_by(|a, b| {
                let a_date = a
                    .published_at
                    .as_ref()
                    .and_then(|d| chrono::DateTime::parse_from_rfc3339(d).ok());
                let b_date = b
                    .published_at
                    .as_ref()
                    .and_then(|d| chrono::DateTime::parse_from_rfc3339(d).ok());

                match (b_date, a_date) {
                    (Some(b), Some(a)) => b.cmp(&a),
                    (Some(_), None) => std::cmp::Ordering::Less,
                    (None, Some(_)) => std::cmp::Ordering::Greater,
                    (None, None) => a.title.cmp(&b.title),
                }
            });

            Ok(posts)
        } else {
            Ok(Vec::new())
        }
    }

    /// Get posts by tag on-demand
    pub async fn get_tag_posts(
        &self,
        content_type: &str,
        language: &str,
        tag: &str,
    ) -> Result<Vec<PostIndexEntry>> {
        let index = self.get_or_generate_index(content_type, language).await?;

        if let Some(tag_index) = index.tags.get(tag) {
            let mut posts = Vec::new();
            for post_id in &tag_index.post_ids {
                if let Some(post) = index.posts.get(post_id) {
                    posts.push(post.clone());
                }
            }

            // Sort by published date (newest first)
            posts.sort_by(|a, b| {
                let a_date = a
                    .published_at
                    .as_ref()
                    .and_then(|d| chrono::DateTime::parse_from_rfc3339(d).ok());
                let b_date = b
                    .published_at
                    .as_ref()
                    .and_then(|d| chrono::DateTime::parse_from_rfc3339(d).ok());

                match (b_date, a_date) {
                    (Some(b), Some(a)) => b.cmp(&a),
                    (Some(_), None) => std::cmp::Ordering::Less,
                    (None, Some(_)) => std::cmp::Ordering::Greater,
                    (None, None) => a.title.cmp(&b.title),
                }
            });

            Ok(posts)
        } else {
            Ok(Vec::new())
        }
    }

    /// Search posts with full-text capabilities
    pub async fn search_posts(
        &self,
        content_type: &str,
        language: &str,
        query: &str,
        limit: Option<usize>,
    ) -> Result<Vec<PostIndexEntry>> {
        let index = self.get_or_generate_index(content_type, language).await?;
        let query_lower = query.to_lowercase();

        let mut matching_posts = Vec::new();

        for post in index.posts.values() {
            let mut score = 0u32;

            // Search in title (weight: 3)
            if post.title.to_lowercase().contains(&query_lower) {
                score += 3;
                if post.title.to_lowercase() == query_lower {
                    score += 5; // Exact match bonus
                }
            }

            // Search in excerpt (weight: 2)
            if let Some(ref excerpt) = post.excerpt {
                if excerpt.to_lowercase().contains(&query_lower) {
                    score += 2;
                }
            }

            // Search in tags (weight: 1)
            for tag in &post.tags {
                if tag.to_lowercase().contains(&query_lower) {
                    score += 1;
                }
            }

            // Search in categories (weight: 1)
            for category in &post.categories {
                if category.to_lowercase().contains(&query_lower) {
                    score += 1;
                }
            }

            if score > 0 {
                matching_posts.push((score, post.clone()));
            }
        }

        // Sort by score (highest first), then by date
        matching_posts.sort_by(
            |(score_a, post_a), (score_b, post_b)| match score_b.cmp(score_a) {
                std::cmp::Ordering::Equal => {
                    let a_date = post_a
                        .published_at
                        .as_ref()
                        .and_then(|d| chrono::DateTime::parse_from_rfc3339(d).ok());
                    let b_date = post_b
                        .published_at
                        .as_ref()
                        .and_then(|d| chrono::DateTime::parse_from_rfc3339(d).ok());

                    match (b_date, a_date) {
                        (Some(b), Some(a)) => b.cmp(&a),
                        (Some(_), None) => std::cmp::Ordering::Less,
                        (None, Some(_)) => std::cmp::Ordering::Greater,
                        (None, None) => post_a.title.cmp(&post_b.title),
                    }
                }
                other => other,
            },
        );

        let results: Vec<PostIndexEntry> = matching_posts
            .into_iter()
            .map(|(_, post)| post)
            .take(limit.unwrap_or(50))
            .collect();

        debug!(
            "Search for '{}' in {}/{} returned {} results",
            query,
            content_type,
            language,
            results.len()
        );
        Ok(results)
    }

    /// Invalidate cache for specific content type/language
    pub async fn invalidate_cache(&self, content_type: &str, language: &str) {
        if self.enable_caching {
            let cache_key = format!("{}:{}", content_type, language);
            let mut cache = self.index_cache.write().await;
            cache.remove(&cache_key);
            debug!("Invalidated cache for {}/{}", content_type, language);
        }
    }

    /// Clear all cache entries
    pub async fn clear_cache(&self) {
        if self.enable_caching {
            let mut cache = self.index_cache.write().await;
            cache.clear();
            info!("Cleared all index cache entries");
        }
    }

    /// Get cache statistics
    pub async fn get_cache_stats(&self) -> (usize, Vec<String>) {
        if self.enable_caching {
            let cache = self.index_cache.read().await;
            let size = cache.len();
            let keys: Vec<String> = cache.keys().cloned().collect();
            (size, keys)
        } else {
            (0, Vec::new())
        }
    }

    /// Check if index exists and is recent
    pub async fn is_index_current(&self, content_type: &str, language: &str) -> bool {
        let index_path = self
            .output_dir
            .join(content_type)
            .join(language)
            .join("index.json");

        if !index_path.exists() {
            return false;
        }

        if let Ok(metadata) = fs::metadata(&index_path).await {
            let modified = metadata
                .modified()
                .unwrap_or(std::time::SystemTime::UNIX_EPOCH);
            let age = std::time::SystemTime::now()
                .duration_since(modified)
                .unwrap_or_default();

            // Consider index current if less than 10 minutes old
            age.as_secs() < 600
        } else {
            false
        }
    }
}

#[cfg(test)]
mod tests {
    use tempfile::TempDir;

    use super::*;

    #[tokio::test]
    async fn test_index_generation() {
        let temp_dir = TempDir::new().unwrap();
        let content_root = temp_dir.path().join("content");
        let output_dir = temp_dir.path().join("indexes");

        // Create test content structure
        let blog_en = content_root.join("blog").join("en");
        fs::create_dir_all(&blog_en).await.unwrap();

        // Create test post
        let post_content = r#"---
title: Test Post
slug: test-post
published: true
tags: [rust, web]
category: tutorials
---
# Test Content"#;

        fs::write(blog_en.join("test-post.md"), post_content)
            .await
            .unwrap();

        // Create indexer and generate indexes
        let indexer = ContentIndexer::new(&content_root, &output_dir);
        let global_index = indexer.generate_all_indexes().await.unwrap();

        // Verify global index
        assert_eq!(global_index.available_types, vec!["blog"]);
        assert_eq!(global_index.available_languages, vec!["en"]);
        assert_eq!(global_index.total_content_count, 1);

        // Verify specific index
        let blog_index = &global_index.indexes["blog"]["en"];
        assert_eq!(blog_index.total_posts, 1);
        assert!(blog_index.posts.contains_key("test-post"));
        assert!(blog_index.tags.contains_key("rust"));
        assert!(blog_index.tags.contains_key("web"));
    }
}
