use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::SystemTime;

use ignore::WalkBuilder;
use ignore::overrides::OverrideBuilder;
use rayon::prelude::*;
use xxhash_rust::xxh3::xxh3_64;

use crate::config::Config;
use crate::error::{Error, Result};
use crate::language::Language;
use crate::structure::CodeStructure;
use crate::watcher::FileWatcher;

/// Metadata about an indexed file.
#[derive(Debug, Clone)]
pub struct FileMetadata {
    /// File size in bytes.
    pub size: u64,

    /// Last modification time.
    pub modified: SystemTime,

    /// File extension (without the dot).
    pub extension: Option<String>,

    /// Detected programming language.
    pub language: Option<Language>,

    /// Whether the file is read-only.
    pub readonly: bool,
}

/// A single indexed file.
#[derive(Debug, Clone)]
pub struct IndexedFile {
    /// Absolute path to the file.
    pub path: PathBuf,

    /// File contents (if read_contents is enabled).
    pub content: Option<String>,

    /// File metadata.
    pub metadata: FileMetadata,

    /// Content hash for change detection (XXH3).
    pub hash: Option<u64>,

    /// Parsed code structure (if parse_structure is enabled).
    pub structure: Option<CodeStructure>,
}

/// A chunk of file content with metadata.
#[derive(Debug, Clone)]
pub struct ContentChunk {
    /// The chunk content.
    pub content: String,

    /// Byte offset in the original file.
    pub byte_offset: usize,

    /// Chunk index (0-based).
    pub index: usize,

    /// Whether this is the last chunk.
    pub is_last: bool,
}

impl IndexedFile {
    /// Get the hash as a hex string.
    pub fn hash_hex(&self) -> Option<String> {
        self.hash.map(|h| format!("{:016x}", h))
    }

    /// Check if this file's content has changed compared to another hash.
    pub fn has_changed(&self, other_hash: u64) -> bool {
        self.hash.map(|h| h != other_hash).unwrap_or(true)
    }

    /// Split content into chunks suitable for embedding models.
    ///
    /// Uses a simple token estimation (4 chars ≈ 1 token) and splits on
    /// paragraph/line boundaries when possible.
    ///
    /// Returns `None` if content is not loaded.
    pub fn chunks(&self, max_tokens: usize) -> Option<Vec<ContentChunk>> {
        let content = self.content.as_ref()?;
        Some(chunk_content(content, max_tokens))
    }

    /// Split content into chunks with custom chars-per-token ratio.
    ///
    /// Different models have different tokenization. Common ratios:
    /// - GPT-style: ~4 chars/token
    /// - Claude: ~3.5 chars/token
    /// - Code: ~2.5 chars/token (more tokens per char due to syntax)
    pub fn chunks_with_ratio(
        &self,
        max_tokens: usize,
        chars_per_token: f32,
    ) -> Option<Vec<ContentChunk>> {
        let content = self.content.as_ref()?;
        Some(chunk_content_with_ratio(
            content,
            max_tokens,
            chars_per_token,
        ))
    }
}

/// Chunk content with default ratio (4 chars ≈ 1 token).
fn chunk_content(content: &str, max_tokens: usize) -> Vec<ContentChunk> {
    chunk_content_with_ratio(content, max_tokens, 4.0)
}

/// Chunk content with custom chars-per-token ratio.
fn chunk_content_with_ratio(
    content: &str,
    max_tokens: usize,
    chars_per_token: f32,
) -> Vec<ContentChunk> {
    let max_chars = (max_tokens as f32 * chars_per_token) as usize;

    if content.len() <= max_chars {
        return vec![ContentChunk {
            content: content.to_string(),
            byte_offset: 0,
            index: 0,
            is_last: true,
        }];
    }

    let mut chunks = Vec::new();
    let mut byte_offset = 0;
    let mut remaining = content;

    while !remaining.is_empty() {
        let chunk_end = find_chunk_boundary(remaining, max_chars);
        let chunk_content = &remaining[..chunk_end];

        let is_last = chunk_end >= remaining.len();

        chunks.push(ContentChunk {
            content: chunk_content.to_string(),
            byte_offset,
            index: chunks.len(),
            is_last,
        });

        byte_offset += chunk_end;
        remaining = &remaining[chunk_end..];
    }

    chunks
}

