use std::collections::HashMap;

use crate::PortDirection;

/// Unique identifier for a node instance within a network.
pub type NodeId = u64;

/// A named, directed port on a node.
#[derive(Clone, Debug, PartialEq)]
pub struct Port {
    pub name: String,
    pub direction: PortDirection,
}

/// A reference to a specific port on a specific node instance.
#[derive(Clone, Debug, PartialEq)]
pub struct PortRef {
    pub node_id: NodeId,
    pub port_name: String,
}

/// A directed connection between two ports.
#[derive(Clone, Debug, PartialEq)]
pub struct Connection {
    pub from: PortRef,
    pub to: PortRef,
}

/// A node in the network. Leaf nodes represent external components;
/// nodes with non-empty `children` represent groups (composites).
#[derive(Clone, Debug, PartialEq)]
pub struct Node {
    pub id: NodeId,
    pub name: String,
    /// Schema name for leaf nodes (e.g. `"Source"`); `"group"` for composites.
    pub type_name: String,
    pub ports: Vec<Port>,
    pub children: Vec<Node>,
}

/// The complete output of a Nexus program.
#[derive(Clone, Debug, PartialEq)]
pub struct Network {
    /// `None` for programs that produce no network.
    pub root: Option<Node>,
    /// All connections in the network, globally owned.
    pub connections: Vec<Connection>,
}

impl Network {
    /// Build a map from `NodeId` to fully qualified path (e.g. `"pipeline/source"`).
    ///
    /// Returns an empty map when `root` is `None`.
    #[must_use]
    pub fn node_paths(&self) -> HashMap<NodeId, String> {
        let mut map = HashMap::new();
        if let Some(root) = &self.root {
            collect_paths(root, &root.name, &mut map);
        }
        map
    }

    /// Build a flat map from `NodeId` to `&Node` for O(1) lookup.
    ///
    /// Returns an empty map when `root` is `None`.
    #[must_use]
    pub fn node_index(&self) -> HashMap<NodeId, &Node> {
        let mut map = HashMap::new();
        if let Some(root) = &self.root {
            collect_index(root, &mut map);
        }
        map
    }
}

fn collect_paths(node: &Node, path: &str, map: &mut HashMap<NodeId, String>) {
    map.insert(node.id, path.to_owned());
    for child in &node.children {
        collect_paths(child, &format!("{path}/{}", child.name), map);
    }
}

fn collect_index<'a>(node: &'a Node, map: &mut HashMap<NodeId, &'a Node>) {
    map.insert(node.id, node);
    for child in &node.children {
        collect_index(child, map);
    }
}
