// r8r: rust port of n8n.io
// Initial author: Michael Cochran <mcochran@cochranblock.org>
// Built for: Tanner Woodrum, intended cofounder
// License: Unlicense (public domain dedication, see UNLICENSE)

use super::error::FlowError;
use super::types::*;
use serde::{Deserialize, Serialize};
use std::path::Path;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkflowMeta {
    pub name: String,
    #[serde(default = "default_version")]
    pub version: u32,
}

fn default_version() -> u32 { 1 }

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Workflow {
    pub workflow: WorkflowMeta,
    pub nodes: Vec<NodeDef>,
    #[serde(default)]
    pub edges: Vec<Edge>,
}

impl Workflow {
    pub fn from_toml_file<P: AsRef<Path>>(path: P) -> Result<Self, FlowError> {
        let path = path.as_ref();
        let raw = std::fs::read_to_string(path)
            .map_err(|_| FlowError::WorkflowNotFound(path.display().to_string()))?;
        Self::from_toml_str(&raw)
    }

    pub fn from_toml_str(s: &str) -> Result<Self, FlowError> {
        let wf: Workflow = toml::from_str(s)?;
        wf.validate()?;
        Ok(wf)
    }

    pub fn validate(&self) -> Result<(), FlowError> {
        let names: std::collections::HashSet<_> = self.nodes.iter().map(|n| &n.name).collect();
        if names.len() != self.nodes.len() {
            return Err(FlowError::Validation("duplicate node name".into()));
        }
        for e in &self.edges {
            if !names.contains(&e.from) {
                return Err(FlowError::Validation(format!("edge from unknown node `{}`", e.from)));
            }
            if !names.contains(&e.to) {
                return Err(FlowError::Validation(format!("edge to unknown node `{}`", e.to)));
            }
        }
        Ok(())
    }

    pub fn node(&self, name: &str) -> Option<&NodeDef> {
        self.nodes.iter().find(|n| n.name == name)
    }

    pub fn edges_from<'a>(&'a self, node: &'a str, output_index: u8) -> impl Iterator<Item = &'a Edge> + 'a {
        self.edges.iter().filter(move |e| e.from == node && e.from_index == output_index)
    }

    pub fn edges_into<'a>(&'a self, node: &'a str) -> impl Iterator<Item = &'a Edge> + 'a {
        self.edges.iter().filter(move |e| e.to == node)
    }

    pub fn input_count(&self, node: &str) -> usize {
        self.edges_into(node).count().max(1)
    }

    pub fn entry_nodes(&self) -> Vec<&NodeDef> {
        let with_incoming: std::collections::HashSet<_> =
            self.edges.iter().map(|e| &e.to).collect();
        self.nodes.iter().filter(|n| !with_incoming.contains(&n.name)).collect()
    }
}

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

    const SAMPLE: &str = r#"
[workflow]
name = "smoke"

[[nodes]]
name = "Webhook"
type = "webhook"

[[nodes]]
name = "Slack"
type = "slack"

[[edges]]
from = "Webhook"
to = "Slack"
"#;

    #[test]
    fn load_minimal() {
        let wf = Workflow::from_toml_str(SAMPLE).unwrap();
        assert_eq!(wf.nodes.len(), 2);
        assert_eq!(wf.edges.len(), 1);
        assert_eq!(wf.entry_nodes()[0].name, "Webhook");
    }

    #[test]
    fn rejects_unknown_edge_target() {
        let bad = r#"
[workflow]
name = "bad"

[[nodes]]
name = "A"
type = "set"

[[edges]]
from = "A"
to = "Nonexistent"
"#;
        assert!(Workflow::from_toml_str(bad).is_err());
    }
}
