//! Output control utilities for MockForge generation
//!
//! This module provides functionality for:
//! - Generating barrel/index files
//! - Applying file extensions and banners
//! - Customizing file naming patterns

use crate::generate_config::{BarrelType, OutputConfig};
use crate::openapi::spec::OpenApiSpec;
use chrono::Utc;
use std::collections::HashMap;
use std::path::{Path, PathBuf};

/// Represents a generated file with metadata
#[derive(Debug, Clone)]
pub struct GeneratedFile {
    /// Relative path from output directory
    pub path: PathBuf,
    /// File content
    pub content: String,
    /// File extension (without dot)
    pub extension: String,
    /// Whether this file should be included in barrel exports
    pub exportable: bool,
}

/// Generate barrel/index files for a directory structure
pub struct BarrelGenerator;

impl BarrelGenerator {
    /// Generate barrel files based on the configuration
    ///
    /// # Arguments
    /// * `output_dir` - Root output directory
    /// * `files` - List of generated files
    /// * `barrel_type` - Type of barrel files to generate
    ///
    /// # Returns
    /// Vector of barrel file paths and contents
    pub fn generate_barrel_files(
        output_dir: &Path,
        files: &[GeneratedFile],
        barrel_type: BarrelType,
    ) -> Result<Vec<(PathBuf, String)>, crate::Error> {
        match barrel_type {
            BarrelType::None => Ok(vec![]),
            BarrelType::Index => Self::generate_index_file(output_dir, files),
            BarrelType::Barrel => Self::generate_barrel_structure(output_dir, files),
        }
    }

    /// Generate a single index.ts file at the root
    fn generate_index_file(
        output_dir: &Path,
        files: &[GeneratedFile],
    ) -> Result<Vec<(PathBuf, String)>, crate::Error> {
        let mut exports = Vec::new();

        // Collect exportable files (TypeScript/JavaScript files)
        for file in files {
            if !file.exportable {
                continue;
            }

            // Determine relative import path
            let rel_path = file.path.clone();
            let import_path = if rel_path.extension().is_some() {
                // Remove extension for import
                rel_path.with_extension("")
            } else {
                rel_path
            };

            // Convert to forward slashes for import paths
            let import_str = import_path
                .to_string_lossy()
                .replace('\\', "/")
                .trim_start_matches("./")
                .to_string();

            // Generate export statement based on extension
            let export = match file.extension.as_str() {
                "ts" | "tsx" => {
                    format!("export * from './{}';", import_str)
                }
                "js" | "jsx" | "mjs" => {
                    // For JS files, may need default exports or named exports
                    format!("export * from './{}';", import_str)
                }
                _ => continue, // Skip non-exportable files
            };

            exports.push(export);
        }

        // Sort exports for consistent output
        exports.sort();

        // Generate index.ts content
        let index_content = if exports.is_empty() {
            "// Generated by MockForge\n// No exportable files found\n".to_string()
        } else {
            format!(
                "// Generated by MockForge\n// Barrel file - exports all generated modules\n\n{}\n",
                exports.join("\n")
            )
        };

        let index_path = output_dir.join("index.ts");
        Ok(vec![(index_path, index_content)])
    }

    /// Generate barrel structure (index files at multiple directory levels)
    fn generate_barrel_structure(
        output_dir: &Path,
        files: &[GeneratedFile],
    ) -> Result<Vec<(PathBuf, String)>, crate::Error> {
        // Group files by directory
        let mut dir_exports: HashMap<PathBuf, Vec<(String, PathBuf)>> = HashMap::new();

        for file in files {
            if !file.exportable {
                continue;
            }

            let parent = file.path.parent().unwrap_or(Path::new("."));

            // Create import path relative to parent directory (not absolute from root)
            // If file is "api/types.ts" and parent is "api", import should be "./types"
            let file_stem = file.path.file_stem().unwrap_or_default();
            let import_str = file_stem.to_string_lossy().to_string();

            dir_exports
                .entry(parent.to_path_buf())
                .or_default()
                .push((format!("export * from './{}';", import_str), file.path.clone()));
        }

        let mut barrel_files = Vec::new();

        // Generate index.ts for each directory with exports
        for (dir, exports) in dir_exports {
            let mut export_lines: Vec<String> = exports.iter().map(|(e, _)| e.clone()).collect();
            export_lines.sort();

            let index_content = if export_lines.is_empty() {
                continue;
            } else {
                format!(
                    "// Generated by MockForge\n// Barrel file for directory: {}\n\n{}\n",
                    dir.display(),
                    export_lines.join("\n")
                )
            };

            let index_path = if dir == Path::new(".") {
                output_dir.join("index.ts")
            } else {
                output_dir.join(dir).join("index.ts")
            };

            barrel_files.push((index_path, index_content));
        }

        Ok(barrel_files)
    }
}

