//! Project configuration types
//!
//! Types for loading and managing mecha10.json project configuration files.

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

#[derive(Debug, Serialize, Deserialize)]
pub struct ProjectConfig {
    pub name: String,
    pub version: String,
    pub robot: RobotConfig,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub simulation: Option<ProjectSimulationConfig>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub lifecycle: Option<LifecycleConfig>,
    #[serde(default)]
    pub nodes: NodesConfig,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub targets: Option<TargetsConfig>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub behaviors: Option<BehaviorsConfig>,
    #[serde(default)]
    pub services: ServicesConfig,
    #[serde(default)]
    pub docker: DockerServicesConfig,
    #[serde(default)]
    pub environments: EnvironmentsConfig,
}

// ============================================================================
// Node Configuration Types
// ============================================================================

/// Node source type based on namespace prefix
#[derive(Debug, Clone, PartialEq)]
pub enum NodeSource {
    /// Framework node bundled in CLI (@mecha10/*)
    Framework,
    /// Local project node (@local/*)
    Project,
    /// Third-party registry node (@<org>/*)
    Registry(String),
}

/// Parsed node specification from namespaced string format
///
/// Node identifiers follow this format:
/// - `@mecha10/listener` - Framework node (bundled in CLI)
/// - `my-custom` - Project node (built from local workspace, no prefix needed)
/// - `@local/my-custom` - Project node (legacy format, still supported)
/// - `@someorg/cool-node` - Registry node (future, from npm-like registry)
///
/// Config paths follow the identifier:
/// - `configs/nodes/@mecha10/listener/config.json`
/// - `configs/nodes/my-custom/config.json`
#[derive(Debug, Clone)]
pub struct NodeSpec {
    /// Node name (derived from identifier, used in lifecycle modes)
    pub name: String,
    /// Full namespaced identifier
    #[allow(dead_code)] // API for future use (registry lookups)
    pub identifier: String,
    /// Node source type
    pub source: NodeSource,
}

impl NodeSpec {
    /// Parse a node identifier string into a NodeSpec
    ///
    /// # Examples
    ///
    /// ```
    /// let spec = NodeSpec::parse("@mecha10/listener").unwrap();
    /// assert_eq!(spec.name, "listener");
    /// assert_eq!(spec.source, NodeSource::Framework);
    ///
    /// let spec = NodeSpec::parse("my-custom").unwrap();
    /// assert_eq!(spec.name, "my-custom");
    /// assert_eq!(spec.source, NodeSource::Project);
    ///
    /// // Legacy format still supported
    /// let spec = NodeSpec::parse("@local/my-custom").unwrap();
    /// assert_eq!(spec.name, "my-custom");
    /// assert_eq!(spec.source, NodeSource::Project);
    /// ```
    pub fn parse(identifier: &str) -> Result<Self> {
        let identifier = identifier.trim();

        // Plain name without @ prefix = project node
        if !identifier.starts_with('@') {
            if identifier.is_empty() {
                anyhow::bail!("Empty node identifier");
            }
            return Ok(Self {
                name: identifier.to_string(),
                identifier: identifier.to_string(),
                source: NodeSource::Project,
            });
        }

        let without_at = identifier.strip_prefix('@').unwrap();
        let parts: Vec<&str> = without_at.splitn(2, '/').collect();

        if parts.len() != 2 {
            anyhow::bail!(
                "Invalid node identifier '{}'. Expected @<scope>/<name> format",
                identifier
            );
        }

        let scope = parts[0];
        let name = parts[1].to_string();

        if scope.is_empty() || name.is_empty() {
            anyhow::bail!("Empty scope or name in identifier: {}", identifier);
        }

        let source = match scope {
            "mecha10" => NodeSource::Framework,
            "local" => NodeSource::Project,
            org => NodeSource::Registry(org.to_string()),
        };

        Ok(Self {
            name,
            identifier: identifier.to_string(),
            source,
        })
    }

    /// Check if this is a framework node
    pub fn is_framework(&self) -> bool {
        matches!(self.source, NodeSource::Framework)
    }

    /// Check if this is a project node
    #[allow(dead_code)] // API for future use
    pub fn is_project(&self) -> bool {
        matches!(self.source, NodeSource::Project)
    }

