//! 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 behaviors: Option<BehaviorsConfig>,
    #[serde(default)]
    pub services: ServicesConfig,
    #[serde(default)]
    pub docker: DockerServicesConfig,
    #[serde(default)]
    pub redis: RedisConfig,
    #[serde(default)]
    pub dashboard: DashboardConfig,
    #[serde(default)]
    pub networking: NetworkingConfig,
    #[serde(default)]
    pub environments: EnvironmentsConfig,
}

#[derive(Debug, Serialize, Deserialize, Default)]
pub struct NodesConfig {
    #[serde(default)]
    pub drivers: Vec<NodeEntry>,
    #[serde(default)]
    pub custom: Vec<NodeEntry>,
}

/// 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,

    /// Directory containing behavior tree templates (relative to project root)
    #[serde(default = "default_behaviors_templates_dir")]
    pub templates_dir: String,

    /// Directory containing behavior configuration files (relative to project root)
    #[serde(default = "default_behaviors_configs_dir")]
    pub configs_dir: String,

    /// Executor configuration
    #[serde(default)]
    pub executor: BehaviorExecutorConfig,
}

/// Behavior executor configuration
///
/// Controls how the behavior tree executor runs.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BehaviorExecutorConfig {
    /// Tick rate in Hz (how many times per second to tick the behavior tree)
    #[serde(default = "default_tick_rate_hz")]
    pub tick_rate_hz: f32,

    /// Maximum number of ticks before stopping (None = run forever)
    #[serde(default)]
    pub max_ticks: Option<usize>,

    /// Whether to log execution statistics
    #[serde(default = "default_log_stats")]
    pub log_stats: bool,
}

impl Default for BehaviorExecutorConfig {
    fn default() -> Self {
        Self {
            tick_rate_hz: default_tick_rate_hz(),
            max_ticks: None,
            log_stats: default_log_stats(),
        }
    }
}

fn default_behaviors_templates_dir() -> String {
    "behaviors".to_string()
}

fn default_behaviors_configs_dir() -> String {
    "configs".to_string()
}

fn default_tick_rate_hz() -> f32 {
    10.0
}

fn default_log_stats() -> bool {
    true
}

#[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, Clone)]
#[allow(dead_code)]
pub struct NodeEntry {
    pub name: String,
    pub path: String,
    #[serde(default)]
    pub config: Option<serde_json::Value>,
    #[serde(default)]
    pub description: Option<String>,
    #[serde(default)]
    pub run_target: Option<String>,
    #[serde(default = "default_enabled")]
    pub enabled: bool,
}

#[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 {
    /// Whether simulation is enabled for this project
    #[serde(default = "default_simulation_enabled")]
    pub enabled: bool,

    /// Config profile to use (dev, production, staging, etc.)
    #[serde(default = "default_config_profile")]
    pub config_profile: String,

    /// 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>,

    // DEPRECATED: Legacy fields for backwards compatibility
    // These are ignored if config_profile is set
    #[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>,
}

fn default_simulation_enabled() -> bool {
    true
}

fn default_config_profile() -> String {
    "dev".to_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 and which should stop.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ModeConfig {
    /// Human-readable description of this mode
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,

    /// Nodes that should run in this mode
    pub nodes: Vec<String>,

    /// Nodes that should be stopped when entering this mode (optional)
    /// This is useful for explicitly stopping nodes that conflict with this mode.
    #[serde(default)]
    pub stop_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(", ")
                ));
            }
        }

        // Check stop_nodes exist
        for node in &self.stop_nodes {
            if !available_nodes.contains(node) {
                return Err(anyhow::anyhow!(
                    "Mode '{}': stop_node '{}' not found in project configuration. Available nodes: {}",
                    mode_name,
                    node,
                    available_nodes.join(", ")
                ));
            }
        }

        Ok(())
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct RedisConfig {
    #[serde(default = "default_redis_url")]
    pub url: String,
}

/// 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,
}

impl Default for RedisConfig {
    fn default() -> Self {
        Self {
            url: default_redis_url(),
        }
    }
}

fn default_redis_url() -> String {
    "redis://localhost:6379".to_string()
}

/// Dashboard configuration
///
/// Controls the dashboard URL for the project.
/// Can be overridden via MECHA10_DASHBOARD_URL environment variable.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DashboardConfig {
    /// Dashboard URL
    /// Default: https://dashboard.mecha.industries
    /// Override with MECHA10_DASHBOARD_URL env var for local development
    #[serde(default = "default_dashboard_url")]
    pub url: String,
}

impl Default for DashboardConfig {
    fn default() -> Self {
        Self {
            url: default_dashboard_url(),
        }
    }
}

impl DashboardConfig {
    /// Get the effective dashboard URL
    ///
    /// Priority:
    /// 1. MECHA10_DASHBOARD_URL environment variable
    /// 2. Config file value
    /// 3. Default (https://dashboard.mecha.industries)
    pub fn effective_url(&self) -> String {
        std::env::var("MECHA10_DASHBOARD_URL").unwrap_or_else(|_| self.url.clone())
    }
}

fn default_dashboard_url() -> String {
    "https://dashboard.mecha.industries".to_string()
}

/// Networking configuration for WebRTC relay and signaling
///
/// Centralizes relay URL configuration for simulation-bridge and other nodes.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct NetworkingConfig {
    /// WebRTC relay configuration
    #[serde(default)]
    pub relay: RelayConfig,
}

impl Default for NetworkingConfig {
    fn default() -> Self {
        Self {
            relay: RelayConfig::default(),
        }
    }
}

/// WebRTC relay configuration
///
/// The relay URL is used by simulation-bridge in client mode to connect
/// to a remote signaling server for WebRTC camera streaming.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct RelayConfig {
    /// WebRTC relay URL for signaling
    /// Default: wss://api.mecha.industries/webrtc-relay
    /// Override with WEBRTC_RELAY_URL env var
    #[serde(default = "default_relay_url")]
    pub url: String,
}

impl Default for RelayConfig {
    fn default() -> Self {
        Self {
            url: default_relay_url(),
        }
    }
}

impl RelayConfig {
    /// Get the effective relay URL
    ///
    /// Priority:
    /// 1. WEBRTC_RELAY_URL environment variable
    /// 2. Config file value
    /// 3. Default (wss://api.mecha.industries/webrtc-relay)
    pub fn effective_url(&self) -> String {
        std::env::var("WEBRTC_RELAY_URL").unwrap_or_else(|_| self.url.clone())
    }
}

fn default_relay_url() -> String {
    "wss://api.mecha.industries/webrtc-relay".to_string()
}

/// 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())
    }
}

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

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

fn default_prod_environment() -> EnvironmentConfig {
    EnvironmentConfig {
        control_plane: "wss://api.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_enabled() -> bool {
    true
}

fn default_compose_file() -> String {
    "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)
}
