//! Content block generation from file attachments.
//!
//! This module provides the `ContentBlockBuilder` for converting file paths into
//! appropriate `ContentBlock` variants based on:
//! - Agent capabilities (embedded context support)
//! - File properties (size, type, content)
//! - Embedding configuration (size limits, redaction, snippet strategy)
//! - Sandbox restrictions (allowed roots, blocklists)

use crate::acp::connector_state::AgentCapabilities;
use crate::acp::protocol::prompt::{Annotations, ContentBlock, EmbeddedResource};
use base64::Engine;
use dirigent_tools::config::{EmbeddingConfig, SandboxConfig};
use dirigent_tools::embedding::{EmbeddingDecider, EmbeddingStrategy};
use dirigent_tools::error::{ToolError, ToolResult};
use dirigent_tools::path::validate_path;
use std::path::{Path, PathBuf};
use tracing::{debug, warn};

/// Builder for generating content blocks from file attachments.
///
/// This orchestrates the file embedding pipeline:
/// 1. Validate paths against sandbox
/// 2. Decide embedding strategy per file
/// 3. Apply redaction to embedded content
/// 4. Generate appropriate ContentBlock variants
/// 5. Track accumulated bytes and file counts
pub struct ContentBlockBuilder {
    /// Agent capabilities (embedded context support).
    // TODO: Capability validation - will be used to validate content blocks against agent capabilities
    #[allow(dead_code)]
    agent_caps: AgentCapabilities,
    /// Embedding configuration.
    embedding_config: EmbeddingConfig,
    /// Sandbox configuration.
    sandbox_config: SandboxConfig,
    /// Embedding decider (tracks accumulated state).
    decider: EmbeddingDecider,
}

impl ContentBlockBuilder {
    /// Create a new content block builder.
    ///
    /// # Arguments
    ///
    /// * `agent_caps` - Agent capabilities (from Initialize response)
    /// * `embedding_config` - Embedding configuration
    /// * `sandbox_config` - Sandbox configuration
    pub fn new(
        agent_caps: AgentCapabilities,
        embedding_config: EmbeddingConfig,
        sandbox_config: SandboxConfig,
    ) -> Self {
        let agent_supports_embedded = agent_caps.supports_embedded_context();
        let decider = EmbeddingDecider::new(embedding_config.clone(), agent_supports_embedded);

        Self {
            agent_caps,
            embedding_config,
            sandbox_config,
            decider,
        }
    }

    /// Build content blocks from a list of file paths.
    ///
    /// This is the main entry point for converting file attachments into content blocks.
    /// It validates each path, decides the embedding strategy, and generates the
    /// appropriate ContentBlock variant.
    ///
    /// # Arguments
    ///
    /// * `files` - List of file paths to process (must be absolute)
    ///
    /// # Returns
    ///
    /// A vector of `ContentBlock` variants, or an error if any path is invalid
    /// or if the embedding pipeline fails.
    ///
    /// # Errors
    ///
    /// - `ToolError::SandboxViolation` - Path outside allowed roots or blocked
    /// - `ToolError::InvalidPath` - Path doesn't exist or is not a file
    /// - `ToolError::FileReadError` - Failed to read file content
    ///
    /// # Security
    ///
    /// All paths are validated against the sandbox before processing. Absolute
    /// paths are never exposed in error messages.
    pub fn build_from_files(&mut self, files: &[PathBuf]) -> ToolResult<Vec<ContentBlock>> {
        let mut blocks = Vec::new();

        for file_path in files {
            // Validate path against sandbox first
            let validated_path = self.validate_file_path(file_path)?;

            // Decide embedding strategy
            let strategy = self.decider.decide(&validated_path)?;

            // Generate content block based on strategy
            match self.generate_content_block(&validated_path, strategy)? {
                Some(block) => blocks.push(block),
                None => {
                    // Strategy was Deny - log and skip
                    let filename = validated_path
                        .file_name()
                        .and_then(|n| n.to_str())
                        .unwrap_or("unknown");
                    warn!("File denied for embedding: {}", filename);
                }
            }
        }

        debug!(
            "Generated {} content blocks from {} files ({} bytes accumulated)",
            blocks.len(),
            files.len(),
            self.decider.accumulated_bytes()
        );

        Ok(blocks)
    }

