//! Node generator service
//!
//! Generates custom node templates for Mecha10 projects.

use crate::paths;
use anyhow::{Context, Result};
use std::path::Path;

/// Service for generating custom node templates
pub struct NodeGeneratorService;

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

    /// Generate a new node in the project
    ///
    /// Creates the following structure:
    /// ```text
    /// nodes/<name>/
    /// ├── Cargo.toml
    /// └── src/
    ///     ├── lib.rs
    ///     ├── main.rs
    ///     └── config.rs
    ///
    /// configs/nodes/<name>/
    /// └── config.json
    /// ```
    pub async fn generate_node(&self, project_root: &Path, name: &str, description: Option<&str>) -> Result<()> {
        // Validate name
        self.validate_node_name(name)?;

        // Check if node already exists
        let node_dir = project_root.join(paths::project::NODES_DIR).join(name);
        if node_dir.exists() {
            anyhow::bail!("Node '{}' already exists at {}", name, node_dir.display());
        }

        // Create node directory structure
        let src_dir = node_dir.join(paths::project::SRC_DIR);
        tokio::fs::create_dir_all(&src_dir)
            .await
            .context("Failed to create node directory")?;

        // Generate files
        self.create_cargo_toml(&node_dir, name).await?;
        self.create_lib_rs(&src_dir, name).await?;
        self.create_main_rs(&src_dir, name).await?;
        self.create_config_rs(&src_dir, name).await?;

        // Create config directory and file
        // Config path: configs/nodes/<name>/config.json (no @local prefix)
        let config_dir = project_root.join(paths::config::NODES_DIR).join(name);
        tokio::fs::create_dir_all(&config_dir)
            .await
            .context("Failed to create config directory")?;
        self.create_config_json(&config_dir).await?;

        // Update mecha10.json
        self.update_mecha10_json(project_root, name, description).await?;

        // Update Cargo.toml workspace members
        self.update_cargo_workspace(project_root, name).await?;

        Ok(())
    }

    /// Validate node name
    fn validate_node_name(&self, name: &str) -> Result<()> {
        if name.is_empty() {
            anyhow::bail!("Node name cannot be empty");
        }

        // Check for valid characters (alphanumeric, hyphens, underscores)
        if !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') {
            anyhow::bail!(
                "Node name '{}' contains invalid characters. Use only letters, numbers, hyphens, and underscores.",
                name
            );
        }

        // Must start with a letter
        if !name.chars().next().map(|c| c.is_ascii_alphabetic()).unwrap_or(false) {
            anyhow::bail!("Node name must start with a letter");
        }

        Ok(())
    }

    /// Convert name to PascalCase (handles both kebab-case and snake_case)
    fn to_pascal_case(&self, name: &str) -> String {
        name.split(['-', '_'])
            .filter(|s| !s.is_empty())
            .map(|word| {
                let mut chars = word.chars();
                match chars.next() {
                    None => String::new(),
                    Some(first) => first.to_uppercase().chain(chars).collect(),
                }
            })
            .collect()
    }

    /// Convert name to snake_case for Rust crate name
    fn to_snake_case(&self, name: &str) -> String {
        name.replace('-', "_")
    }

    /// Create Cargo.toml for the node
    async fn create_cargo_toml(&self, node_dir: &Path, name: &str) -> Result<()> {
        let content = format!(
            r#"[package]
name = "{name}"
version = "0.1.0"
edition = "2021"

[lib]
name = "{crate_name}"
path = "src/lib.rs"

[[bin]]
name = "{name}"
path = "src/main.rs"

[dependencies]
anyhow = "1.0"
async-trait = "0.1"
mecha10-core = "0.1"
serde = {{ version = "1.0", features = ["derive"] }}
tokio = {{ version = "1.40", features = ["full"] }}
tracing = "0.1"
"#,
            name = name,
            crate_name = self.to_snake_case(name),
        );

        tokio::fs::write(node_dir.join(paths::rust::CARGO_TOML), content).await?;
        Ok(())
    }

    /// Create src/lib.rs with node implementation
    async fn create_lib_rs(&self, src_dir: &Path, name: &str) -> Result<()> {
        let pascal_name = self.to_pascal_case(name);
        let content = format!(
            r#"//! {pascal_name} Node
//!
//! Custom node generated by mecha10 CLI.

mod config;

pub use config::{pascal_name}Config;
use mecha10_core::prelude::*;
use mecha10_core::topics::Topic;

/// Message published by this node
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HelloMessage {{
    pub message: String,
    pub count: u64,
}}

impl Message for HelloMessage {{}}

/// Topic for hello messages
pub const HELLO_TOPIC: Topic<HelloMessage> = Topic::new("/{name}/hello");

/// {pascal_name} node
#[derive(Debug, Node)]
#[node(name = "{name}")]
pub struct {pascal_name}Node {{
    config: {pascal_name}Config,
    count: u64,
}}

#[async_trait]
impl NodeImpl for {pascal_name}Node {{
    type Config = {pascal_name}Config;

    async fn init(config: Self::Config) -> Result<Self> {{
        info!("Initializing {name} node (rate: {{}} Hz)", config.rate_hz);
        Ok(Self {{ config, count: 0 }})
    }}

    async fn run(&mut self, ctx: &Context) -> Result<()> {{
        let interval_ms = (1000.0 / self.config.rate_hz) as u64;
        let mut interval = tokio::time::interval(tokio::time::Duration::from_millis(interval_ms));

        info!("{pascal_name} node running");

        loop {{
            interval.tick().await;

            self.count += 1;

            let message = HelloMessage {{
                message: format!("Hello from {name} #{{}}", self.count),
                count: self.count,
            }};

            ctx.publish_to(HELLO_TOPIC, &message).await?;
            info!("Published: {{}}", message.message);
        }}
    }}
}}
"#,
            pascal_name = pascal_name,
            name = name,
        );

        tokio::fs::write(src_dir.join("lib.rs"), content).await?;
        Ok(())
    }

    /// Create src/main.rs binary entrypoint
    async fn create_main_rs(&self, src_dir: &Path, name: &str) -> Result<()> {
        let crate_name = self.to_snake_case(name);
        let content = format!(
            r#"//! {name} Node Binary
//!
//! Runs the {name} node as a standalone binary.

use mecha10_core::prelude::*;

#[tokio::main]
async fn main() -> anyhow::Result<()> {{
    init_logging();

    // Uses auto-generated run() from #[derive(Node)]
    {crate_name}::run().await.map_err(|e| anyhow::anyhow!(e))
}}
"#,
            name = name,
            crate_name = crate_name,
        );

        tokio::fs::write(src_dir.join("main.rs"), content).await?;
        Ok(())
    }

    /// Create src/config.rs with configuration struct
    async fn create_config_rs(&self, src_dir: &Path, name: &str) -> Result<()> {
        let pascal_name = self.to_pascal_case(name);
        let content = format!(
            r#"//! {name} node configuration

use mecha10_core::prelude::*;

/// {pascal_name} node configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct {pascal_name}Config {{
    /// Rate at which to publish messages (Hz)
    #[serde(default = "default_rate_hz")]
    pub rate_hz: f32,
}}

