//! Workspace indexer for processing source files.

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

use rayon::prelude::*;

use crate::chunker::Chunker;
use crate::embeddings::{create_embedding_text, embed_chunks, Embedder};
use crate::engine::ProgressCallback;
use crate::error::Result;
use crate::types::{Chunk, Document, DocumentId, IndexPhase, IndexProgress, Language, WorkspaceId};

/// Indexes source files in a workspace.
pub struct WorkspaceIndexer<C: Chunker, E: Embedder> {
    workspace_id: WorkspaceId,
    root_path: PathBuf,
    chunker: Arc<C>,
    embedder: Arc<E>,
    batch_size: usize,
}

impl<C: Chunker, E: Embedder> WorkspaceIndexer<C, E> {
    /// Create a new workspace indexer.
    pub fn new(
        workspace_id: WorkspaceId,
        root_path: PathBuf,
        chunker: Arc<C>,
        embedder: Arc<E>,
        batch_size: usize,
    ) -> Self {
        Self {
            workspace_id,
            root_path,
            chunker,
            embedder,
            batch_size,
        }
    }

    /// Index a list of files.
    pub fn index_files(
        &self,
        files: &[PathBuf],
        progress: Option<&ProgressCallback>,
    ) -> Result<(Vec<Document>, Vec<Chunk>, Vec<(String, Vec<f32>)>)> {
        let total = files.len();
        let mut all_documents = Vec::new();
        let mut all_chunks = Vec::new();

        // Phase 1: Parse files into chunks
        if let Some(cb) = progress {
            cb(IndexProgress {
                phase: IndexPhase::Parsing,
                processed: 0,
                total,
                current_file: None,
                eta_seconds: None,
            });
        }

        for (i, file) in files.iter().enumerate() {
            if let Some(cb) = progress {
                cb(IndexProgress {
                    phase: IndexPhase::Parsing,
                    processed: i,
                    total,
                    current_file: Some(file.clone()),
                    eta_seconds: None,
                });
            }

            match self.process_file(file) {
                Ok((doc, chunks)) => {
                    all_documents.push(doc);
                    all_chunks.extend(chunks);
                }
                Err(e) => {
                    tracing::warn!("Failed to process {}: {}", file.display(), e);
                }
            }
        }

        // Phase 2: Generate embeddings
        if let Some(cb) = progress {
            cb(IndexProgress {
                phase: IndexPhase::Embedding,
                processed: 0,
                total: all_chunks.len(),
                current_file: None,
                eta_seconds: None,
            });
        }

        let embeddings = self.generate_embeddings(&all_chunks, progress)?;

        // Phase 3: Done
        if let Some(cb) = progress {
            cb(IndexProgress {
                phase: IndexPhase::Complete,
                processed: total,
                total,
                current_file: None,
                eta_seconds: Some(0.0),
            });
        }

        Ok((all_documents, all_chunks, embeddings))
    }

    /// Process a single file into a document and chunks.
    fn process_file(&self, path: &Path) -> Result<(Document, Vec<Chunk>)> {
        // Read file content
        let content = std::fs::read_to_string(path)?;
        let metadata = std::fs::metadata(path)?;

        // Compute content hash
        let hash = blake3::hash(content.as_bytes()).to_hex().to_string();

        // Determine language
        let extension = path
            .extension()
            .and_then(|e| e.to_str())
            .unwrap_or("");
        let language = Language::from_extension(extension);

        // Create relative path
        let relative_path = path
            .strip_prefix(&self.root_path)
            .unwrap_or(path)
            .to_path_buf();

        // Create document
        let document = Document {
            id: DocumentId::new(),
            relative_path,
            absolute_path: path.to_path_buf(),
            language,
            content_hash: hash,
            size_bytes: metadata.len(),
            modified_at: metadata
                .modified()
                .ok()
                .map(|t| chrono::DateTime::from(t))
                .unwrap_or_else(chrono::Utc::now),
        };

        // Chunk the content
        let chunks = self.chunker.chunk(&document, &content)?;

        Ok((document, chunks))
    }

