//! Asset resolution system with fallback support

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

use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json;
use toml;
use walkdir::WalkDir;

use crate::config::{AssetSource, RusteloConfig};
use crate::error::{Result, RusteloError};

/// Asset resolver that implements the fallback strategy
pub struct AssetResolver {
    pub config: RusteloConfig,
    pub processors: FileProcessorRegistry,
}

impl AssetResolver {
    /// Create a new asset resolver with configuration
    pub fn new(config: RusteloConfig) -> Self {
        Self {
            config,
            processors: FileProcessorRegistry::new(),
        }
    }

    /// Resolve an asset using the fallback strategy
    pub fn resolve(&self, asset_path: &str) -> Result<Option<PathBuf>> {
        for source in &self.config.framework.asset_resolution.priority {
            if let Some(path) = self.try_resolve_from_source(asset_path, source)? {
                return Ok(Some(path));
            }
        }
        Ok(None)
    }

    /// Try to resolve asset from a specific source
    pub fn try_resolve_from_source(
        &self,
        asset_path: &str,
        source: &AssetSource,
    ) -> Result<Option<PathBuf>> {
        let base_path = match source {
            AssetSource::Local => &self.config.paths.local_assets,
            AssetSource::Framework => &self.config.paths.framework_assets,
            AssetSource::Generated => &self.config.paths.output,
        };

        let full_path = base_path.join(asset_path);

        if full_path.exists() {
            Ok(Some(full_path))
        } else {
            Ok(None)
        }
    }

    /// Check if an asset exists using fallback resolution
    pub fn exists(&self, asset_path: &str) -> Result<bool> {
        Ok(self.resolve(asset_path)?.is_some())
    }

    /// List all assets in a directory using fallback resolution
    pub fn list_assets(&self, directory: &str) -> Result<Vec<AssetInfo>> {
        let mut assets = Vec::new();
        let mut seen = std::collections::HashSet::new();

        // Go through sources in priority order
        for source in &self.config.framework.asset_resolution.priority {
            let base_path = match source {
                AssetSource::Local => &self.config.paths.local_assets,
                AssetSource::Framework => &self.config.paths.framework_assets,
                AssetSource::Generated => &self.config.paths.output,
            };

            let dir_path = base_path.join(directory);
            if dir_path.exists() && dir_path.is_dir() {
                for entry in WalkDir::new(dir_path) {
                    let entry =
                        entry.map_err(|e| RusteloError::generic(format!("Walk error: {}", e)))?;
                    let path = entry.path();

                    if path.is_file() {
                        if let Ok(relative) = path.strip_prefix(base_path) {
                            let relative_str = relative.to_string_lossy().to_string();

                            // Only add if we haven't seen this asset before (priority order)
                            if seen.insert(relative_str.clone()) {
                                let category = AssetCategory::from_path(path);
                                assets.push(AssetInfo {
                                    path: relative_str,
                                    source: source.clone(),
                                    full_path: path.to_path_buf(),
                                    category,
                                });
                            }
                        }
                    }
                }
            }
        }

        Ok(assets)
    }

    /// Copy an asset from source to target location
    pub fn copy_asset(&self, source_path: &str, target_path: &Path) -> Result<()> {
        if let Some(source) = self.resolve(source_path)? {
            if let Some(parent) = target_path.parent() {
                std::fs::create_dir_all(parent).map_err(RusteloError::Io)?;
            }

            std::fs::copy(source, target_path).map_err(RusteloError::Io)?;

            Ok(())
        } else {
            Err(RusteloError::generic(format!(
                "Asset not found: {}",
                source_path
            )))
        }
    }

    /// Merge assets from all sources into target directory
    pub fn merge_assets(&self, target_dir: &Path) -> Result<Vec<AssetInfo>> {
        let mut merged = Vec::new();

        // Get all unique assets across all sources
        for source in &self.config.framework.asset_resolution.priority {
            let assets = self.list_assets_from_source("", source)?;

            for asset in assets {
                // Check if we already have this asset from higher priority source
                if !merged.iter().any(|a: &AssetInfo| a.path == asset.path) {
                    let target_path = target_dir.join(&asset.path);
                    self.copy_asset(&asset.path, &target_path)?;
                    merged.push(asset);
                }
            }
        }

        Ok(merged)
    }

