//! Embedding decision logic for file attachments.
//!
//! This module implements the decision matrix for choosing between embedded content,
//! resource links, or snippets based on capabilities, size, and configuration.

use std::path::Path;

use crate::config::EmbeddingConfig;
use crate::error::ToolResult;
use crate::fs::file_type::detect_file_type;

/// Strategy for including a file in an ACP prompt.
#[derive(Debug, Clone, PartialEq)]
pub enum EmbeddingStrategy {
    /// Embed full file content as text resource.
    EmbedText {
        /// File content (possibly redacted).
        content: String,
        /// MIME type for the content.
        mime_type: String,
    },

    /// Embed file as base64-encoded blob.
    EmbedBlob {
        /// Base64-encoded binary data.
        data: Vec<u8>,
        /// MIME type for the binary data.
        mime_type: String,
    },

    /// Create a resource link (don't embed full content).
    Link {
        /// URI for the resource (e.g., dirigent://resource/<hash>).
        uri: String,
        /// Human-readable name (relative path).
        name: String,
        /// File size in bytes.
        size: u64,
        /// MIME type (if known).
        mime_type: Option<String>,
    },

    /// Embed a snippet (head/tail) with optional link to full file.
    Snippet {
        /// Head portion of the file.
        head: String,
        /// Tail portion of the file.
        tail: String,
        /// Total size of the original file.
        total_size: u64,
        /// MIME type.
        mime_type: String,
    },

    /// Deny the attachment (too large, blocked, etc.).
    Deny {
        /// Reason for denial.
        reason: String,
    },
}

/// Embedding decider.
///
/// Decides how to include files in ACP prompts based on agent capabilities,
/// file properties, and configuration limits.
pub struct EmbeddingDecider {
    /// Embedding configuration.
    config: EmbeddingConfig,
    /// Whether the agent supports embedded context.
    agent_supports_embedded: bool,
    /// Total bytes accumulated across all files in this prompt.
    accumulated_bytes: usize,
    /// Number of files processed so far.
    file_count: usize,
}

impl EmbeddingDecider {
    /// Create a new embedding decider.
    ///
    /// # Arguments
    ///
    /// * `config` - Embedding configuration with size limits and policies
    /// * `agent_supports_embedded` - Whether the agent advertised `embeddedContext` capability
    pub fn new(config: EmbeddingConfig, agent_supports_embedded: bool) -> Self {
        Self {
            config,
            agent_supports_embedded,
            accumulated_bytes: 0,
            file_count: 0,
        }
    }