/// Apply banner to file content
pub fn apply_banner(content: &str, banner_template: &str, source_path: Option<&Path>) -> String {
    // Replace template placeholders with actual values
    let timestamp = Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string();
    let generator = "MockForge";
    let source = source_path
        .map(|p| p.display().to_string())
        .unwrap_or_else(|| "unknown".to_string());

    let banner = banner_template
        .replace("{{timestamp}}", &timestamp)
        .replace("{{source}}", &source)
        .replace("{{generator}}", generator);

    // Detect file type from content to determine comment style
    let comment_style = if content.trim_start().starts_with("//")
        || content.trim_start().starts_with("/*")
        || content.trim_start().starts_with("*")
    {
        // Already has comments - assume line comments
        "line"
    } else if content.trim_start().starts_with("#") {
        // Script file (shell, Python, etc.)
        "hash"
    } else {
        // Default: try to infer from common patterns
        // Check if it looks like TypeScript/JavaScript
        if content.contains("export") || content.contains("import") {
            "line"
        } else {
            "block"
        }
    };

    // Format banner according to detected style
    let formatted_banner = match comment_style {
        "hash" => {
            // Hash-style comments (#)
            format!("# {}\n", banner.replace('\n', "\n# "))
        }
        "line" => {
            // Line-style comments (//)
            format!("// {}\n", banner.replace('\n', "\n// "))
        }
        _ => {
            // Block-style comments (/* */)
            format!("/*\n * {}\n */\n", banner.replace('\n', "\n * "))
        }
    };

    format!("{}\n{}", formatted_banner, content)
}

/// Apply file extension override
pub fn apply_extension(file_path: &Path, extension: Option<&str>) -> PathBuf {
    match extension {
        Some(ext) => {
            // Remove any existing extension and add new one
            file_path.with_extension(ext)
        }
        None => file_path.to_path_buf(),
    }
}

/// Apply file naming template
pub fn apply_file_naming_template(template: &str, context: &HashMap<&str, &str>) -> String {
    let mut result = template.to_string();

    // Replace all placeholders
    for (key, value) in context {
        let placeholder = format!("{{{{{}}}}}", key);
        result = result.replace(&placeholder, value);
    }

    result
}

/// Context for file naming templates extracted from OpenAPI spec
///
/// This struct holds contextual information extracted from OpenAPI specifications
/// that can be used to generate file names using template patterns.
#[derive(Debug, Clone)]
pub struct FileNamingContext {
    /// Mapping of file/operation names to their context values
    /// Contains operation-specific values like tag, path, method, etc.
    context_map: HashMap<String, HashMap<String, String>>,
    /// Default context values when no specific mapping exists
    /// Used as fallback when a name is not found in the context map
    defaults: HashMap<String, String>,
}

impl FileNamingContext {
    /// Create a new empty context with defaults
    pub fn new() -> Self {
        let mut defaults = HashMap::new();
        defaults.insert("tag".to_string(), "api".to_string());
        defaults.insert("operation".to_string(), String::new());
        defaults.insert("path".to_string(), String::new());

        Self {
            context_map: HashMap::new(),
            defaults,
        }
    }

    /// Get context values for a given name
    pub fn get_context_for_name(&self, name: &str) -> HashMap<&str, &str> {
        if let Some(context) = self.context_map.get(name) {
            context.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect()
        } else {
            // Return defaults
            self.defaults.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect()
        }
    }
}

impl Default for FileNamingContext {
    fn default() -> Self {
        Self::new()
    }
}