    /// Validate a file path against sandbox configuration.
    ///
    /// This ensures the path:
    /// - Is within allowed roots
    /// - Is not in blocklist
    /// - Exists and is a file
    ///
    /// # Security
    ///
    /// Error messages never expose absolute paths - only basenames.
    fn validate_file_path(&self, path: &Path) -> ToolResult<PathBuf> {
        // Convert to string for validation
        let path_str = path
            .to_str()
            .ok_or_else(|| ToolError::InvalidConfig(
                format!("Path contains invalid UTF-8: {}", path.display())
            ))?;

        // Validate against sandbox
        let canonical_path = validate_path(path_str, &self.sandbox_config)?;

        // Ensure it's a file, not a directory
        if !canonical_path.is_file() {
            let filename = canonical_path
                .file_name()
                .and_then(|n| n.to_str())
                .unwrap_or("unknown");
            return Err(ToolError::FileReadError {
                path: filename.to_string(),
                source: std::io::Error::new(
                    std::io::ErrorKind::InvalidInput,
                    "Not a file"
                ),
            });
        }

        Ok(canonical_path)
    }

    /// Generate a content block from a validated path and strategy.
    ///
    /// Returns `None` if the strategy is `Deny`.
    fn generate_content_block(
        &self,
        path: &Path,
        strategy: EmbeddingStrategy,
    ) -> ToolResult<Option<ContentBlock>> {
        match strategy {
            EmbeddingStrategy::EmbedText { content, mime_type } => {
                // Apply redaction if configured
                let redacted_content = self.apply_redaction(&content);

                // Generate URI for the resource
                let uri = self.generate_uri(path);

                // Create embedded resource
                let resource = EmbeddedResource::Text {
                    uri: uri.clone(),
                    text: redacted_content,
                    mime_type: Some(mime_type),
                };

                // Create annotations (mark as assistant-focused)
                let annotations = Some(Annotations {
                    audience: Some(vec!["assistant".to_string()]),
                    priority: Some(0.5),
                });

                Ok(Some(ContentBlock::Resource {
                    resource,
                    annotations,
                }))
            }

            EmbeddingStrategy::EmbedBlob { data, mime_type } => {
                // Encode as base64
                let blob = base64::engine::general_purpose::STANDARD.encode(&data);

                // Generate URI
                let uri = self.generate_uri(path);

                // Create embedded resource
                let resource = EmbeddedResource::Blob {
                    uri: uri.clone(),
                    blob,
                    mime_type: Some(mime_type),
                };

                // Create annotations
                let annotations = Some(Annotations {
                    audience: Some(vec!["assistant".to_string()]),
                    priority: Some(0.5),
                });

                Ok(Some(ContentBlock::Resource {
                    resource,
                    annotations,
                }))
            }

            EmbeddingStrategy::Link {
                uri,
                name,
                size,
                mime_type,
            } => {
                // Create resource link
                let annotations = Some(Annotations {
                    audience: Some(vec!["assistant".to_string()]),
                    priority: Some(0.3),
                });

                Ok(Some(ContentBlock::ResourceLink {
                    uri,
                    name,
                    mime_type,
                    title: None,
                    description: None,
                    size: Some(size),
                    annotations,
                }))
            }

            EmbeddingStrategy::Snippet {
                head,
                tail,
                total_size: _,
                mime_type,
            } => {
                // For snippets, create both a resource (embedded snippet) and a link (full file)
                // Combine head and tail with separator
                let separator = "\n\n[... truncated ...]\n\n";
                let snippet_content = format!("{}{}{}", head, separator, tail);

                // Apply redaction to snippet
                let redacted_snippet = self.apply_redaction(&snippet_content);

                // Generate URI
                let uri = self.generate_uri(path);

                // Create embedded resource for snippet
                let resource = EmbeddedResource::Text {
                    uri: uri.clone(),
                    text: redacted_snippet,
                    mime_type: Some(mime_type.clone()),
                };

                // Create annotations for snippet
                let annotations = Some(Annotations {
                    audience: Some(vec!["assistant".to_string()]),
                    priority: Some(0.4),
                });

                // Return the snippet as embedded resource
                // Note: Could optionally also include a ResourceLink to the full file
                Ok(Some(ContentBlock::Resource {
                    resource,
                    annotations,
                }))
            }

            EmbeddingStrategy::Deny { reason } => {
                // Log denial reason
                let filename = path
                    .file_name()
                    .and_then(|n| n.to_str())
                    .unwrap_or("unknown");
                warn!("File embedding denied for {}: {}", filename, reason);

                // Return None to skip this file
                Ok(None)
            }
        }
    }