    /// Process and resolve a file with intelligent handling
    pub async fn process_file(
        &self,
        asset_path: &str,
        target_path: &Path,
        variables: HashMap<String, String>,
    ) -> Result<ProcessedFile> {
        if let Some(source_path) = self.resolve(asset_path)? {
            let context = ProcessingContext {
                variables,
                priority: self.config.framework.asset_resolution.priority.clone(),
                enable_merging: true,
            };

            self.processors
                .process_file(&source_path, target_path, &context)
                .await
        } else {
            Err(RusteloError::generic(format!(
                "Asset not found: {}",
                asset_path
            )))
        }
    }

    /// Process and merge files from multiple sources
    pub async fn process_with_fallback(
        &self,
        asset_path: &str,
        target_path: &Path,
        variables: HashMap<String, String>,
    ) -> Result<ProcessedFile> {
        let mut found_files = Vec::new();

        // Collect all matching files across sources (in priority order)
        for source in &self.config.framework.asset_resolution.priority {
            if let Some(path) = self.try_resolve_from_source(asset_path, source)? {
                found_files.push(path);
            }
        }

        if found_files.is_empty() {
            return Err(RusteloError::generic(format!(
                "Asset not found: {}",
                asset_path
            )));
        }

        let context = ProcessingContext {
            variables,
            priority: self.config.framework.asset_resolution.priority.clone(),
            enable_merging: true,
        };

        // Get processor for the file type
        if let Some(processor) = self.processors.find_processor(&found_files[0]) {
            if processor.can_merge() && found_files.len() > 1 {
                // Use intelligent merging for config files
                processor
                    .merge_files(&found_files, target_path, &context)
                    .await
            } else {
                // Use highest priority file for other types
                processor
                    .process_file(&found_files[0], target_path, &context)
                    .await
            }
        } else {
            // Fallback processor
            self.processors
                .process_file(&found_files[0], target_path, &context)
                .await
        }
    }

    /// Process entire asset directory with fallback resolution
    pub async fn process_assets(
        &self,
        target_dir: &Path,
        variables: HashMap<String, String>,
    ) -> Result<Vec<ProcessedFile>> {
        let mut processed = Vec::new();
        let mut seen_files = std::collections::HashSet::new();

        // Collect all unique asset paths across sources
        for source in &self.config.framework.asset_resolution.priority {
            let assets = self.list_assets_from_source("", source)?;

            for asset in assets {
                if seen_files.insert(asset.path.clone()) {
                    let target_path = target_dir.join(&asset.path);

                    match self
                        .process_with_fallback(&asset.path, &target_path, variables.clone())
                        .await
                    {
                        Ok(result) => processed.push(result),
                        Err(e) => {
                            tracing::warn!("Failed to process asset {}: {}", asset.path, e);
                        }
                    }
                }
            }
        }

        Ok(processed)
    }

    /// Process assets for specific categories only
    pub async fn process_assets_by_category(
        &self,
        target_dir: &Path,
        categories: &[AssetCategory],
        variables: HashMap<String, String>,
    ) -> Result<Vec<ProcessedFile>> {
        let mut processed = Vec::new();
        let mut seen_files = std::collections::HashSet::new();

        // Collect all unique asset paths across sources, filtered by category
        for source in &self.config.framework.asset_resolution.priority {
            let assets = self.list_assets_from_source("", source)?;

            for asset in assets {
                // Filter by category
                if !categories.contains(&asset.category) {
                    continue;
                }

                if seen_files.insert(asset.path.clone()) {
                    let target_path = target_dir.join(&asset.path);

                    match self
                        .process_with_fallback(&asset.path, &target_path, variables.clone())
                        .await
                    {
                        Ok(result) => processed.push(result),
                        Err(e) => {
                            tracing::warn!("Failed to process asset {}: {}", asset.path, e);
                        }
                    }
                }
            }
        }

        Ok(processed)
    }

    /// List assets filtered by category
    pub fn list_assets_by_category(
        &self,
        directory: &str,
        categories: &[AssetCategory],
    ) -> Result<Vec<AssetInfo>> {
        let all_assets = self.list_assets(directory)?;
        Ok(all_assets
            .into_iter()
            .filter(|asset| categories.contains(&asset.category))
            .collect())
    }

    /// Get asset statistics by category
    pub fn get_category_stats(&self, directory: &str) -> Result<HashMap<AssetCategory, usize>> {
        let assets = self.list_assets(directory)?;
        let mut stats = HashMap::new();

        for asset in assets {
            *stats.entry(asset.category).or_insert(0) += 1;
        }

        Ok(stats)
    }