/// Build file naming context from OpenAPI specification
pub fn build_file_naming_context(spec: &OpenApiSpec) -> FileNamingContext {
    let mut context = FileNamingContext::new();
    let mut context_map = HashMap::new();

    // Extract all paths and operations
    let all_paths = spec.all_paths_and_operations();

    for (path, operations) in all_paths {
        for (method, operation) in operations {
            // Build name from operation ID, or generate one from method + path
            let name = operation.operation_id.clone().unwrap_or_else(|| {
                // Generate name from method and path
                let path_name = path.trim_matches('/').replace('/', "_").replace(['{', '}'], "");
                format!("{}_{}", method.to_lowercase(), path_name)
            });

            // Build context for this operation
            let mut op_context = HashMap::new();

            // Get primary tag (first tag, or empty)
            let tag = operation.tags.first().cloned().unwrap_or_else(|| "api".to_string());
            op_context.insert("tag".to_string(), tag);

            // Operation method
            op_context.insert("operation".to_string(), method.to_lowercase());

            // API path
            op_context.insert("path".to_string(), path.clone());

            // Name (operation ID or generated name)
            op_context.insert("name".to_string(), name.clone());

            // Store in context map
            context_map.insert(name.clone(), op_context.clone());

            // Also map by simplified name (for cases where filename doesn't match operation ID)
            let simple_name = name.to_lowercase().replace(['-', ' '], "_");
            if simple_name != name {
                context_map.insert(simple_name, op_context);
            }
        }
    }

    // Also add schema names from components
    if let Some(schemas) = spec.schemas() {
        for (schema_name, _) in schemas {
            let mut schema_context = HashMap::new();
            schema_context.insert("name".to_string(), schema_name.clone());
            schema_context.insert("tag".to_string(), "schemas".to_string());
            schema_context.insert("operation".to_string(), String::new());
            schema_context.insert("path".to_string(), String::new());

            context_map.insert(schema_name.clone(), schema_context);
        }
    }

    context.context_map = context_map;
    context
}

/// Process generated files with output control options
pub fn process_generated_file(
    mut file: GeneratedFile,
    config: &OutputConfig,
    source_path: Option<&Path>,
    naming_context: Option<&FileNamingContext>,
) -> GeneratedFile {
    // Apply file naming template if specified (before extension processing)
    if let Some(template) = &config.file_naming_template {
        // Extract base name and extension
        let parent = file.path.parent().unwrap_or(Path::new("."));
        let old_stem = file.path.file_stem().unwrap_or_default().to_string_lossy().to_string();

        // Build context for template from OpenAPI spec if available
        let context_values: HashMap<&str, &str> = if let Some(ctx) = naming_context {
            // Try to get context for this file name
            let mut values = ctx.get_context_for_name(&old_stem);
            // If name not found, add name as default
            if !values.contains_key("name") {
                values.insert("name", &old_stem);
            }
            values
        } else {
            // Fallback to defaults
            let mut fallback = HashMap::new();
            fallback.insert("name", old_stem.as_str());
            fallback.insert("tag", "api");
            fallback.insert("operation", "");
            fallback.insert("path", "");
            fallback
        };

        let new_name = apply_file_naming_template(template, &context_values);

        // Reconstruct path with new name
        let ext = file.path.extension().and_then(|e| e.to_str()).unwrap_or("");
        let new_filename = if ext.is_empty() {
            new_name
        } else {
            format!("{}.{}", new_name, ext)
        };
        file.path = parent.join(new_filename);
    }

    // Apply extension override if specified
    if let Some(ext) = &config.extension {
        file.path = apply_extension(&file.path, Some(ext));
        file.extension = ext.clone();
    }

    // Apply banner if specified
    if let Some(banner_template) = &config.banner {
        file.content = apply_banner(&file.content, banner_template, source_path);
    }

    file
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_apply_banner() {
        let content = "export const test = 1;";
        let banner = "Generated by {{generator}}\nSource: {{source}}";
        let source = Some(Path::new("api.yaml"));

        let result = apply_banner(content, banner, source);
        assert!(result.contains("MockForge"));
        assert!(result.contains("api.yaml"));
        assert!(result.contains("export const test"));
    }

    #[test]
    fn test_apply_extension() {
        let path = Path::new("output/file.js");
        let new_path = apply_extension(path, Some("ts"));
        assert_eq!(new_path, PathBuf::from("output/file.ts"));
    }

    #[test]
    fn test_apply_file_naming_template() {
        let template = "{{name}}_{{tag}}";
        let mut context = HashMap::new();
        context.insert("name", "user");
        context.insert("tag", "api");

        let result = apply_file_naming_template(template, &context);
        assert_eq!(result, "user_api");
    }

    #[test]
    fn test_generate_index_file() {
        let output_dir = Path::new("/tmp/test");
        let files = vec![
            GeneratedFile {
                path: PathBuf::from("types.ts"),
                content: "export type User = {};".to_string(),
                extension: "ts".to_string(),
                exportable: true,
            },
            GeneratedFile {
                path: PathBuf::from("client.ts"),
                content: "export const client = {};".to_string(),
                extension: "ts".to_string(),
                exportable: true,
            },
        ];

        let result = BarrelGenerator::generate_index_file(output_dir, &files).unwrap();
        assert_eq!(result.len(), 1);
        assert!(result[0].0.ends_with("index.ts"));
        assert!(result[0].1.contains("export * from './types'"));
        assert!(result[0].1.contains("export * from './client'"));
    }
}