    /// Generate a stable URI for a file path.
    ///
    /// Uses BLAKE3 hash of the canonical path for stability and opacity.
    fn generate_uri(&self, path: &Path) -> String {
        use blake3::Hasher;

        let mut hasher = Hasher::new();
        hasher.update(path.to_string_lossy().as_bytes());
        let hash = hasher.finalize();

        format!("dirigent://resource/{}", hash.to_hex())
    }

    /// Apply redaction patterns to content.
    ///
    /// This is a best-effort operation that doesn't modify files on disk.
    fn apply_redaction(&self, content: &str) -> String {
        if self.embedding_config.redact_patterns.is_empty() {
            return content.to_string();
        }

        // Use ContentRedactor from dirigent_tools
        match dirigent_tools::embedding::ContentRedactor::new(&self.embedding_config.redact_patterns)
        {
            Ok(redactor) => redactor.redact(content),
            Err(e) => {
                warn!("Failed to create redactor: {}", e);
                content.to_string()
            }
        }
    }

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

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

/// Convenience function for building content blocks from files.
///
/// This is a simplified API for one-off conversions.
///
/// # Arguments
///
/// * `files` - List of file paths to convert (must be absolute)
/// * `agent_caps` - Agent capabilities
/// * `embedding_config` - Embedding configuration
/// * `sandbox_config` - Sandbox configuration
///
/// # Returns
///
/// A vector of `ContentBlock` variants.
pub fn build_content_blocks_from_files(
    files: &[PathBuf],
    agent_caps: &AgentCapabilities,
    embedding_config: &EmbeddingConfig,
    sandbox_config: &SandboxConfig,
) -> ToolResult<Vec<ContentBlock>> {
    let mut builder = ContentBlockBuilder::new(
        agent_caps.clone(),
        embedding_config.clone(),
        sandbox_config.clone(),
    );

    builder.build_from_files(files)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::acp::connector_state::PromptCapabilities;
    use tempfile::TempDir;

    fn create_test_agent_caps(embedded_context: bool) -> AgentCapabilities {
        AgentCapabilities {
            prompt_capabilities: Some(PromptCapabilities {
                embedded_context: Some(embedded_context),
                ..Default::default()
            }),
            ..Default::default()
        }
    }

    fn create_test_config(temp_dir: &TempDir) -> (EmbeddingConfig, SandboxConfig) {
        let embedding_config = EmbeddingConfig {
            max_embed_bytes: 1000,
            allow_resource_link: true,
            redact_patterns: vec![],
            snippet_strategy: dirigent_tools::config::SnippetStrategy::HeadTail,
            max_files_per_prompt: 10,
        };

        let mut sandbox_config = SandboxConfig::default();
        sandbox_config.allowed_roots = vec![temp_dir.path().to_path_buf()];
        sandbox_config.normalize_roots();

        (embedding_config, sandbox_config)
    }

    #[test]
    fn test_small_text_file_embeds() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("test.txt");
        std::fs::write(&file_path, "Hello, world!").unwrap();

        let agent_caps = create_test_agent_caps(true);
        let (embedding_config, sandbox_config) = create_test_config(&temp_dir);

        let blocks = build_content_blocks_from_files(
            &[file_path],
            &agent_caps,
            &embedding_config,
            &sandbox_config,
        )
        .unwrap();

        assert_eq!(blocks.len(), 1);
        match &blocks[0] {
            ContentBlock::Resource { resource, .. } => match resource {
                EmbeddedResource::Text { text, .. } => {
                    assert_eq!(text, "Hello, world!");
                }
                _ => panic!("Expected text resource"),
            },
            _ => panic!("Expected resource block"),
        }
    }