/// Find a good boundary to split content at, preferring paragraph/line breaks.
fn find_chunk_boundary(content: &str, max_chars: usize) -> usize {
    if content.len() <= max_chars {
        return content.len();
    }

    let search_region = &content[..max_chars];

    // Try to find paragraph break (double newline)
    if let Some(pos) = search_region.rfind("\n\n") {
        if pos > max_chars / 2 {
            return pos + 2; // Include the newlines
        }
    }

    // Try to find line break
    if let Some(pos) = search_region.rfind('\n') {
        if pos > max_chars / 2 {
            return pos + 1;
        }
    }

    // Try to find sentence end
    for pattern in [". ", "? ", "! ", ".\n", "?\n", "!\n"] {
        if let Some(pos) = search_region.rfind(pattern) {
            if pos > max_chars / 2 {
                return pos + pattern.len();
            }
        }
    }

    // Try to find word boundary
    if let Some(pos) = search_region.rfind(' ') {
        if pos > max_chars / 2 {
            return pos + 1;
        }
    }

    // Fall back to char boundary
    let mut end = max_chars;
    while end > 0 && !content.is_char_boundary(end) {
        end -= 1;
    }
    end
}

/// Fast filesystem indexer with .gitignore support.
#[derive(Debug, Clone)]
pub struct FileIndexer {
    root: PathBuf,
    config: Config,
}

impl FileIndexer {
    /// Create a new indexer for the given root path with default configuration.
    pub fn new<P: AsRef<Path>>(root: P) -> Self {
        Self {
            root: root.as_ref().to_path_buf(),
            config: Config::default(),
        }
    }

    /// Create a new indexer with custom configuration.
    pub fn with_config<P: AsRef<Path>>(root: P, config: Config) -> Self {
        Self {
            root: root.as_ref().to_path_buf(),
            config,
        }
    }

    /// Get the root path.
    pub fn root(&self) -> &Path {
        &self.root
    }

    /// Get the configuration.
    pub fn config(&self) -> &Config {
        &self.config
    }

    /// Iterate over all files matching the configuration.
    pub fn files(&self) -> FileIter {
        FileIter::new(self.root.clone(), self.config.clone())
    }

    /// Iterate and collect results, returning errors encountered.
    pub fn files_result(&self) -> impl Iterator<Item = Result<IndexedFile>> + '_ {
        FileIterResult::new(self.root.clone(), self.config.clone())
    }

    /// Collect all files in parallel. Faster for large directories with I/O-bound operations.
    ///
    /// Note: This collects paths first, then processes in parallel. For streaming,
    /// use `files()` iterator instead.
    pub fn files_parallel(&self) -> Vec<IndexedFile> {
        let config = Arc::new(self.config.clone());
        let extensions = &config.extensions;

        // Collect paths first (can't parallelize the walk itself easily)
        let paths: Vec<PathBuf> = build_walker(&self.root, &config)
            .filter_map(|e| e.ok())
            .filter(|e| e.file_type().map(|ft| ft.is_file()).unwrap_or(false))
            .filter(|entry| {
                if extensions.is_empty() {
                    return true;
                }
                entry
                    .path()
                    .extension()
                    .and_then(|e| e.to_str())
                    .map(|e| extensions.iter().any(|ext| ext == e))
                    .unwrap_or(false)
            })
            .map(|e| e.path().to_path_buf())
            .collect();

        // Process files in parallel
        let read_contents = config.read_contents;
        let max_content_size = config.max_content_size;
        let parse_structure = config.parse_structure;

        paths
            .into_par_iter()
            .filter_map(|path| {
                process_file(&path, read_contents, max_content_size, parse_structure).ok()
            })
            .collect()
    }

    /// Create a file watcher for this indexer's root.
    pub fn watch(&self) -> Result<FileWatcher> {
        FileWatcher::new(&self.root, self.config.clone())
    }

    /// Count total files (consumes an iteration).
    pub fn count(&self) -> usize {
        self.files().count()
    }

    /// Check if a specific path would be included by this indexer.
    pub fn includes(&self, path: &Path) -> bool {
        if !path.starts_with(&self.root) {
            return false;
        }

        // Check extension filter
        if !self.config.extensions.is_empty() {
            if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
                if !self.config.extensions.iter().any(|e| e == ext) {
                    return false;
                }
            } else {
                return false;
            }
        }

        true
    }
}

/// Iterator over indexed files.
pub struct FileIter {
    inner: Box<dyn Iterator<Item = IndexedFile> + Send>,
}

