use std::path::Path;
use std::str::FromStr;

use serde::{Deserialize, Serialize};
use vapora_channels::config::ChannelConfig;

use crate::error::{ConfigError, Result};

#[derive(Debug, Clone, Deserialize)]
pub struct WorkflowsConfig {
    pub engine: EngineConfig,
    pub workflows: Vec<WorkflowConfig>,
    /// Outbound notification channels keyed by name. Absent from TOML → no
    /// notifications sent. Each entry becomes a channel in `ChannelRegistry`.
    #[serde(default)]
    pub channels: std::collections::HashMap<String, ChannelConfig>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct EngineConfig {
    pub max_parallel_tasks: usize,
    pub workflow_timeout: u64,
    pub approval_gates_enabled: bool,
    #[serde(default)]
    pub cedar_policy_dir: Option<String>,
}

/// Per-workflow notification targets, keyed by event type.
///
/// Each field is a list of channel names registered in `[channels]`.
///
/// ```toml
/// [[workflows]]
/// name = "deploy-prod"
/// trigger = "schedule"
///
/// [workflows.notifications]
/// on_completed          = ["team-slack"]
/// on_failed             = ["team-slack", "ops-telegram"]
/// on_approval_required  = ["team-slack"]
/// ```
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct WorkflowNotifications {
    #[serde(default)]
    pub on_completed: Vec<String>,
    #[serde(default)]
    pub on_failed: Vec<String>,
    #[serde(default)]
    pub on_approval_required: Vec<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct WorkflowConfig {
    pub name: String,
    pub trigger: String,
    pub stages: Vec<StageConfig>,
    #[serde(default)]
    pub schedule: Option<ScheduleConfig>,
    #[serde(default)]
    pub notifications: WorkflowNotifications,
}

/// Cron-based scheduling configuration for `trigger = "schedule"` workflows.
#[derive(Debug, Clone, Deserialize)]
pub struct ScheduleConfig {
    /// 5-field standard cron (`min hour dom month dow`) or 7-field cron crate
    /// format (`sec min hour dom month dow year`).
    pub cron: String,
    /// IANA timezone identifier for cron evaluation (e.g.
    /// `"America/New_York"`). Defaults to UTC when absent.
    #[serde(default)]
    pub timezone: Option<String>,
    /// Allow a new instance to start while a previous one is still running.
    #[serde(default)]
    pub allow_concurrent: bool,
    /// Fire all missed slots on restart (capped at 10). When false, only the
    /// next slot is fired and missed slots are discarded.
    #[serde(default)]
    pub catch_up: bool,
    /// Optional JSON object merged into each triggered workflow's initial
    /// context.
    #[serde(default)]
    pub initial_context: Option<serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StageConfig {
    pub name: String,
    pub agents: Vec<String>,
    #[serde(default)]
    pub parallel: bool,
    #[serde(default)]
    pub max_parallel: Option<usize>,
    #[serde(default)]
    pub approval_required: bool,
    /// Agent roles that receive a compensation task if this stage needs to be
    /// rolled back via Saga.
    #[serde(default)]
    pub compensation_agents: Option<Vec<String>>,
}

impl WorkflowsConfig {
    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
        let path = path.as_ref();

        let (raw, is_json) = if path.extension().and_then(|e| e.to_str()) == Some("ncl") {
            let out = std::process::Command::new("nickel")
                .args(["export", "--format", "json"])
                .arg(path)
                .output()
                .map_err(|e| {
                    ConfigError::IoError(std::io::Error::other(format!(
                        "Failed to invoke nickel for {:?}: {}",
                        path, e
                    )))
                })?;
            if !out.status.success() {
                let stderr = String::from_utf8_lossy(&out.stderr);
                return Err(ConfigError::IoError(std::io::Error::other(format!(
                    "nickel export failed for {:?}: {}",
                    path, stderr
                )))
                .into());
            }
            let json = String::from_utf8(out.stdout).map_err(|e| {
                ConfigError::IoError(std::io::Error::other(format!(
                    "nickel output is not valid UTF-8: {}",
                    e
                )))
            })?;
            (json, true)
        } else {
            let content = std::fs::read_to_string(path).map_err(ConfigError::IoError)?;
            (content, false)
        };

        let interpolated = interpolate_env_vars(&raw);

        let config: WorkflowsConfig = if is_json {
            serde_json::from_str(&interpolated).map_err(ConfigError::ParseJson)?
        } else {
            toml::from_str(&interpolated).map_err(|e| {
                ConfigError::IoError(std::io::Error::other(format!(
                    "Failed to parse TOML: {}",
                    e
                )))
            })?
        };

        config.validate()?;
        Ok(config)
    }

    pub fn validate(&self) -> Result<()> {
        if self.workflows.is_empty() {
            return Err(ConfigError::Invalid("No workflows defined".to_string()).into());
        }

        for workflow in &self.workflows {
            if workflow.stages.is_empty() {
                return Err(ConfigError::Invalid(format!(
                    "Workflow '{}' has no stages",
                    workflow.name
                ))
                .into());
            }

            for stage in &workflow.stages {
                if stage.agents.is_empty() {
                    return Err(ConfigError::Invalid(format!(
                        "Stage '{}' in workflow '{}' has no agents",
                        stage.name, workflow.name
                    ))
                    .into());
                }
            }

            // Trigger/schedule alignment: "schedule" requires a [schedule] block
            // and a parseable cron expression (fail-fast rather than silently
            // skipping at runtime).
            if workflow.trigger == "schedule" {
                validate_schedule_config(&workflow.name, &workflow.schedule)?;
            }
        }

        Ok(())
    }

    pub fn get_workflow(&self, name: &str) -> Option<&WorkflowConfig> {
        self.workflows.iter().find(|w| w.name == name)
    }
}

/// Expand every `${VAR}` / `${VAR:-default}` reference in `content`.
/// Unresolved vars without a default are replaced with an empty string.
fn interpolate_env_vars(content: &str) -> String {
    let mut result = String::with_capacity(content.len());
    let mut remaining = content;
    while let Some(start) = remaining.find("${") {
        result.push_str(&remaining[..start]);
        let after_open = &remaining[start + 2..];
        if let Some(close) = after_open.find('}') {
            let var_expr = &after_open[..close];
            let value = if let Some(sep) = var_expr.find(":-") {
                let var_name = &var_expr[..sep];
                let default_val = &var_expr[sep + 2..];
                std::env::var(var_name).unwrap_or_else(|_| default_val.to_string())
            } else {
                std::env::var(var_expr).unwrap_or_default()
            };
            result.push_str(&value);
            remaining = &after_open[close + 1..];
        } else {
            result.push_str("${");
            remaining = after_open;
        }
    }
    result.push_str(remaining);
    result
}

fn validate_schedule_config(
    workflow_name: &str,
    schedule: &Option<ScheduleConfig>,
) -> std::result::Result<(), ConfigError> {
    let sc = schedule.as_ref().ok_or_else(|| {
        ConfigError::Invalid(format!(
            "Workflow '{}' has trigger = \"schedule\" but no [schedule] block",
            workflow_name
        ))
    })?;

    // Validate cron expression (inline normalisation avoids circular dep with
    // schedule.rs).
    let normalized = match sc.cron.split_whitespace().count() {
        5 => format!("0 {} *", sc.cron),
        6 => format!("{} *", sc.cron),
        _ => sc.cron.clone(),
    };
    cron::Schedule::from_str(&normalized).map_err(|e| {
        ConfigError::Invalid(format!(
            "Workflow '{}' has invalid cron '{}': {}",
            workflow_name, sc.cron, e
        ))
    })?;

    // Validate timezone when provided.
    if let Some(tz) = &sc.timezone {
        tz.parse::<chrono_tz::Tz>().map_err(|_| {
            ConfigError::Invalid(format!(
                "Workflow '{}' has invalid timezone '{}': not a valid IANA identifier",
                workflow_name, tz
            ))
        })?;
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_workflow_config() {
        let toml_str = r#"
[engine]
max_parallel_tasks = 10
workflow_timeout = 3600
approval_gates_enabled = true

[[workflows]]
name = "test_workflow"
trigger = "manual"

[[workflows.stages]]
name = "design"
agents = ["architect"]
parallel = false
approval_required = false

[[workflows.stages]]
name = "implementation"
agents = ["developer", "developer"]
parallel = true
max_parallel = 2
approval_required = false
        "#;

        let config: WorkflowsConfig = toml::from_str(toml_str).unwrap();

        assert_eq!(config.engine.max_parallel_tasks, 10);
        assert_eq!(config.workflows.len(), 1);
        assert_eq!(config.workflows[0].name, "test_workflow");
        assert_eq!(config.workflows[0].stages.len(), 2);
        assert_eq!(config.workflows[0].stages[0].name, "design");
        assert!(!config.workflows[0].stages[0].parallel);
        assert!(config.workflows[0].stages[1].parallel);
    }

    #[test]
    fn test_validation_no_workflows() {
        let config = WorkflowsConfig {
            engine: EngineConfig {
                max_parallel_tasks: 10,
                workflow_timeout: 3600,
                approval_gates_enabled: true,
                cedar_policy_dir: None,
            },
            workflows: vec![],
            channels: std::collections::HashMap::new(),
        };

        assert!(config.validate().is_err());
    }

    #[test]
    fn test_validation_no_stages() {
        let toml_str = r#"
[engine]
max_parallel_tasks = 10
workflow_timeout = 3600
approval_gates_enabled = true

[[workflows]]
name = "test_workflow"
trigger = "manual"
stages = []
        "#;

        let config: WorkflowsConfig = toml::from_str(toml_str).unwrap();
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_get_workflow() {
        let toml_str = r#"
[engine]
max_parallel_tasks = 10
workflow_timeout = 3600
approval_gates_enabled = true

[[workflows]]
name = "workflow_a"
trigger = "manual"

[[workflows.stages]]
name = "stage1"
agents = ["agent1"]

[[workflows]]
name = "workflow_b"
trigger = "manual"

[[workflows.stages]]
name = "stage2"
agents = ["agent2"]
        "#;

        let config: WorkflowsConfig = toml::from_str(toml_str).unwrap();

        assert!(config.get_workflow("workflow_a").is_some());
        assert!(config.get_workflow("workflow_b").is_some());
        assert!(config.get_workflow("nonexistent").is_none());
    }

    #[test]
    fn test_schedule_trigger_valid() {
        let toml_str = r#"
[engine]
max_parallel_tasks = 4
workflow_timeout = 3600
approval_gates_enabled = false

[[workflows]]
name = "nightly_analysis"
trigger = "schedule"

[workflows.schedule]
cron = "0 2 * * *"
allow_concurrent = false
catch_up = false

[[workflows.stages]]
name = "analyze"
agents = ["analyst"]
        "#;

        let config: WorkflowsConfig = toml::from_str(toml_str).unwrap();
        assert!(
            config.validate().is_ok(),
            "Valid schedule config should pass"
        );
        let wf = config.get_workflow("nightly_analysis").unwrap();
        let sc = wf.schedule.as_ref().unwrap();
        assert_eq!(sc.cron, "0 2 * * *");
        assert!(!sc.allow_concurrent);
        assert!(!sc.catch_up);
    }

    #[test]
    fn test_schedule_trigger_missing_block() {
        let toml_str = r#"
[engine]
max_parallel_tasks = 4
workflow_timeout = 3600
approval_gates_enabled = false

[[workflows]]
name = "no_schedule_block"
trigger = "schedule"

[[workflows.stages]]
name = "work"
agents = ["worker"]
        "#;

        let config: WorkflowsConfig = toml::from_str(toml_str).unwrap();
        assert!(
            config.validate().is_err(),
            "schedule trigger without [schedule] block must fail validation"
        );
    }

    #[test]
    fn test_schedule_trigger_invalid_cron() {
        let toml_str = r#"
[engine]
max_parallel_tasks = 4
workflow_timeout = 3600
approval_gates_enabled = false

[[workflows]]
name = "bad_cron"
trigger = "schedule"

[workflows.schedule]
cron = "not a valid cron expression"

[[workflows.stages]]
name = "work"
agents = ["worker"]
        "#;

        let config: WorkflowsConfig = toml::from_str(toml_str).unwrap();
        assert!(
            config.validate().is_err(),
            "Invalid cron expression must fail validation"
        );
    }
}