    /// Decide the embedding strategy for a file.
    ///
    /// This implements the decision tree from the file embedding policy:
    /// 1. File type detection
    /// 2. Size checks (per-file and total accumulated)
    /// 3. Capability check (embeddedContext)
    /// 4. Strategy selection (embed text, blob, link, snippet, deny)
    ///
    /// # Arguments
    ///
    /// * `path` - Path to the file to embed
    ///
    /// # Returns
    ///
    /// The chosen embedding strategy.
    pub fn decide(&mut self, path: &Path) -> ToolResult<EmbeddingStrategy> {
        // Check file count limit
        if self.file_count >= self.config.max_files_per_prompt as usize {
            return Ok(EmbeddingStrategy::Deny {
                reason: format!(
                    "Maximum file count ({}) exceeded",
                    self.config.max_files_per_prompt
                ),
            });
        }

        // Get file metadata
        let metadata = std::fs::metadata(path)?;
        let file_size = metadata.len();

        // Detect file type
        let file_type = detect_file_type(path)?;

        // Build a name for the file (use file name, not full path)
        let name = path
            .file_name()
            .and_then(|n| n.to_str())
            .unwrap_or("unknown")
            .to_string();

        // Decide based on file type and capabilities
        let strategy = if file_type.is_binary {
            // Binary files: prefer link, allow small blobs if needed
            if file_size <= self.config.max_embed_bytes && self.agent_supports_embedded {
                // Small binary - could embed as blob, but prefer link for efficiency
                if self.config.allow_resource_link {
                    EmbeddingStrategy::Link {
                        uri: format!("dirigent://resource/{}", Self::hash_path(path)),
                        name,
                        size: file_size,
                        mime_type: file_type.mime_type,
                    }
                } else {
                    // Embed as blob only if linking is disabled
                    let data = std::fs::read(path)?;
                    EmbeddingStrategy::EmbedBlob {
                        data,
                        mime_type: file_type.mime_type.unwrap_or_else(|| "application/octet-stream".to_string()),
                    }
                }
            } else {
                // Large binary - link only
                if self.config.allow_resource_link {
                    EmbeddingStrategy::Link {
                        uri: format!("dirigent://resource/{}", Self::hash_path(path)),
                        name,
                        size: file_size,
                        mime_type: file_type.mime_type,
                    }
                } else {
                    EmbeddingStrategy::Deny {
                        reason: format!("Binary file too large ({} bytes) and linking is disabled", file_size),
                    }
                }
            }
        } else {
            // Text files: embed if small and capability supports, otherwise link or snippet
            if !self.agent_supports_embedded {
                // Agent doesn't support embedded context - always link
                if self.config.allow_resource_link {
                    EmbeddingStrategy::Link {
                        uri: format!("dirigent://resource/{}", Self::hash_path(path)),
                        name,
                        size: file_size,
                        mime_type: file_type.mime_type,
                    }
                } else {
                    EmbeddingStrategy::Deny {
                        reason: "Agent does not support embedded context and linking is disabled".to_string(),
                    }
                }
            } else if file_size <= self.config.max_embed_bytes {
                // Small text file - check total accumulated bytes
                let new_total = self.accumulated_bytes + file_size as usize;

                // For Phase 03, use max_embed_bytes * max_files_per_prompt as total cap
                let max_total_bytes = (self.config.max_embed_bytes as usize)
                    * (self.config.max_files_per_prompt as usize);

                if new_total <= max_total_bytes {
                    // Embed the file
                    let content = std::fs::read_to_string(path)?;
                    EmbeddingStrategy::EmbedText {
                        content,
                        mime_type: file_type.mime_type.unwrap_or_else(|| "text/plain; charset=utf-8".to_string()),
                    }
                } else {
                    // Exceeds total byte cap - link or deny
                    if self.config.allow_resource_link {
                        EmbeddingStrategy::Link {
                            uri: format!("dirigent://resource/{}", Self::hash_path(path)),
                            name,
                            size: file_size,
                            mime_type: file_type.mime_type,
                        }
                    } else {
                        EmbeddingStrategy::Deny {
                            reason: format!("Total embedded bytes would exceed limit ({} bytes)", max_total_bytes),
                        }
                    }
                }
            } else {
                // Large text file - link or snippet
                if self.config.allow_resource_link {
                    EmbeddingStrategy::Link {
                        uri: format!("dirigent://resource/{}", Self::hash_path(path)),
                        name,
                        size: file_size,
                        mime_type: file_type.mime_type,
                    }
                } else if self.config.snippet_strategy != crate::config::SnippetStrategy::HeadTail {
                    // Snippet embedding not configured
                    EmbeddingStrategy::Deny {
                        reason: format!("File too large ({} bytes) for embedding and linking is disabled", file_size),
                    }
                } else {
                    // Generate snippet (handled in EMBED-05)
                    // For now, deny and indicate snippet is needed
                    EmbeddingStrategy::Deny {
                        reason: "Snippet generation not yet implemented".to_string(),
                    }
                }
            }
        };

        // Update accumulated bytes if we're embedding
        match &strategy {
            EmbeddingStrategy::EmbedText { content, .. } => {
                self.accumulated_bytes += content.len();
                self.file_count += 1;
            }
            EmbeddingStrategy::EmbedBlob { data, .. } => {
                self.accumulated_bytes += data.len();
                self.file_count += 1;
            }
            EmbeddingStrategy::Link { .. } => {
                self.file_count += 1;
            }
            EmbeddingStrategy::Snippet { .. } => {
                self.file_count += 1;
            }
            EmbeddingStrategy::Deny { .. } => {
                // Don't increment count for denied files
            }
        }

        Ok(strategy)
    }

    /// Get the total bytes accumulated so far.
    pub fn accumulated_bytes(&self) -> usize {
        self.accumulated_bytes
    }

    /// Get the number of files processed so far.
    pub fn file_count(&self) -> usize {
        self.file_count
    }

    /// Generate a stable hash for a file path (used in URIs).
    ///
    /// This creates an opaque, stable identifier for the file.
    fn hash_path(path: &Path) -> String {
        use std::collections::hash_map::DefaultHasher;
        use std::hash::{Hash, Hasher};

        let mut hasher = DefaultHasher::new();
        path.hash(&mut hasher);
        format!("{:x}", hasher.finish())
    }
}