    /// Generate embeddings for chunks.
    fn generate_embeddings(
        &self,
        chunks: &[Chunk],
        progress: Option<&ProgressCallback>,
    ) -> Result<Vec<(String, Vec<f32>)>> {
        if chunks.is_empty() {
            return Ok(Vec::new());
        }

        let total = chunks.len();
        let mut all_embeddings = Vec::with_capacity(total);

        // Process in batches
        for (batch_idx, batch) in chunks.chunks(self.batch_size).enumerate() {
            let processed = batch_idx * self.batch_size;

            if let Some(cb) = progress {
                cb(IndexProgress {
                    phase: IndexPhase::Embedding,
                    processed,
                    total,
                    current_file: None,
                    eta_seconds: None,
                });
            }

            // Prepare texts for embedding
            let texts: Vec<String> = batch
                .iter()
                .map(|c| create_embedding_text(c))
                .collect();

            let text_refs: Vec<&str> = texts.iter().map(|s| s.as_str()).collect();

            // Generate embeddings
            let embeddings = self.embedder.embed_batch(&text_refs)?;

            // Pair with chunk IDs
            for (chunk, embedding) in batch.iter().zip(embeddings.into_iter()) {
                all_embeddings.push((chunk.id.0.to_string(), embedding));
            }
        }

        Ok(all_embeddings)
    }
}

/// Parallel file processor for large workspaces.
pub struct ParallelIndexer<C: Chunker + Send + Sync, E: Embedder + Send + Sync> {
    chunker: Arc<C>,
    embedder: Arc<E>,
    root_path: PathBuf,
}

impl<C: Chunker + Send + Sync, E: Embedder + Send + Sync> ParallelIndexer<C, E> {
    pub fn new(chunker: Arc<C>, embedder: Arc<E>, root_path: PathBuf) -> Self {
        Self {
            chunker,
            embedder,
            root_path,
        }
    }

    /// Process files in parallel.
    pub fn process_parallel(&self, files: &[PathBuf]) -> Vec<(Document, Vec<Chunk>)> {
        files
            .par_iter()
            .filter_map(|path| self.process_file(path).ok())
            .collect()
    }

    fn process_file(&self, path: &Path) -> Result<(Document, Vec<Chunk>)> {
        let content = std::fs::read_to_string(path)?;
        let metadata = std::fs::metadata(path)?;

        let hash = blake3::hash(content.as_bytes()).to_hex().to_string();

        let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("");
        let language = Language::from_extension(extension);

        let relative_path = path
            .strip_prefix(&self.root_path)
            .unwrap_or(path)
            .to_path_buf();

        let document = Document {
            id: DocumentId::new(),
            relative_path,
            absolute_path: path.to_path_buf(),
            language,
            content_hash: hash,
            size_bytes: metadata.len(),
            modified_at: metadata
                .modified()
                .ok()
                .map(|t| chrono::DateTime::from(t))
                .unwrap_or_else(chrono::Utc::now),
        };

        let chunks = self.chunker.chunk(&document, &content)?;

        Ok((document, chunks))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::chunker::DefaultChunker;
    use crate::config::ChunkingConfig;
    use crate::embeddings::model::RandomEmbedder;
    use tempfile::TempDir;

    #[test]
    fn test_indexer_process_file() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("test.rs");
        std::fs::write(&file_path, "fn main() { println!(\"Hello\"); }").unwrap();

        let chunker = Arc::new(DefaultChunker::new(ChunkingConfig::default()).unwrap());
        let embedder = Arc::new(RandomEmbedder::new(384));

        let indexer = WorkspaceIndexer::new(
            WorkspaceId::new(),
            temp_dir.path().to_path_buf(),
            chunker,
            embedder,
            32,
        );

        let (doc, chunks) = indexer.process_file(&file_path).unwrap();

        assert_eq!(doc.language, Language::Rust);
        assert!(!doc.content_hash.is_empty());
    }
}
