//! Init service for project initialization
//!
//! Handles business logic for creating new Mecha10 projects including:
//! - Directory structure creation
//! - Component addition
//! - Framework path detection

use crate::paths;
use anyhow::Result;
use std::path::Path;

/// Service for initializing new Mecha10 projects
///
/// # Examples
///
/// ```rust,ignore
/// use mecha10_cli::services::InitService;
///
/// let service = InitService::new();
///
/// // Create project structure
/// service.create_project_directories(&project_path).await?;
///
/// // Add example nodes
/// service.add_example_node(&project_path, "speaker").await?;
/// ```
pub struct InitService;

impl InitService {
    /// Create a new InitService
    pub fn new() -> Self {
        Self
    }

    /// Create project directory structure
    ///
    /// Creates all the necessary directories for a new Mecha10 project:
    /// - nodes/ - Custom node implementations
    /// - drivers/ - Hardware driver implementations
    /// - types/ - Shared type definitions
    /// - behaviors/ - Behavior tree definitions
    /// - config/ - Configuration files
    /// - logs/ - Runtime logs
    /// - simulation/ - Simulation environments
    /// - simulation/models/ - Robot physical models (for Godot)
    /// - assets/ - Static assets for simulation and visualization
    /// - assets/images/ - Image assets for simulation textures, UI, etc.
    pub async fn create_project_directories(&self, path: &Path) -> Result<()> {
        let dirs = vec![
            paths::project::NODES_DIR,
            paths::project::DRIVERS_DIR,
            paths::project::TYPES_DIR,
            paths::project::BEHAVIORS_DIR,
            paths::project::LOGS_DIR,
            paths::project::SIMULATION_DIR,
            paths::project::SIMULATION_MODELS_DIR,
            paths::project::ASSETS_DIR,
            paths::project::ASSETS_IMAGES_DIR,
            // Config directories (nodes grouped by scope: @mecha10/, @local/)
            paths::config::NODES_DIR,
            paths::config::SIMULATION_DIR,
        ];

        for dir in dirs {
            let dir_path = path.join(dir);
            tokio::fs::create_dir_all(&dir_path).await?;

            // Create .gitkeep to ensure empty directories are tracked
            tokio::fs::write(dir_path.join(".gitkeep"), "").await?;
        }

        Ok(())
    }

    /// Add an example node from the catalog to the project
    ///
    /// This method:
    /// 1. Searches the component catalog for the component
    /// 2. Generates component files from templates (if any)
    /// 3. Updates Cargo.toml (workspace members or dependencies)
    /// 4. Updates mecha10.json configuration
    ///
    /// # Arguments
    ///
    /// * `project_root` - Root path of the project
    /// * `component_name` - Name of the component to add (e.g., "speaker", "listener")
    pub async fn add_example_node(&self, project_root: &Path, component_name: &str) -> Result<()> {
        use crate::component_catalog::ComponentCatalog;

        // Load catalog and find component
        let catalog = ComponentCatalog::new();
        let components = catalog.search(component_name);

        if components.is_empty() {
            return Err(anyhow::anyhow!("Component '{}' not found in catalog", component_name));
        }

        let component = &components[0];

        // Generate component files from catalog templates (if any)
        // For monorepo nodes like speaker/listener, files vec is empty
        for file_template in &component.files {
            let file_path = project_root.join(&file_template.path);

            // Create parent directories
            if let Some(parent) = file_path.parent() {
                tokio::fs::create_dir_all(parent).await?;
            }

            // Write file content
            tokio::fs::write(&file_path, &file_template.content).await?;
        }

        // Only update workspace Cargo.toml if files were generated
        // For monorepo nodes, skip workspace member updates since there's no wrapper directory
        if !component.files.is_empty() {
            self.add_workspace_member(project_root, component_name).await?;
        }

        // For monorepo nodes, add as dependency to Cargo.toml
        let is_monorepo_node = component.files.is_empty();
        if is_monorepo_node {
            self.add_dependency(project_root, component).await?;
        }

        // Update mecha10.json configuration
        self.add_node_to_config(project_root, component_name, component, is_monorepo_node)
            .await?;

        // Copy node configs from framework package to project
        self.copy_node_configs(project_root, component_name).await?;

        Ok(())
    }