fn default_rate_hz() -> f32 {{
    1.0
}}

impl Default for {pascal_name}Config {{
    fn default() -> Self {{
        Self {{
            rate_hz: default_rate_hz(),
        }}
    }}
}}
"#,
            pascal_name = pascal_name,
            name = name,
        );

        tokio::fs::write(src_dir.join("config.rs"), content).await?;
        Ok(())
    }

    /// Create configs/nodes/@local/<name>/config.json with dev/production sections
    async fn create_config_json(&self, config_dir: &Path) -> Result<()> {
        let content = r#"{
  "dev": {
    "rate_hz": 1.0,
    "topics": {
      "publishes": [],
      "subscribes": []
    }
  },
  "production": {
    "rate_hz": 1.0,
    "topics": {
      "publishes": [],
      "subscribes": []
    }
  }
}
"#;

        tokio::fs::write(config_dir.join("config.json"), content).await?;
        Ok(())
    }

    /// Update mecha10.json to add the new node
    async fn update_mecha10_json(&self, project_root: &Path, name: &str, _description: Option<&str>) -> Result<()> {
        let config_path = project_root.join(paths::PROJECT_CONFIG);

        // Read existing config
        let content = tokio::fs::read_to_string(&config_path)
            .await
            .context("Failed to read mecha10.json")?;

        let mut config: serde_json::Value = serde_json::from_str(&content).context("Failed to parse mecha10.json")?;

        // Node identifier: just the name (no @local/ prefix)
        let node_identifier = name.to_string();

        // Add to nodes array
        if let Some(nodes) = config.get_mut("nodes") {
            if let Some(arr) = nodes.as_array_mut() {
                // Check if node already exists
                let exists = arr.iter().any(|n| n.as_str() == Some(&node_identifier));
                if exists {
                    anyhow::bail!("Node '{}' already exists in mecha10.json", name);
                }
                arr.push(serde_json::Value::String(node_identifier.clone()));
            }
        } else {
            // Create nodes section
            config["nodes"] = serde_json::json!([node_identifier.clone()]);
        }

        // Also add to lifecycle.modes.dev.nodes for immediate use in dev mode
        if let Some(lifecycle) = config.get_mut("lifecycle") {
            if let Some(modes) = lifecycle.get_mut("modes") {
                if let Some(dev) = modes.get_mut("dev") {
                    if let Some(dev_nodes) = dev.get_mut("nodes") {
                        if let Some(arr) = dev_nodes.as_array_mut() {
                            let exists = arr.iter().any(|n| n.as_str() == Some(&node_identifier));
                            if !exists {
                                arr.push(serde_json::Value::String(node_identifier));
                            }
                        }
                    }
                }
            }
        }

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

        Ok(())
    }

    /// Update Cargo.toml workspace members to include the new node
    async fn update_cargo_workspace(&self, project_root: &Path, name: &str) -> Result<()> {
        let cargo_path = project_root.join(paths::rust::CARGO_TOML);

        // Read existing Cargo.toml
        let content = tokio::fs::read_to_string(&cargo_path)
            .await
            .context("Failed to read Cargo.toml")?;

        let mut doc: toml::Value = content.parse().context("Failed to parse Cargo.toml")?;

        // Get or create workspace.members array
        let node_path = format!("nodes/{}", name);

        if let Some(workspace) = doc.get_mut("workspace") {
            if let Some(members) = workspace.get_mut("members") {
                if let Some(arr) = members.as_array_mut() {
                    // Check if already exists
                    let exists = arr.iter().any(|m| m.as_str() == Some(&node_path));
                    if !exists {
                        arr.push(toml::Value::String(node_path));
                    }
                }
            } else {
                // Create members array
                workspace.as_table_mut().unwrap().insert(
                    "members".to_string(),
                    toml::Value::Array(vec![toml::Value::String(node_path)]),
                );
            }
        } else {
            // Create workspace section with members
            let mut workspace_table = toml::map::Map::new();
            workspace_table.insert(
                "members".to_string(),
                toml::Value::Array(vec![toml::Value::String(node_path)]),
            );
            doc.as_table_mut()
                .unwrap()
                .insert("workspace".to_string(), toml::Value::Table(workspace_table));
        }

        // Write updated Cargo.toml
        let updated_content = toml::to_string_pretty(&doc)?;
        tokio::fs::write(&cargo_path, updated_content).await?;

        Ok(())
    }
}

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