    /// Get the package/crate path for this node
    ///
    /// - Framework: mecha10-nodes-{name}
    /// - Project: nodes/{name}
    /// - Registry: node_modules/@{org}/{name} (future)
    pub fn package_path(&self) -> String {
        match &self.source {
            NodeSource::Framework => format!("mecha10-nodes-{}", self.name),
            NodeSource::Project => format!("nodes/{}", self.name),
            NodeSource::Registry(org) => format!("node_modules/@{}/{}", org, self.name),
        }
    }

    /// Get the config directory path for this node (relative to project root)
    ///
    /// Config path follows the identifier structure:
    /// - `@mecha10/listener` → `configs/nodes/@mecha10/listener/`
    /// - `my-custom` → `configs/nodes/my-custom/`
    /// - `@local/my-custom` → `configs/nodes/my-custom/` (legacy: uses name only)
    /// - `@someorg/node` → `configs/nodes/@someorg/node/`
    #[allow(dead_code)] // API for future use
    pub fn config_dir(&self) -> String {
        match &self.source {
            NodeSource::Framework => format!("configs/nodes/{}", self.identifier),
            NodeSource::Project => format!("configs/nodes/{}", self.name),
            NodeSource::Registry(_) => format!("configs/nodes/{}", self.identifier),
        }
    }
}

/// Nodes configuration - array of namespaced node identifiers
///
/// Node identifiers follow this format:
/// - `@mecha10/listener` - Framework node (bundled in CLI)
/// - `@local/my-custom` - Project node (built from local workspace)
/// - `@someorg/cool-node` - Registry node (future, from npm-like registry)
///
/// Example:
/// ```json
/// {
///   "nodes": [
///     "@mecha10/listener",
///     "@mecha10/speaker",
///     "@local/my-custom"
///   ]
/// }
/// ```
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
#[serde(transparent)]
pub struct NodesConfig(pub Vec<String>);

impl NodesConfig {
    /// Create a new empty nodes config
    #[allow(dead_code)] // API for future use
    pub fn new() -> Self {
        Self(Vec::new())
    }

    /// Get all node specs from the configuration
    pub fn get_node_specs(&self) -> Vec<NodeSpec> {
        self.0.iter().filter_map(|id| NodeSpec::parse(id).ok()).collect()
    }

    /// Get all node names (for lifecycle mode validation)
    pub fn get_node_names(&self) -> Vec<String> {
        self.get_node_specs().iter().map(|s| s.name.clone()).collect()
    }

    /// Find a node by name
    pub fn find_by_name(&self, name: &str) -> Option<NodeSpec> {
        self.get_node_specs().into_iter().find(|s| s.name == name)
    }

    /// Check if a node exists
    pub fn contains(&self, name: &str) -> bool {
        self.find_by_name(name).is_some()
    }

    /// Add a node to the configuration
    pub fn add_node(&mut self, identifier: &str) {
        if !self.0.contains(&identifier.to_string()) {
            self.0.push(identifier.to_string());
        }
    }

    /// Remove a node from the configuration
    #[allow(dead_code)] // API for future use
    pub fn remove_node(&mut self, name: &str) {
        self.0
            .retain(|id| NodeSpec::parse(id).map(|s| s.name != name).unwrap_or(true));
    }

    /// Get the raw list of identifiers
    #[allow(dead_code)] // API for future use
    pub fn identifiers(&self) -> &[String] {
        &self.0
    }

    /// Check if empty
    #[allow(dead_code)] // API for future use
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    /// Get count of nodes
    #[allow(dead_code)] // API for future use
    pub fn len(&self) -> usize {
        self.0.len()
    }
}

// ============================================================================
// Targets Configuration Types
// ============================================================================

/// Target execution configuration - defines where nodes run
///
/// Nodes can run either on the robot (as local processes) or remotely
/// (in Docker containers). This configuration determines the execution
/// target for each node.
///
/// Example:
/// ```json
/// {
///   "targets": {
///     "robot": ["@mecha10/motor", "@mecha10/camera", "@mecha10/websocket-bridge"],
///     "remote": ["@mecha10/object-detector", "@mecha10/image-classifier"]
///   }
/// }
/// ```
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct TargetsConfig {
    /// Nodes that run on the robot as local processes
    /// These are typically hardware drivers and low-latency control nodes
    #[serde(default)]
    pub robot: Vec<String>,

    /// Nodes that run remotely in Docker containers
    /// These are typically AI/ML nodes with platform-specific dependencies
    #[serde(default)]
    pub remote: Vec<String>,
}