    /// Add a workspace member to Cargo.toml
    async fn add_workspace_member(&self, project_root: &Path, component_name: &str) -> Result<()> {
        let cargo_toml_path = project_root.join(paths::rust::CARGO_TOML);
        let content = tokio::fs::read_to_string(&cargo_toml_path).await?;

        let new_member = format!("nodes/{}", component_name);
        let updated_content = if content.contains("members = []") {
            // Replace empty array
            content.replace("members = []", &format!("members = [\n    \"{}\",\n]", new_member))
        } else if let Some(start) = content.find("members = [") {
            // Find the closing bracket
            if let Some(end) = content[start..].find(']') {
                let insert_pos = start + end;
                // Check if there are existing members
                let members_section = &content[start..insert_pos];
                if members_section.contains('\"') {
                    // Has existing members, add comma and new member
                    format!(
                        "{}\n    \"{}\",{}",
                        &content[..insert_pos],
                        new_member,
                        &content[insert_pos..]
                    )
                } else {
                    // Empty but not [], add member
                    format!(
                        "{}\n    \"{}\",\n{}",
                        &content[..insert_pos],
                        new_member,
                        &content[insert_pos..]
                    )
                }
            } else {
                content
            }
        } else {
            content
        };

        tokio::fs::write(&cargo_toml_path, updated_content).await?;
        Ok(())
    }

    /// Add a dependency to Cargo.toml
    async fn add_dependency(&self, project_root: &Path, component: &crate::component_catalog::Component) -> Result<()> {
        let cargo_toml_path = project_root.join(paths::rust::CARGO_TOML);
        let content = tokio::fs::read_to_string(&cargo_toml_path).await?;

        // Get the actual package name from the component's cargo dependencies
        let package_name = if let Some(dep) = component.cargo_dependencies.first() {
            dep.name.clone()
        } else {
            // Fallback to old behavior if no cargo dependencies specified
            format!("mecha10-nodes-{}", component.id)
        };

        // Add dependency after mecha10-core if not already present
        if !content.contains(&package_name) {
            let updated_content = if let Some(pos) = content.find("mecha10-core = \"0.1\"") {
                // Find end of line
                if let Some(newline_pos) = content[pos..].find('\n') {
                    let insert_pos = pos + newline_pos + 1;
                    format!(
                        "{}{} = \"0.1\"\n{}",
                        &content[..insert_pos],
                        package_name,
                        &content[insert_pos..]
                    )
                } else {
                    content
                }
            } else {
                content
            };

            tokio::fs::write(&cargo_toml_path, updated_content).await?;
        }

        Ok(())
    }

    /// Add node entry to mecha10.json
    async fn add_node_to_config(
        &self,
        project_root: &Path,
        component_name: &str,
        _component: &crate::component_catalog::Component,
        is_monorepo_node: bool,
    ) -> Result<()> {
        use crate::services::ConfigService;

        let config_path = project_root.join(paths::PROJECT_CONFIG);
        let mut config = ConfigService::load_from(&config_path).await?;

        // Create node identifier based on source
        let node_identifier = if is_monorepo_node {
            // Framework node: @mecha10/node-name
            format!("@mecha10/{}", component_name)
        } else {
            // Project node: @local/node-name
            format!("@local/{}", component_name)
        };

        // Add node using the new format
        config.nodes.add_node(&node_identifier);

        // Save updated config
        let config_json = serde_json::to_string_pretty(&config)?;
        tokio::fs::write(&config_path, config_json).await?;

        Ok(())
    }