impl FileIter {
    fn new(root: PathBuf, config: Config) -> Self {
        let walker = build_walker(&root, &config);
        let read_contents = config.read_contents;
        let max_content_size = config.max_content_size;
        let parse_structure = config.parse_structure;
        let extensions = config.extensions.clone();

        let iter = walker
            .filter_map(|entry| entry.ok())
            .filter(|entry| entry.file_type().map(|ft| ft.is_file()).unwrap_or(false))
            .filter(move |entry| {
                if extensions.is_empty() {
                    return true;
                }
                entry
                    .path()
                    .extension()
                    .and_then(|e| e.to_str())
                    .map(|e| extensions.iter().any(|ext| ext == e))
                    .unwrap_or(false)
            })
            .filter_map(move |entry| {
                let path = entry.path().to_path_buf();
                process_file(&path, read_contents, max_content_size, parse_structure).ok()
            });

        Self {
            inner: Box::new(iter),
        }
    }
}

impl Iterator for FileIter {
    type Item = IndexedFile;

    fn next(&mut self) -> Option<Self::Item> {
        self.inner.next()
    }
}

/// Iterator that returns Result for each file.
pub struct FileIterResult {
    inner: Box<dyn Iterator<Item = Result<IndexedFile>> + Send>,
}

impl FileIterResult {
    fn new(root: PathBuf, config: Config) -> Self {
        let walker = build_walker(&root, &config);
        let read_contents = config.read_contents;
        let max_content_size = config.max_content_size;
        let parse_structure = config.parse_structure;
        let extensions = config.extensions.clone();

        let iter = walker
            .filter_map(|entry| match entry {
                Ok(e) if e.file_type().map(|ft| ft.is_file()).unwrap_or(false) => Some(Ok(e)),
                Ok(_) => None,
                Err(e) => Some(Err(Error::Walk(e))),
            })
            .filter(move |entry_result| {
                if extensions.is_empty() {
                    return true;
                }
                match entry_result {
                    Ok(entry) => entry
                        .path()
                        .extension()
                        .and_then(|e| e.to_str())
                        .map(|e| extensions.iter().any(|ext| ext == e))
                        .unwrap_or(false),
                    Err(_) => true, // Pass through errors
                }
            })
            .map(move |entry_result| {
                let entry = entry_result?;
                let path = entry.path().to_path_buf();
                process_file(&path, read_contents, max_content_size, parse_structure)
            });

        Self {
            inner: Box::new(iter),
        }
    }
}

impl Iterator for FileIterResult {
    type Item = Result<IndexedFile>;

    fn next(&mut self) -> Option<Self::Item> {
        self.inner.next()
    }
}

fn build_walker(root: &Path, config: &Config) -> ignore::Walk {
    let mut builder = WalkBuilder::new(root);

    builder
        .hidden(!config.include_hidden)
        .git_ignore(config.respect_gitignore)
        .git_global(config.respect_gitignore)
        .git_exclude(config.respect_gitignore)
        .follow_links(config.follow_symlinks)
        .standard_filters(config.respect_gitignore);

    if let Some(depth) = config.max_depth {
        builder.max_depth(Some(depth));
    }

    // Add custom ignore files
    for ignore_file in &config.custom_ignore_files {
        builder.add_ignore(ignore_file);
    }

    // Build overrides for include/exclude patterns
    if !config.exclude_patterns.is_empty() || !config.include_patterns.is_empty() {
        let mut overrides = OverrideBuilder::new(root);

        // Add include patterns (whitelist)
        for pattern in &config.include_patterns {
            // Include patterns are added as-is
            let _ = overrides.add(pattern);
        }

        // Add exclude patterns (negate them with !)
        for pattern in &config.exclude_patterns {
            // Exclude patterns need to be negated
            let negated = format!("!{}", pattern);
            let _ = overrides.add(&negated);
        }

        if let Ok(overrides) = overrides.build() {
            builder.overrides(overrides);
        }
    }

    builder.build()
}