/// Target type for node execution
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)] // Will be used by remote CLI commands
pub enum NodeTarget {
    /// Run as local process on robot
    Robot,
    /// Run in remote Docker container
    Remote,
    /// No explicit target - runs as local process (default)
    Default,
}

#[allow(dead_code)] // Will be used by remote CLI commands
impl TargetsConfig {
    /// Get the target for a given node identifier
    ///
    /// # Arguments
    ///
    /// * `node_id` - Node identifier (e.g., "@mecha10/object-detector")
    ///
    /// # Returns
    ///
    /// The target type for the node
    pub fn get_target(&self, node_id: &str) -> NodeTarget {
        if self.robot.iter().any(|n| n == node_id) {
            NodeTarget::Robot
        } else if self.remote.iter().any(|n| n == node_id) {
            NodeTarget::Remote
        } else {
            NodeTarget::Default
        }
    }

    /// Check if a node should run remotely
    pub fn is_remote(&self, node_id: &str) -> bool {
        self.remote.iter().any(|n| n == node_id)
    }

    /// Check if a node should run on robot
    pub fn is_robot(&self, node_id: &str) -> bool {
        self.robot.iter().any(|n| n == node_id)
    }

    /// Get all remote node identifiers
    pub fn remote_nodes(&self) -> &[String] {
        &self.remote
    }

    /// Get all robot node identifiers
    #[allow(dead_code)]
    pub fn robot_nodes(&self) -> &[String] {
        &self.robot
    }

    /// Check if there are any remote nodes configured
    pub fn has_remote_nodes(&self) -> bool {
        !self.remote.is_empty()
    }

    /// Check if any remote nodes are custom (@local/*) vs framework (@mecha10/*)
    pub fn has_custom_remote_nodes(&self) -> bool {
        self.remote.iter().any(|n| n.starts_with("@local/") || !n.starts_with('@'))
    }

    /// Get only the framework remote nodes (@mecha10/*)
    pub fn framework_remote_nodes(&self) -> Vec<&str> {
        self.remote
            .iter()
            .filter(|n| n.starts_with("@mecha10/"))
            .map(|s| s.as_str())
            .collect()
    }

    /// Get sorted remote nodes for hashing
    pub fn sorted_remote_nodes(&self) -> Vec<String> {
        let mut nodes = self.remote.clone();
        nodes.sort();
        nodes
    }
}

/// Behavior tree configuration
///
/// Controls behavior tree execution for autonomous robot behaviors.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BehaviorsConfig {
    /// Name of the active behavior to run (e.g., "idle_wander")
    pub active: String,
}