    /// Copy behavior tree templates from framework to project
    ///
    /// Copies seed templates from `packages/behavior-runtime/seeds/` to
    /// `{project}/behaviors/` for use with the behavior tree system.
    ///
    /// # Arguments
    ///
    /// * `project_root` - Root path of the project
    pub async fn copy_behavior_templates(&self, project_root: &Path) -> Result<()> {
        // Detect framework path to find source templates
        // Skip silently if framework path not available (standalone CLI mode)
        let framework_path = match self.detect_framework_path() {
            Ok(path) => path,
            Err(_) => return Ok(()), // Not in dev mode, skip copying
        };
        let source_templates_path = std::path::PathBuf::from(&framework_path)
            .join("packages")
            .join("behavior-runtime")
            .join("seeds");

        // Check if source templates directory exists
        if !source_templates_path.exists() {
            // No templates to copy - this is okay
            return Ok(());
        }

        // Create destination directory
        let dest_templates_path = project_root.join("behaviors");
        tokio::fs::create_dir_all(&dest_templates_path).await?;

        // Copy all .json templates
        let mut entries = tokio::fs::read_dir(&source_templates_path).await?;
        while let Some(entry) = entries.next_entry().await? {
            let path = entry.path();
            if path.extension().and_then(|s| s.to_str()) == Some("json") {
                if let Some(filename) = path.file_name() {
                    let dest_file = dest_templates_path.join(filename);
                    tokio::fs::copy(&path, &dest_file).await?;
                }
            }
        }

        Ok(())
    }

    /// Copy simulation configuration file from framework to project
    ///
    /// Copies `packages/simulation/configs/config.json` to
    /// `{project}/configs/simulation/config.json`
    ///
    /// The config file contains both dev and production sections:
    /// ```json
    /// {
    ///   "dev": { ... },
    ///   "production": { ... }
    /// }
    /// ```
    ///
    /// # Arguments
    ///
    /// * `project_root` - Root path of the project
    pub async fn copy_simulation_configs(&self, project_root: &Path) -> Result<()> {
        // Detect framework path to find source config
        // Skip silently if framework path not available (standalone CLI mode)
        let framework_path = match self.detect_framework_path() {
            Ok(path) => path,
            Err(_) => return Ok(()), // Not in dev mode, skip copying
        };
        let source_config = std::path::PathBuf::from(&framework_path)
            .join("packages")
            .join("simulation")
            .join("configs")
            .join("config.json");

        // Check if source config exists
        if !source_config.exists() {
            return Ok(());
        }

        // Create destination directory and copy file
        let dest_dir = project_root.join("configs").join("simulation");
        tokio::fs::create_dir_all(&dest_dir).await?;

        let dest_file = dest_dir.join("config.json");
        tokio::fs::copy(&source_config, &dest_file).await?;

        Ok(())
    }

    /// Copy simulation asset files from framework to project
    ///
    /// Copies image assets from `packages/simulation/environments/basic_arena/assets/images/`
    /// to `{project}/assets/images/`
    ///
    /// This includes cat images (aiko.jpg, phoebe.jpg) used in the basic arena environment
    ///
    /// # Arguments
    ///
    /// * `project_root` - Root path of the project
    pub async fn copy_simulation_assets(&self, project_root: &Path) -> Result<()> {
        // Detect framework path to find source assets
        // Skip silently if framework path not available (standalone CLI mode)
        let framework_path = match self.detect_framework_path() {
            Ok(path) => path,
            Err(_) => return Ok(()), // Not in dev mode, skip copying
        };
        let source_assets_path = std::path::PathBuf::from(&framework_path)
            .join("packages")
            .join("simulation")
            .join("environments")
            .join("basic_arena")
            .join("assets")
            .join("images");

        // Check if source assets directory exists
        if !source_assets_path.exists() {
            // No assets to copy - this is okay
            return Ok(());
        }

        // Create destination directory
        let dest_assets_path = project_root.join("assets").join("images");
        tokio::fs::create_dir_all(&dest_assets_path).await?;

        // Copy image files
        let images = vec!["aiko.jpg", "phoebe.jpg"];
        for image in images {
            let source_file = source_assets_path.join(image);
            if source_file.exists() {
                let dest_file = dest_assets_path.join(image);
                tokio::fs::copy(&source_file, &dest_file).await?;
            }
        }

        Ok(())
    }