    /// Organize assets into category-based directory structure
    pub async fn organize_by_category(
        &self,
        target_dir: &Path,
        variables: HashMap<String, String>,
    ) -> Result<HashMap<AssetCategory, Vec<ProcessedFile>>> {
        let mut organized = HashMap::new();

        for category in AssetCategory::all() {
            let category_assets =
                self.list_assets_by_category("", std::slice::from_ref(&category))?;
            let mut processed_files = Vec::new();

            for asset in category_assets {
                let category_dir = target_dir.join(category.default_directory());
                let target_path = category_dir.join(&asset.path);

                match self
                    .process_with_fallback(&asset.path, &target_path, variables.clone())
                    .await
                {
                    Ok(result) => processed_files.push(result),
                    Err(e) => {
                        tracing::warn!(
                            "Failed to process asset {} in category {:?}: {}",
                            asset.path,
                            category,
                            e
                        );
                    }
                }
            }

            if !processed_files.is_empty() {
                organized.insert(category, processed_files);
            }
        }

        Ok(organized)
    }

    /// List assets from a specific source
    pub fn list_assets_from_source(
        &self,
        directory: &str,
        source: &AssetSource,
    ) -> Result<Vec<AssetInfo>> {
        let mut assets = Vec::new();

        let base_path = match source {
            AssetSource::Local => &self.config.paths.local_assets,
            AssetSource::Framework => &self.config.paths.framework_assets,
            AssetSource::Generated => &self.config.paths.output,
        };

        let search_path = if directory.is_empty() {
            base_path.clone()
        } else {
            base_path.join(directory)
        };

        if search_path.exists() && search_path.is_dir() {
            for entry in WalkDir::new(search_path) {
                let entry =
                    entry.map_err(|e| RusteloError::generic(format!("Walk error: {}", e)))?;
                let path = entry.path();

                if path.is_file() {
                    if let Ok(relative) = path.strip_prefix(base_path) {
                        let category = AssetCategory::from_path(path);
                        assets.push(AssetInfo {
                            path: relative.to_string_lossy().to_string(),
                            source: source.clone(),
                            full_path: path.to_path_buf(),
                            category,
                        });
                    }
                }
            }
        }

        Ok(assets)
    }
}

/// File processor trait for handling different asset types
#[async_trait]
pub trait FileProcessor: Send + Sync {
    /// Get the file extensions this processor handles
    fn handles_extensions(&self) -> Vec<&'static str>;

    /// Process a file during resolution
    async fn process_file(
        &self,
        source_path: &Path,
        target_path: &Path,
        context: &ProcessingContext,
    ) -> Result<ProcessedFile>;

    /// Check if this processor can merge files of this type
    fn can_merge(&self) -> bool {
        false
    }

    /// Merge multiple files of the same type
    async fn merge_files(
        &self,
        files: &[PathBuf],
        target_path: &Path,
        context: &ProcessingContext,
    ) -> Result<ProcessedFile> {
        if files.is_empty() {
            return Err(RusteloError::generic("Cannot merge empty file list"));
        }

        // Default behavior: use the highest priority file
        let source_path = &files[0];
        self.process_file(source_path, target_path, context).await
    }
}

/// Context for file processing
#[derive(Debug, Clone)]
pub struct ProcessingContext {
    /// Configuration variables for template substitution
    pub variables: std::collections::HashMap<String, String>,
    /// Asset source priority
    pub priority: Vec<AssetSource>,
    /// Whether to enable merging for configuration files
    pub enable_merging: bool,
}

/// Result of file processing
#[derive(Debug)]
pub struct ProcessedFile {
    /// Target path where the file was written
    pub target_path: PathBuf,
    /// Source path that was processed
    pub source_path: PathBuf,
    /// Whether the file was merged from multiple sources
    pub merged: bool,
    /// Processor that handled the file
    pub processor: String,
}

/// Registry for file processors
pub struct FileProcessorRegistry {
    processors: Vec<Box<dyn FileProcessor>>,
}

impl FileProcessorRegistry {
    /// Create a new registry with default processors
    pub fn new() -> Self {
        let mut registry = Self {
            processors: Vec::new(),
        };

        // Register default processors
        registry.register(Box::new(crate::processors::ScriptFileProcessor));
        registry.register(Box::new(crate::processors::ConfigFileProcessor));
        registry.register(Box::new(crate::processors::ContentFileProcessor));
        registry.register(Box::new(crate::processors::StaticFileProcessor));

        registry
    }