#[derive(Debug, Serialize, Deserialize, Default)]
pub struct ServicesConfig {
    #[serde(default)]
    pub http_api: Option<HttpApiServiceConfig>,
    #[serde(default)]
    pub database: Option<DatabaseServiceConfig>,
    #[serde(default)]
    pub job_processor: Option<JobProcessorServiceConfig>,
    #[serde(default)]
    pub scheduler: Option<SchedulerServiceConfig>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct HttpApiServiceConfig {
    pub host: String,
    pub port: u16,
    #[serde(default = "default_enable_cors")]
    pub _enable_cors: bool,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DatabaseServiceConfig {
    pub url: String,
    #[serde(default = "default_max_connections")]
    pub max_connections: u32,
    #[serde(default = "default_timeout_seconds")]
    pub timeout_seconds: u64,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct JobProcessorServiceConfig {
    #[serde(default = "default_worker_count")]
    pub worker_count: usize,
    #[serde(default = "default_max_queue_size")]
    pub max_queue_size: usize,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SchedulerServiceConfig {
    pub tasks: Vec<ScheduledTaskConfig>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[allow(dead_code)]
pub struct ScheduledTaskConfig {
    pub name: String,
    pub cron: String,
    pub topic: String,
    pub payload: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct RobotConfig {
    pub id: String,
    #[serde(default)]
    pub platform: Option<String>,
    #[serde(default)]
    pub description: Option<String>,
}

/// Project-level simulation configuration stored in mecha10.json
///
/// This controls simulation settings for the project, distinct from
/// the runtime simulation config loaded from configs/{profile}/simulation/.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ProjectSimulationConfig {
    /// Optional scenario name (loads configs/{profile}/simulation/{scenario}.json)
    /// If not specified, loads configs/{profile}/simulation/config.json
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub scenario: Option<String>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub model: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub model_config: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub environment: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub environment_config: Option<String>,
}

/// Lifecycle configuration for mode-based node management
///
/// Enables dynamic node lifecycle based on operational modes.
/// Each mode defines which nodes should run in that context.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct LifecycleConfig {
    /// Available operational modes
    pub modes: std::collections::HashMap<String, ModeConfig>,

    /// Default mode on startup
    #[serde(default = "default_lifecycle_mode")]
    pub default_mode: String,
}

/// Configuration for a single operational mode
///
/// Defines which nodes should run in this mode.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ModeConfig {
    /// Nodes that should run in this mode
    pub nodes: Vec<String>,
}

fn default_lifecycle_mode() -> String {
    "startup".to_string()
}

// Validation for lifecycle configuration
impl LifecycleConfig {
    /// Validate lifecycle configuration against available nodes
    ///
    /// # Arguments
    ///
    /// * `available_nodes` - List of node names defined in project config
    ///
    /// # Errors
    ///
    /// Returns error if:
    /// - Default mode doesn't exist in modes
    /// - Any mode references undefined nodes
    #[allow(dead_code)] // Will be used in Phase 3
    pub fn validate(&self, available_nodes: &[String]) -> Result<()> {
        // Check that default mode exists
        if !self.modes.contains_key(&self.default_mode) {
            return Err(anyhow::anyhow!(
                "Default mode '{}' not found in modes. Available modes: {}",
                self.default_mode,
                self.modes.keys().map(|k| k.as_str()).collect::<Vec<_>>().join(", ")
            ));
        }

        // Validate each mode
        for (mode_name, mode_config) in &self.modes {
            mode_config.validate(mode_name, available_nodes)?;
        }

        Ok(())
    }
}

impl ModeConfig {
    /// Validate mode configuration against available nodes
    ///
    /// # Arguments
    ///
    /// * `mode_name` - Name of this mode (for error messages)
    /// * `available_nodes` - List of node names defined in project config
    ///
    /// # Errors
    ///
    /// Returns error if any referenced node doesn't exist in project config
    #[allow(dead_code)] // Will be used in Phase 3
    pub fn validate(&self, mode_name: &str, available_nodes: &[String]) -> Result<()> {
        // Check all nodes exist
        for node in &self.nodes {
            if !available_nodes.contains(node) {
                return Err(anyhow::anyhow!(
                    "Mode '{}': node '{}' not found in project configuration. Available nodes: {}",
                    mode_name,
                    node,
                    available_nodes.join(", ")
                ));
            }
        }

        Ok(())
    }
}

/// Docker services configuration for project-level Docker Compose management
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
pub struct DockerServicesConfig {
    /// Services needed for on-robot execution (typically just Redis for message passing)
    #[serde(default)]
    pub robot: Vec<String>,

    /// Services needed for edge/remote execution (Redis + databases, etc.)
    #[serde(default)]
    pub edge: Vec<String>,

    /// Path to docker-compose.yml relative to project root
    #[serde(default = "default_compose_file")]
    pub compose_file: String,

    /// Whether to auto-start services in dev mode
    #[serde(default = "default_auto_start")]
    pub auto_start: bool,
}

/// Environment-specific configuration
///
/// Allows configuring different URLs for dev, staging, and production environments.
/// The active environment is determined by:
/// 1. `MECHA10_ENV` environment variable
/// 2. `--env` CLI flag
/// 3. Default: "dev" when running `mecha10 dev`, "prod" otherwise
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct EnvironmentsConfig {
    /// Development environment (local control plane)
    #[serde(default = "default_dev_environment")]
    pub dev: EnvironmentConfig,

    /// Staging environment (optional)
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub staging: Option<EnvironmentConfig>,

    /// Production environment (cloud control plane)
    #[serde(default = "default_prod_environment")]
    pub prod: EnvironmentConfig,
}

impl Default for EnvironmentsConfig {
    fn default() -> Self {
        Self {
            dev: default_dev_environment(),
            staging: None,
            prod: default_prod_environment(),
        }
    }
}

impl EnvironmentsConfig {
    /// Get the environment config for the given environment name
    pub fn get(&self, env: &str) -> Option<&EnvironmentConfig> {
        match env {
            "dev" | "development" => Some(&self.dev),
            "staging" => self.staging.as_ref(),
            "prod" | "production" => Some(&self.prod),
            _ => None,
        }
    }

    /// Get the current environment config based on MECHA10_ENV
    pub fn current(&self) -> &EnvironmentConfig {
        let env = std::env::var("MECHA10_ENV").unwrap_or_else(|_| "dev".to_string());
        self.get(&env).unwrap_or(&self.dev)
    }

    /// Get the control plane URL for the current environment
    pub fn control_plane_url(&self) -> String {
        std::env::var("MECHA10_CONTROL_PLANE_URL").unwrap_or_else(|_| self.current().control_plane.clone())
    }

    /// Get the API URL for the current environment
    /// Derived: {control_plane}/api
    #[allow(dead_code)]
    pub fn api_url(&self) -> String {
        format!("{}/api", self.control_plane_url())
    }

    /// Get the dashboard URL for the current environment
    /// Derived: {control_plane}/dashboard
    pub fn dashboard_url(&self) -> String {
        std::env::var("MECHA10_DASHBOARD_URL").unwrap_or_else(|_| format!("{}/dashboard", self.control_plane_url()))
    }

    /// Get the WebRTC relay URL for the current environment
    /// Derived: ws(s)://{control_plane_host}/webrtc-relay
    pub fn relay_url(&self) -> String {
        if let Ok(url) = std::env::var("WEBRTC_RELAY_URL") {
            return url;
        }

        let control_plane = self.control_plane_url();
        // Convert http(s) to ws(s)
        let ws_url = if control_plane.starts_with("https://") {
            control_plane.replace("https://", "wss://")
        } else if control_plane.starts_with("http://") {
            control_plane.replace("http://", "ws://")
        } else {
            control_plane
        };
        format!("{}/webrtc-relay", ws_url)
    }

    /// Get the Redis URL (always local, env var override)
    pub fn redis_url(&self) -> String {
        std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://localhost:6380".to_string())
    }
}

/// Configuration for a specific environment
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct EnvironmentConfig {
    /// Control plane base URL (e.g., "http://localhost:8100" or "https://mecha.industries/api")
    pub control_plane: String,
}

fn default_dev_environment() -> EnvironmentConfig {
    EnvironmentConfig {
        control_plane: "http://localhost:8100".to_string(),
    }
}

fn default_prod_environment() -> EnvironmentConfig {
    EnvironmentConfig {
        control_plane: "https://mecha.industries".to_string(),
    }
}

// Default value functions
fn default_enable_cors() -> bool {
    true
}

fn default_max_connections() -> u32 {
    10
}

fn default_timeout_seconds() -> u64 {
    30
}

fn default_worker_count() -> usize {
    4
}

fn default_max_queue_size() -> usize {
    100
}

fn default_compose_file() -> String {
    "docker/docker-compose.yml".to_string()
}

fn default_auto_start() -> bool {
    true
}

/// Load robot_id from mecha10.json
#[allow(dead_code)]
pub async fn load_robot_id(config_path: &PathBuf) -> Result<String> {
    let content = tokio::fs::read_to_string(config_path)
        .await
        .context("Failed to read mecha10.json")?;

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

    Ok(config.robot.id)
}

/// Load full project config from mecha10.json
pub async fn load_project_config(config_path: &PathBuf) -> Result<ProjectConfig> {
    let content = tokio::fs::read_to_string(config_path)
        .await
        .context("Failed to read mecha10.json")?;

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

    Ok(config)
}