fn process_file(
    path: &Path,
    read_contents: bool,
    max_content_size: u64,
    parse_structure: bool,
) -> Result<IndexedFile> {
    let fs_metadata = fs::metadata(path).map_err(|e| Error::Io {
        path: path.to_path_buf(),
        source: e,
    })?;

    let size = fs_metadata.len();
    let modified = fs_metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH);
    let readonly = fs_metadata.permissions().readonly();

    let extension = path
        .extension()
        .and_then(|e| e.to_str())
        .map(|s| s.to_string());

    let language = Language::from_path(path);

    let (content, hash) = if read_contents && size <= max_content_size {
        match fs::read(path) {
            Ok(bytes) => {
                let hash = xxh3_64(&bytes);
                let content = String::from_utf8(bytes).ok();
                (content, Some(hash))
            }
            Err(_) => (None, None),
        }
    } else {
        (None, None)
    };

    // Parse structure if enabled and we have content and a supported language
    let structure = if parse_structure {
        match (&content, language) {
            (Some(content), Some(lang)) => Some(CodeStructure::parse(content, lang)),
            _ => None,
        }
    } else {
        None
    };

    Ok(IndexedFile {
        path: path.to_path_buf(),
        content,
        metadata: FileMetadata {
            size,
            modified,
            extension,
            language,
            readonly,
        },
        hash,
        structure,
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs::File;
    use std::io::Write;
    use tempfile::TempDir;

    #[test]
    fn test_index_directory() {
        let dir = TempDir::new().unwrap();
        let file_path = dir.path().join("test.rs");
        let mut file = File::create(&file_path).unwrap();
        writeln!(file, "fn main() {{}}").unwrap();

        let indexer = FileIndexer::new(dir.path());
        let files: Vec<_> = indexer.files().collect();

        assert_eq!(files.len(), 1);
        assert_eq!(files[0].path, file_path);
        assert_eq!(files[0].metadata.language, Some(Language::Rust));
    }

    #[test]
    fn test_extension_filter() {
        let dir = TempDir::new().unwrap();
        File::create(dir.path().join("test.rs")).unwrap();
        File::create(dir.path().join("test.py")).unwrap();
        File::create(dir.path().join("test.txt")).unwrap();

        let config = Config::builder().extensions(vec!["rs"]).build();
        let indexer = FileIndexer::with_config(dir.path(), config);
        let files: Vec<_> = indexer.files().collect();

        assert_eq!(files.len(), 1);
        assert!(files[0].path.to_string_lossy().ends_with(".rs"));
    }

    #[test]
    fn test_content_hashing() {
        let dir = TempDir::new().unwrap();
        let file_path = dir.path().join("test.txt");
        let mut file = File::create(&file_path).unwrap();
        writeln!(file, "hello world").unwrap();

        let indexer = FileIndexer::new(dir.path());
        let files: Vec<_> = indexer.files().collect();

        assert_eq!(files.len(), 1);
        assert!(files[0].hash.is_some());
        assert!(files[0].content.is_some());
    }

    #[test]
    fn test_exclude_patterns() {
        let dir = TempDir::new().unwrap();
        File::create(dir.path().join("test.rs")).unwrap();
        File::create(dir.path().join("test.py")).unwrap();
        File::create(dir.path().join("test.txt")).unwrap();

        // Exclude .txt files
        let config = Config::builder().exclude("*.txt").build();
        let indexer = FileIndexer::with_config(dir.path(), config);
        let files: Vec<_> = indexer.files().collect();

        assert_eq!(files.len(), 2);
        assert!(
            files
                .iter()
                .all(|f| !f.path.to_string_lossy().ends_with(".txt"))
        );
    }

    #[test]
    fn test_exclude_multiple_patterns() {
        let dir = TempDir::new().unwrap();
        File::create(dir.path().join("test.rs")).unwrap();
        File::create(dir.path().join("test.py")).unwrap();
        File::create(dir.path().join("test.txt")).unwrap();
        File::create(dir.path().join("test.log")).unwrap();

        // Exclude .txt and .log files
        let config = Config::builder()
            .exclude_patterns(vec!["*.txt", "*.log"])
            .build();
        let indexer = FileIndexer::with_config(dir.path(), config);
        let files: Vec<_> = indexer.files().collect();

        assert_eq!(files.len(), 2);
        assert!(files.iter().all(|f| {
            let path = f.path.to_string_lossy();
            !path.ends_with(".txt") && !path.ends_with(".log")
        }));
    }

    #[test]
    fn test_include_patterns() {
        let dir = TempDir::new().unwrap();
        File::create(dir.path().join("test.rs")).unwrap();
        File::create(dir.path().join("test.py")).unwrap();
        File::create(dir.path().join("test.txt")).unwrap();

        // Only include .rs files
        let config = Config::builder().include("*.rs").build();
        let indexer = FileIndexer::with_config(dir.path(), config);
        let files: Vec<_> = indexer.files().collect();

        assert_eq!(files.len(), 1);
        assert!(files[0].path.to_string_lossy().ends_with(".rs"));
    }

    #[test]
    fn test_exclude_directory_pattern() {
        let dir = TempDir::new().unwrap();
        std::fs::create_dir(dir.path().join("src")).unwrap();
        std::fs::create_dir(dir.path().join("target")).unwrap();
        File::create(dir.path().join("src/main.rs")).unwrap();
        File::create(dir.path().join("target/debug.rs")).unwrap();

        // Exclude target directory
        let config = Config::builder().exclude("target/**").build();
        let indexer = FileIndexer::with_config(dir.path(), config);
        let files: Vec<_> = indexer.files().collect();

        assert_eq!(files.len(), 1);
        assert!(files[0].path.to_string_lossy().contains("src"));
    }
}