    /// Register a new file processor
    pub fn register(&mut self, processor: Box<dyn FileProcessor>) {
        self.processors.push(processor);
    }

    /// Find the appropriate processor for a file
    pub fn find_processor(&self, file_path: &Path) -> Option<&dyn FileProcessor> {
        let extension = file_path
            .extension()
            .and_then(|ext| ext.to_str())
            .unwrap_or("");

        self.processors
            .iter()
            .find(|processor| processor.handles_extensions().contains(&extension))
            .map(|p| p.as_ref())
    }

    /// Process a single file with the appropriate processor
    pub async fn process_file(
        &self,
        source_path: &Path,
        target_path: &Path,
        context: &ProcessingContext,
    ) -> Result<ProcessedFile> {
        if let Some(processor) = self.find_processor(source_path) {
            processor
                .process_file(source_path, target_path, context)
                .await
        } else {
            // Default fallback: simple copy
            if let Some(parent) = target_path.parent() {
                std::fs::create_dir_all(parent).map_err(RusteloError::Io)?;
            }

            std::fs::copy(source_path, target_path).map_err(RusteloError::Io)?;

            Ok(ProcessedFile {
                target_path: target_path.to_path_buf(),
                source_path: source_path.to_path_buf(),
                merged: false,
                processor: "fallback".to_string(),
            })
        }
    }
}

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

/// Asset category for logical organization
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AssetCategory {
    /// Build-time assets: justfiles, build scripts, CI configs
    Build,
    /// Runtime configuration: app configs, environment files
    Config,
    /// Static web assets: CSS, JS, images, fonts
    Static,
    /// Content assets: markdown, templates, documentation
    Content,
    /// Development tools: linting configs, IDE settings
    Development,
    /// Deployment assets: Docker, k8s manifests, service configs
    Deployment,
}

impl AssetCategory {
    /// Get the default directory name for this category
    pub fn default_directory(&self) -> &'static str {
        match self {
            AssetCategory::Build => "scripts",
            AssetCategory::Config => "configs",
            AssetCategory::Static => "public",
            AssetCategory::Content => "content",
            AssetCategory::Development => "dev",
            AssetCategory::Deployment => "deploy",
        }
    }

    /// Determine category from file path
    pub fn from_path(path: &Path) -> Self {
        let path_str = path.to_string_lossy().to_lowercase();
        let extension = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");

        // Check directory-based categorization first
        if path_str.contains("/scripts/") || path_str.contains("/build/") {
            return AssetCategory::Build;
        }
        if path_str.contains("/configs/") || path_str.contains("/config/") {
            return AssetCategory::Config;
        }
        if path_str.contains("/public/")
            || path_str.contains("/static/")
            || path_str.contains("/assets/")
        {
            return AssetCategory::Static;
        }
        if path_str.contains("/content/") || path_str.contains("/docs/") {
            return AssetCategory::Content;
        }
        if path_str.contains("/dev/") || path_str.contains("/.") {
            return AssetCategory::Development;
        }
        if path_str.contains("/deploy/")
            || path_str.contains("/k8s/")
            || path_str.contains("/docker/")
        {
            return AssetCategory::Deployment;
        }

        // Extension-based categorization
        match extension {
            // Build assets
            "just" | "mk" | "make" | "cmake" | "ninja" | "gradle" | "pom" => AssetCategory::Build,
            "sh" | "bash" | "zsh" | "fish" | "bat" | "cmd" | "ps1" => AssetCategory::Build,
            "py" | "rb" | "pl" | "lua"
                if path_str.contains("build") || path_str.contains("script") =>
            {
                AssetCategory::Build
            }

            // Config assets
            "json" | "toml" | "yaml" | "yml" | "ini" | "conf" | "config" => AssetCategory::Config,
            "env" | "properties" | "cfg" | "xml" => AssetCategory::Config,

            // Static web assets
            "css" | "scss" | "sass" | "less" => AssetCategory::Static,
            "js" | "mjs" | "ts" | "jsx" | "tsx" => AssetCategory::Static,
            "png" | "jpg" | "jpeg" | "gif" | "svg" | "webp" | "avif" | "ico" => {
                AssetCategory::Static
            }
            "woff" | "woff2" | "ttf" | "otf" | "eot" => AssetCategory::Static,
            "mp4" | "webm" | "mp3" | "wav" | "ogg" => AssetCategory::Static,

            // Content assets
            "md" | "markdown" | "html" | "htm" | "txt" | "rst" => AssetCategory::Content,
            "adoc" | "asciidoc" | "wiki" | "textile" => AssetCategory::Content,

            // Development assets
            "gitignore" | "gitattributes" | "editorconfig" => AssetCategory::Development,
            "eslintrc" | "prettierrc" | "stylelintrc" => AssetCategory::Development,
            "code-workspace" | "vscode" => AssetCategory::Development,

            // Deployment assets
            "dockerfile" | "docker-compose" | "k8s" | "helm" => AssetCategory::Deployment,
            "service" | "socket" | "timer" => AssetCategory::Deployment,

            // Default to content for unknown types
            _ => AssetCategory::Content,
        }
    }

    /// Get all available categories
    pub fn all() -> Vec<AssetCategory> {
        vec![
            AssetCategory::Build,
            AssetCategory::Config,
            AssetCategory::Static,
            AssetCategory::Content,
            AssetCategory::Development,
            AssetCategory::Deployment,
        ]
    }
}