    #[test]
    fn test_large_file_creates_link() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("large.txt");
        let large_content = "x".repeat(2000); // Exceeds 1000 byte limit
        std::fs::write(&file_path, &large_content).unwrap();

        let agent_caps = create_test_agent_caps(true);
        let (embedding_config, sandbox_config) = create_test_config(&temp_dir);

        let blocks = build_content_blocks_from_files(
            &[file_path],
            &agent_caps,
            &embedding_config,
            &sandbox_config,
        )
        .unwrap();

        assert_eq!(blocks.len(), 1);
        match &blocks[0] {
            ContentBlock::ResourceLink { size, .. } => {
                assert_eq!(*size, Some(2000));
            }
            _ => panic!("Expected resource link block"),
        }
    }

    #[test]
    fn test_sandbox_violation_rejected() {
        let temp_dir = TempDir::new().unwrap();
        let outside_dir = TempDir::new().unwrap();
        let file_path = outside_dir.path().join("outside.txt");
        std::fs::write(&file_path, "Outside sandbox").unwrap();

        let agent_caps = create_test_agent_caps(true);
        let (embedding_config, sandbox_config) = create_test_config(&temp_dir);

        let result = build_content_blocks_from_files(
            &[file_path],
            &agent_caps,
            &embedding_config,
            &sandbox_config,
        );

        assert!(result.is_err());
        assert!(matches!(result, Err(ToolError::SandboxViolation { .. })));
    }

    #[test]
    fn test_multiple_files_accumulate_bytes() {
        let temp_dir = TempDir::new().unwrap();

        let file1 = temp_dir.path().join("file1.txt");
        std::fs::write(&file1, "File 1 content").unwrap();

        let file2 = temp_dir.path().join("file2.txt");
        std::fs::write(&file2, "File 2 content").unwrap();

        let agent_caps = create_test_agent_caps(true);
        let (embedding_config, sandbox_config) = create_test_config(&temp_dir);

        let blocks = build_content_blocks_from_files(
            &[file1, file2],
            &agent_caps,
            &embedding_config,
            &sandbox_config,
        )
        .unwrap();

        assert_eq!(blocks.len(), 2);
    }

    #[test]
    fn test_uri_generation_is_stable() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("stable.txt");
        std::fs::write(&file_path, "Stable content").unwrap();

        let agent_caps = create_test_agent_caps(true);
        let (embedding_config, sandbox_config) = create_test_config(&temp_dir);

        let blocks1 = build_content_blocks_from_files(
            &[file_path.clone()],
            &agent_caps,
            &embedding_config,
            &sandbox_config,
        )
        .unwrap();

        let blocks2 = build_content_blocks_from_files(
            &[file_path],
            &agent_caps,
            &embedding_config,
            &sandbox_config,
        )
        .unwrap();

        // Extract URIs and compare
        match (&blocks1[0], &blocks2[0]) {
            (
                ContentBlock::Resource {
                    resource: EmbeddedResource::Text { uri: uri1, .. },
                    ..
                },
                ContentBlock::Resource {
                    resource: EmbeddedResource::Text { uri: uri2, .. },
                    ..
                },
            ) => {
                assert_eq!(uri1, uri2, "URIs should be stable");
            }
            _ => panic!("Expected resource blocks"),
        }
    }
}