/// Standalone function for simple decision-making (no state tracking).
///
/// This is a convenience wrapper for one-off decisions without state.
pub fn decide_embedding_strategy(
    path: &Path,
    agent_supports_embedded: bool,
    config: &EmbeddingConfig,
) -> ToolResult<EmbeddingStrategy> {
    let mut decider = EmbeddingDecider::new(config.clone(), agent_supports_embedded);
    decider.decide(path)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::SnippetStrategy;
    use std::io::Write;
    use tempfile::NamedTempFile;

    fn default_config() -> EmbeddingConfig {
        EmbeddingConfig {
            max_embed_bytes: 256_000,
            allow_resource_link: true,
            redact_patterns: vec![],
            snippet_strategy: SnippetStrategy::HeadTail,
            max_files_per_prompt: 10,
        }
    }

    #[test]
    fn test_small_text_file_with_capability() {
        let mut temp = NamedTempFile::with_suffix(".txt").unwrap();
        temp.write_all(b"Hello, world!").unwrap();

        let config = default_config();
        let mut decider = EmbeddingDecider::new(config, true);

        let strategy = decider.decide(temp.path()).unwrap();

        match strategy {
            EmbeddingStrategy::EmbedText { content, mime_type } => {
                assert_eq!(content, "Hello, world!");
                assert!(mime_type.contains("text"));
            }
            _ => panic!("Expected EmbedText strategy"),
        }
    }

    #[test]
    fn test_small_text_file_without_capability() {
        let mut temp = NamedTempFile::with_suffix(".txt").unwrap();
        temp.write_all(b"Hello, world!").unwrap();

        let config = default_config();
        let mut decider = EmbeddingDecider::new(config, false);

        let strategy = decider.decide(temp.path()).unwrap();

        match strategy {
            EmbeddingStrategy::Link { .. } => {
                // Correct - should link when capability is missing
            }
            _ => panic!("Expected Link strategy when capability is missing"),
        }
    }

    #[test]
    fn test_large_text_file_creates_link() {
        let mut temp = NamedTempFile::with_suffix(".txt").unwrap();
        // Create file larger than max_embed_bytes
        let large_content = "x".repeat(300_000);
        temp.write_all(large_content.as_bytes()).unwrap();

        let config = default_config();
        let mut decider = EmbeddingDecider::new(config, true);

        let strategy = decider.decide(temp.path()).unwrap();

        match strategy {
            EmbeddingStrategy::Link { size, .. } => {
                assert_eq!(size, 300_000);
            }
            _ => panic!("Expected Link strategy for large file"),
        }
    }

    #[test]
    fn test_binary_file_creates_link() {
        let mut temp = NamedTempFile::with_suffix(".png").unwrap();
        temp.write_all(b"\x89PNG\r\n\x1a\n").unwrap();

        let config = default_config();
        let mut decider = EmbeddingDecider::new(config, true);

        let strategy = decider.decide(temp.path()).unwrap();

        match strategy {
            EmbeddingStrategy::Link { mime_type, .. } => {
                assert_eq!(mime_type, Some("image/png".to_string()));
            }
            _ => panic!("Expected Link strategy for binary file"),
        }
    }

    #[test]
    fn test_max_file_count_exceeded() {
        let mut config = default_config();
        config.max_files_per_prompt = 2;

        let mut decider = EmbeddingDecider::new(config, true);

        // Process 2 files successfully
        let mut temp1 = NamedTempFile::with_suffix(".txt").unwrap();
        temp1.write_all(b"File 1").unwrap();
        let _ = decider.decide(temp1.path()).unwrap();

        let mut temp2 = NamedTempFile::with_suffix(".txt").unwrap();
        temp2.write_all(b"File 2").unwrap();
        let _ = decider.decide(temp2.path()).unwrap();

        // Third file should be denied
        let mut temp3 = NamedTempFile::with_suffix(".txt").unwrap();
        temp3.write_all(b"File 3").unwrap();
        let strategy = decider.decide(temp3.path()).unwrap();

        match strategy {
            EmbeddingStrategy::Deny { reason } => {
                assert!(reason.contains("Maximum file count"));
            }
            _ => panic!("Expected Deny strategy when file count exceeded"),
        }
    }

    #[test]
    fn test_linking_disabled_denies_large_file() {
        let mut temp = NamedTempFile::with_suffix(".txt").unwrap();
        let large_content = "x".repeat(300_000);
        temp.write_all(large_content.as_bytes()).unwrap();

        let mut config = default_config();
        config.allow_resource_link = false;

        let mut decider = EmbeddingDecider::new(config, true);

        let strategy = decider.decide(temp.path()).unwrap();

        match strategy {
            EmbeddingStrategy::Deny { reason } => {
                assert!(reason.contains("linking is disabled"));
            }
            _ => panic!("Expected Deny when linking disabled and file too large"),
        }
    }

    #[test]
    fn test_accumulated_bytes_tracking() {
        let config = default_config();
        let mut decider = EmbeddingDecider::new(config, true);

        assert_eq!(decider.accumulated_bytes(), 0);

        let mut temp1 = NamedTempFile::with_suffix(".txt").unwrap();
        temp1.write_all(b"Hello").unwrap();
        let _ = decider.decide(temp1.path()).unwrap();

        assert_eq!(decider.accumulated_bytes(), 5);

        let mut temp2 = NamedTempFile::with_suffix(".txt").unwrap();
        temp2.write_all(b"World!").unwrap();
        let _ = decider.decide(temp2.path()).unwrap();

        assert_eq!(decider.accumulated_bytes(), 11);
    }

    #[test]
    fn test_hash_path_is_stable() {
        let path = Path::new("/test/file.txt");
        let hash1 = EmbeddingDecider::hash_path(path);
        let hash2 = EmbeddingDecider::hash_path(path);

        assert_eq!(hash1, hash2);
    }
}