/// Information about a resolved asset
#[derive(Debug, Clone)]
pub struct AssetInfo {
    /// Relative path of the asset
    pub path: String,
    /// Source where the asset was found
    pub source: AssetSource,
    /// Full filesystem path to the asset
    pub full_path: PathBuf,
    /// Asset category
    pub category: AssetCategory,
}

/// Asset merger for intelligent configuration merging
pub struct AssetMerger;

impl AssetMerger {
    /// Merge JSON configuration files intelligently
    pub fn merge_json(base: &str, override_content: &str) -> Result<String> {
        let base_value: serde_json::Value = serde_json::from_str(base)
            .map_err(|e| RusteloError::generic(format!("Invalid base JSON: {}", e)))?;

        let override_value: serde_json::Value = serde_json::from_str(override_content)
            .map_err(|e| RusteloError::generic(format!("Invalid override JSON: {}", e)))?;

        let merged = Self::merge_json_values(base_value, override_value);

        serde_json::to_string_pretty(&merged)
            .map_err(|e| RusteloError::generic(format!("JSON serialization error: {}", e)))
    }

    /// Merge TOML configuration files intelligently
    pub fn merge_toml(base: &str, override_content: &str) -> Result<String> {
        let base_value: toml::Value = toml::from_str(base)
            .map_err(|e| RusteloError::generic(format!("Invalid base TOML: {}", e)))?;

        let override_value: toml::Value = toml::from_str(override_content)
            .map_err(|e| RusteloError::generic(format!("Invalid override TOML: {}", e)))?;

        let merged = Self::merge_toml_values(base_value, override_value);

        toml::to_string_pretty(&merged)
            .map_err(|e| RusteloError::generic(format!("TOML serialization error: {}", e)))
    }

    fn merge_json_values(
        base: serde_json::Value,
        override_val: serde_json::Value,
    ) -> serde_json::Value {
        match (base, override_val) {
            (serde_json::Value::Object(mut base_obj), serde_json::Value::Object(override_obj)) => {
                for (key, value) in override_obj {
                    if let Some(base_value) = base_obj.get(&key) {
                        base_obj.insert(key, Self::merge_json_values(base_value.clone(), value));
                    } else {
                        base_obj.insert(key, value);
                    }
                }
                serde_json::Value::Object(base_obj)
            }
            (serde_json::Value::Array(mut base_arr), serde_json::Value::Array(override_arr)) => {
                base_arr.extend(override_arr);
                serde_json::Value::Array(base_arr)
            }
            (_, override_val) => override_val, // Override takes precedence for non-objects
        }
    }

    fn merge_toml_values(base: toml::Value, override_val: toml::Value) -> toml::Value {
        match (base, override_val) {
            (toml::Value::Table(mut base_table), toml::Value::Table(override_table)) => {
                for (key, value) in override_table {
                    if let Some(base_value) = base_table.get(&key) {
                        base_table.insert(key, Self::merge_toml_values(base_value.clone(), value));
                    } else {
                        base_table.insert(key, value);
                    }
                }
                toml::Value::Table(base_table)
            }
            (toml::Value::Array(mut base_arr), toml::Value::Array(override_arr)) => {
                base_arr.extend(override_arr);
                toml::Value::Array(base_arr)
            }
            (_, override_val) => override_val, // Override takes precedence
        }
    }
}