    /// Copy node configuration file from framework package to project
    ///
    /// Copies `packages/nodes/{node}/configs/config.json` to
    /// `{project}/configs/nodes/@mecha10/{node}/config.json`
    ///
    /// The config file contains both dev and production sections:
    /// ```json
    /// {
    ///   "dev": { ... },
    ///   "production": { ... }
    /// }
    /// ```
    ///
    /// # Arguments
    ///
    /// * `project_root` - Root path of the project
    /// * `component_name` - Name of the node (e.g., "speaker", "listener")
    async fn copy_node_configs(&self, project_root: &Path, component_name: &str) -> Result<()> {
        // Detect framework path to find source config
        // Skip silently if framework path not available (standalone CLI mode)
        let framework_path = match self.detect_framework_path() {
            Ok(path) => path,
            Err(_) => return Ok(()), // Not in dev mode, skip copying
        };
        let source_config = std::path::PathBuf::from(&framework_path)
            .join("packages")
            .join("nodes")
            .join(component_name)
            .join("configs")
            .join("config.json");

        // Check if source config exists
        if !source_config.exists() {
            return Ok(());
        }

        // Config path: configs/nodes/@mecha10/{node}/config.json
        let dest_dir = project_root
            .join("configs")
            .join("nodes")
            .join("@mecha10")
            .join(component_name);

        // Create destination directory and copy file
        tokio::fs::create_dir_all(&dest_dir).await?;

        let dest_file = dest_dir.join("config.json");
        tokio::fs::copy(&source_config, &dest_file).await?;

        Ok(())
    }

    /// Copy all default node configuration files from framework to project
    ///
    /// Copies configs for all default nodes from `packages/nodes/{node}/configs/`
    /// to `{project}/configs/nodes/@mecha10/{node}/config.json`
    ///
    /// This is called during `mecha10 init` to set up node configs.
    /// Skips silently if framework path is not available (standalone CLI mode).
    ///
    /// # Arguments
    ///
    /// * `project_root` - Root path of the project
    pub async fn copy_all_node_configs(&self, project_root: &Path) -> Result<()> {
        // Default nodes that should have configs copied
        let default_nodes = [
            "behavior-executor",
            "image-classifier",
            "listener",
            "llm-command",
            "object-detector",
            "simulation-bridge",
            "speaker",
            "teleop",
            "websocket-bridge",
        ];

        for node in default_nodes {
            self.copy_node_configs(project_root, node).await?;
        }

        Ok(())
    }

    /// Detect the framework path for development mode
    ///
    /// Searches for the mecha10-monorepo in the following order:
    /// 1. MECHA10_FRAMEWORK_PATH environment variable
    /// 2. Walking up the directory tree from current directory
    ///
    /// This is used when creating projects in development mode to link
    /// to the local framework instead of published crates.
    ///
    /// # Returns
    ///
    /// The absolute path to the framework root, or an error if not found.
    pub fn detect_framework_path(&self) -> Result<String> {
        // First check if MECHA10_FRAMEWORK_PATH is set
        if let Ok(path) = std::env::var("MECHA10_FRAMEWORK_PATH") {
            let path_buf = std::path::PathBuf::from(&path);

            // Expand ~ to home directory if present
            let expanded_path = if let Some(stripped) = path.strip_prefix("~/") {
                if let Ok(home) = std::env::var("HOME") {
                    std::path::PathBuf::from(home).join(stripped)
                } else {
                    path_buf.clone()
                }
            } else {
                path_buf.clone()
            };

            // Validate that this is actually the framework directory
            let core_path = expanded_path.join("packages").join("core");
            if core_path.exists() {
                return Ok(expanded_path.to_string_lossy().to_string());
            } else {
                return Err(anyhow::anyhow!(
                    "MECHA10_FRAMEWORK_PATH is set to '{}' but this doesn't appear to be the framework root.\n\
                     Expected to find packages/core directory at that location.",
                    path
                ));
            }
        }

        // Fall back to walking up from current directory
        let mut current = std::env::current_dir()?;

        loop {
            // Check if this directory contains packages/core (framework marker)
            let core_path = current.join("packages").join("core");
            if core_path.exists() {
                // Found the framework root
                return Ok(current.to_string_lossy().to_string());
            }

            // Check if we reached the filesystem root
            if !current.pop() {
                return Err(anyhow::anyhow!(
                    "Could not detect framework path. Either:\n\
                     1. Set MECHA10_FRAMEWORK_PATH environment variable, or\n\
                     2. Run from within the mecha10-monorepo directory\n\n\
                     Example: export MECHA10_FRAMEWORK_PATH=~/src/laboratory-one/mecha10"
                ));
            }
        }
    }
}

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