#![allow(clippy::result_large_err)] // We accept large errors on the stack for now. Boxing all error types is a pain.

mod builtin_methods;
mod environment;
pub mod interpret_error;
mod interpret_error_ext;
mod interpret_warning;
mod network;
mod value;

pub use interpret_error::{InterpretError, InterpretErrorKind};
pub use interpret_warning::{InterpretWarning, InterpretWarningKind};
pub use network::{Connection, Network, Node, Port, PortRef};

use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use std::sync::Arc;

use crate::parser::{BinaryOp, Literal, Parameter, PortSpec, TypeId, UnaryOp};
use crate::{Ast, Expr, Location, Parser, PortDirection, Scanner, Source, Stmt};
use environment::{Binding, Environment};
use interpret_error_ext::InterpretErrorExt;
use miette::Result;
use value::{GroupId, InternalConnection, NodeId, NodeTypeSchema, Value, ValueError};

/// Per-port entry in `port_schema_stack`: (`port_name`, direction, `fan_out_targets`).
type PortSchemaEntry = (String, PortDirection, Vec<(NodeId, String)>);

/// Default maximum number of iterations for any loop construct.
const MAX_LOOP_ITERATIONS: u64 = 1_000_000;

/// Default maximum call depth for function calls.
const MAX_CALL_DEPTH: u64 = 64;

/// Default maximum nesting depth for block expressions.
const MAX_BLOCK_DEPTH: u64 = 50;

/// Default maximum recursive evaluation depth for expressions.
const MAX_EVAL_DEPTH: u64 = 50;

/// Default maximum number of elements in an array.
const MAX_ARRAY_SIZE: usize = 65_536;

/// Result of evaluating a single iteration of a `for` loop body.
enum ForBodyResult {
    Ok,
    Break(Value),
    Continue,
}

/// Bundled callable description used by `eval_callable` to unify fn and closure calls.
struct CallableSpec {
    display_name: String,
    params: Vec<Parameter>,
    return_type: Option<TypeId>,
    body: Expr,
    /// `Some` for closures (captured bindings scope), `None` for plain functions.
    captured: Option<HashMap<String, Binding>>,
}

/// Nexus interpreter.
pub struct Interpreter<'a> {
    source: Source<'a>,
    ast: Vec<Stmt>,
    env: Environment,
    call_depth: u64,
    block_depth: u64,
    eval_depth: u64,
    /// Cumulative loop iteration counter; counts across all nested loops within one function
    /// invocation. Prevents nested loops from circumventing `MAX_LOOP_ITERATIONS` by keeping
    /// each individual loop's count below the limit.
    loop_iterations: u64,
    /// Monotonic counter shared by `NodeId` and `GroupId` allocation; guarantees globally
    /// unique values so `InternalConnection` duplicate detection is never ambiguous.
    next_instance_id: u64,
    /// Top-level connections (outside any group body).
    connections: Vec<InternalConnection>,
    /// Per-group connection lists, one entry per active group body evaluation.
    /// `eval_connect_stmt` pushes to the innermost entry when inside a group.
    connection_stack: Vec<Vec<InternalConnection>>,
    /// Per-group member maps, one entry per active group body evaluation.
    member_stack: Vec<HashMap<String, Value>>,
    /// Per-group port schema lists, one entry per active group body evaluation.
    /// Each entry is `(port_name, direction, fan_out_targets)` where `fan_out_targets` is the
    /// list of `(NodeId, port_name)` pairs used to generate `InternalConnection`s in
    /// `eval_group_expr`. Frozen into `NodeTypeSchema` (direction only) at `}`.
    port_schema_stack: Vec<Vec<PortSchemaEntry>>,
    /// Maps a `NodeId` to "owner.member" to enforce parent-claim for node instances.
    claimed_node_ids: HashMap<u64, String>,
    /// Maps a `GroupId` to "owner.member" to enforce parent-claim for sub-groups.
    claimed_group_ids: HashMap<u64, String>,
    /// Directory of the current source file, used for resolving relative `use` imports.
    source_dir: Option<PathBuf>,
    /// Canonical absolute paths of files already loaded (shared across the import tree via clone+merge).
    loaded_files: HashSet<PathBuf>,
    /// Additional directories to search for `use` imports after the source directory.
    search_paths: Vec<PathBuf>,
    /// Accumulated warnings emitted during interpretation.
    warnings: Vec<InterpretWarning>,
}

impl<'a> Interpreter<'a> {
    /// Create a new Interpreter.
    ///
    /// # Example
    ///
    /// ```
    /// use nexus_lang::{Parser, Scanner, Interpreter, Source};
    ///
    /// let source = Source::new("print 42;");
    /// let tokens = Scanner::new(source).scan().expect("Scanning failed");
    /// let ast    = Parser::new(source, tokens).parse().expect("Parsing failed");
    ///
    /// let interpreter = Interpreter::new(source, ast);
    /// ```
    #[must_use]
    pub fn new(source: Source<'a>, ast: Ast) -> Self {
        let source_dir = source
            .name()
            .map(std::path::Path::new)
            .and_then(|p| std::fs::canonicalize(p).ok())
            .and_then(|p| p.parent().map(PathBuf::from))
            .or_else(|| std::env::current_dir().ok());

        Interpreter {
            source,
            ast,
            env: Environment::new(),
            call_depth: 0,
            block_depth: 0,
            eval_depth: 0,
            loop_iterations: 0,
            next_instance_id: 0,
            connections: Vec::new(),
            connection_stack: Vec::new(),
            member_stack: Vec::new(),
            port_schema_stack: Vec::new(),
            claimed_node_ids: HashMap::new(),
            claimed_group_ids: HashMap::new(),
            source_dir,
            loaded_files: HashSet::new(),
            search_paths: Vec::new(),
            warnings: Vec::new(),
        }
    }

    /// Set additional library search paths for `use` import resolution.
    #[must_use]
    pub fn with_search_paths(mut self, paths: Vec<PathBuf>) -> Self {
        self.search_paths = paths;
        self
    }

    /// Interpret the AST and evaluate the result.
    ///
    /// # Errors
    ///
    /// This function will return a `InterpretError` if the AST can not be parsed properly.
    ///
    /// Note: The current implementation will bail out at the first error it encounters.
    ///
    /// # Example
    ///
    /// ```
    /// use nexus_lang::{Parser, Scanner, Interpreter, Source};
    ///
    /// let source = Source::new("print 42;");
    /// let tokens = Scanner::new(source).scan().expect("Scanning failed");
    /// let ast    = Parser::new(source, tokens).parse().expect("Parsing failed");
    ///
    /// let interpreter = Interpreter::new(source, ast);
    /// let _ = interpreter.eval().expect("Interpreter evaluation failed.");
    /// ```
    pub fn eval(mut self) -> Result<(Network, Vec<InterpretWarning>), InterpretError> {
        let ast = std::mem::take(&mut self.ast);

        let (setup, result_stmt) = match ast.last() {
            Some(Stmt::Expr {
                discarded: false, ..
            }) => (&ast[..ast.len() - 1], ast.last()),
            _ => (&ast[..], None),
        };

        for stmt in setup {
            if let Stmt::Expr { expr, discarded } = stmt {
                let val = self.eval_expr(expr)?;
                if !discarded && result_stmt.is_some() && !matches!(val, Value::Unit) {
                    return Err(self.error(
                        expr.as_ref(),
                        InterpretErrorKind::MultipleTopLevelResults,
                        None,
                        None,
                    ));
                }

                if !matches!(val, Value::Unit) {
                    self.push_warning(expr.loc(), None, InterpretWarningKind::UnusedValue);
                }
            } else {
                self.eval_stmt(stmt)?;
            }
        }

        let root = if let Some(Stmt::Expr { expr, .. }) = result_stmt {
            let (value, name) = match expr.as_ref() {
                Expr::Group { stmts, .. } => (
                    self.eval_group_expr(expr, stmts, Some("root".to_owned()))?,
                    "root".to_owned(),
                ),
                Expr::Node { type_name, loc } => (
                    self.instantiate_node(loc, type_name, Some("root".to_owned()))?,
                    "root".to_owned(),
                ),
                Expr::Identifier { name, .. } => (self.eval_expr(expr)?, name.clone()),
                _ => (self.eval_expr(expr)?, "root".to_owned()),
            };
            Some((force_rename(value, &name), name))
        } else {
            None
        };

        self.check_unused_bindings();

        Ok((
            build_network(
                root.as_ref().map(|(v, n)| (v, n.as_str())),
                self.connections,
            ),
            self.warnings,
        ))
    }

    fn push_warning(&mut self, loc: &Location, length: Option<usize>, kind: InterpretWarningKind) {
        let warning = InterpretWarning::new(self.source.into(), loc, kind, None::<String>, length);
        self.warnings.push(warning);
    }

    fn check_unused_bindings(&mut self) {
        let unused = self.env.unused_in_current_scope();
        for (name, loc) in unused {
            if let Some(l) = loc {
                self.push_warning(&l, Some(name.len()), InterpretWarningKind::UnusedValue);
            }
        }
    }

    fn eval_expr(&mut self, expr: &Expr) -> Result<Value, InterpretError> {
        if self.eval_depth >= MAX_EVAL_DEPTH {
            return Err(self.error(
                expr,
                InterpretErrorKind::ExpressionDepthExceeded(MAX_EVAL_DEPTH),
                Some(&format!(
                    "Expression evaluation depth exceeds limit of {MAX_EVAL_DEPTH}"
                )),
                None,
            ));
        }
        self.eval_depth += 1;
        let result = match expr {
            Expr::Array { elements, .. } => self.eval_array_literal(expr, elements),
            Expr::ArrayFill {
                type_id, length, ..
            } => self.eval_array_fill(expr, type_id, length),
            Expr::Unary { op, factor, .. } => self.eval_unary_expr(op, factor),
            Expr::Binary { op, lhs, rhs, .. } => self.eval_binary_expr(expr, op, lhs, rhs),
            Expr::Break { value, .. } => self.eval_break(expr, value.as_deref()),
            Expr::Block { stmts, result, .. } => {
                if self.block_depth >= MAX_BLOCK_DEPTH {
                    return Err(self.error(
                        expr,
                        InterpretErrorKind::BlockDepthExceeded(MAX_BLOCK_DEPTH),
                        Some(&format!(
                            "Block nesting depth exceeds limit of {MAX_BLOCK_DEPTH}"
                        )),
                        None,
                    ));
                }
                self.block_depth += 1;
                let outcome = self.eval_block(stmts, result.as_deref());
                self.block_depth -= 1;
                outcome
            }
            Expr::Call { callee, args, .. } => self.eval_call(expr, callee, args),
            Expr::Closure {
                params,
                return_type,
                body,
                ..
            } => Ok(self.eval_closure(params, return_type.as_ref(), body)),
            Expr::Continue { .. } => self.eval_continue(expr),
            Expr::Identifier { name, .. } => self.eval_identifier(name.as_str(), expr),
            Expr::If {
                condition,
                then_branch,
                else_branch,
                ..
            } => self.eval_if(condition, then_branch, else_branch.as_deref()),
            Expr::Index { array, index, .. } => self.eval_index(expr, array, index),
            Expr::For {
                variable,
                var_loc,
                iterable,
                body,
                ..
            } => self.eval_for(variable, var_loc, iterable, body),
            Expr::Group { stmts, .. } => self.eval_group_expr(expr, stmts, None),
            Expr::Literal(l) => Ok(l.into()),
            Expr::MemberAccess { object, field, loc } => {
                // self.NAME resolves from the active group's member map, not the environment.
                if matches!(object.as_ref(), Expr::Self_ { .. }) {
                    return self.eval_self_member_access(field, loc);
                }
                self.eval_member_access(expr, object, field)
            }
            Expr::Self_ { .. } => Err(self.error(
                expr,
                InterpretErrorKind::SelfOutsideGroup,
                Some("'self' can only appear as 'self.name' inside a group body"),
                None,
            )),
            Expr::MethodCall {
                object,
                method,
                args,
                ..
            } => self.eval_method_call(expr, object, method, args),
            Expr::Loop { body, .. } => self.eval_loop(body),
            Expr::Node { type_name, loc } => self.instantiate_node(loc, type_name, None),
            Expr::Ref { expr, .. } => self.eval_ref(expr),
            Expr::Range {
                start,
                end,
                inclusive,
                ..
            } => self.eval_range(start, end, *inclusive),
            Expr::While {
                condition, body, ..
            } => self.eval_while(condition, body),
        };
        self.eval_depth -= 1;
        result
    }

    fn eval_block(
        &mut self,
        stmts: &[Stmt],
        result: Option<&Expr>,
    ) -> Result<Value, InterpretError> {
        self.env.push_scope();
        let outcome = self.eval_block_body(stmts, result);
        if outcome.is_ok() {
            self.check_unused_bindings();
        }
        self.env.pop_scope();
        outcome
    }

    fn eval_block_body(
        &mut self,
        stmts: &[Stmt],
        result: Option<&Expr>,
    ) -> Result<Value, InterpretError> {
        for stmt in stmts {
            self.eval_stmt(stmt)?;
        }

        if let Some(result_expr) = result {
            self.eval_expr(result_expr)
        } else {
            Ok(Value::Unit)
        }
    }

    /// Evaluate a single statement. Used by `eval`, `eval_block`, and `eval_group_expr`.
    fn eval_stmt(&mut self, stmt: &Stmt) -> Result<(), InterpretError> {
        match stmt {
            Stmt::Assignment {
                target, expr, loc, ..
            } => self.eval_assignment_stmt(target, expr, loc),
            Stmt::Assert {
                condition,
                message,
                loc,
            } => self.eval_assert(condition, message.as_deref(), loc),
            Stmt::Connect { from, targets, loc } => self.eval_connect_stmt(from, targets, loc),
            Stmt::ConstDecl {
                name,
                type_id,
                expr,
                loc,
            } => self.eval_const_decl(name, type_id, expr, loc),
            Stmt::Expr { expr, .. } => {
                let val = self.eval_expr(expr)?;
                if !matches!(val, Value::Unit) {
                    let loc = expr.loc();
                    self.push_warning(loc, None, InterpretWarningKind::UnusedValue);
                }
                Ok(())
            }
            Stmt::FnDecl {
                name,
                params,
                return_type,
                body,
                loc,
            } => self.eval_fn_decl(name, params, return_type.as_ref(), body, loc),
            Stmt::MemberDecl { name, expr, loc } => self.eval_member_decl(name, expr, loc),
            Stmt::PortDecl { name, exprs, loc } => self.eval_port_decl(name, exprs, loc),
            Stmt::NodeTypeDecl {
                name, ports, loc, ..
            } => self.eval_node_type_decl(name, ports, loc),
            Stmt::Print { expr } => self.eval_print(expr),
            Stmt::Return { expr, .. } => self.eval_return_stmt(stmt, expr.as_deref()).map(|_| ()),
            Stmt::UseDecl { name, loc } => self.eval_use(name, loc),
            Stmt::VarDecl {
                name,
                mutable,
                type_id,
                expr,
                loc,
            } => self.eval_var_decl(name, *mutable, type_id.as_ref(), expr.as_deref(), loc),
        }
    }

    fn eval_if(
        &mut self,
        condition: &Expr,
        then_branch: &Expr,
        else_branch: Option<&Expr>,
    ) -> Result<Value, InterpretError> {
        let condition_value = self.eval_expr(condition)?;

        let Value::Boolean(cond) = condition_value else {
            return Err(self.error(
                condition,
                InterpretErrorKind::ConditionNotBoolean,
                Some("Expected a boolean expression as condition"),
                None,
            ));
        };

        if cond {
            self.eval_expr(then_branch)
        } else if let Some(else_expr) = else_branch {
            self.eval_expr(else_expr)
        } else {
            Ok(Value::Unit)
        }
    }

    fn eval_break(&mut self, expr: &Expr, value: Option<&Expr>) -> Result<Value, InterpretError> {
        let val = if let Some(v) = value {
            self.eval_expr(v)?
        } else {
            Value::Unit
        };
        Err(self.error(expr, InterpretErrorKind::BreakSignal(val), None, None))
    }

    fn eval_continue(&mut self, expr: &Expr) -> Result<Value, InterpretError> {
        Err(self.error(expr, InterpretErrorKind::ContinueSignal, None, None))
    }

    fn eval_array_literal(
        &mut self,
        _expr: &Expr,
        element_exprs: &[Expr],
    ) -> Result<Value, InterpretError> {
        let mut elements = Vec::with_capacity(element_exprs.len());

        for elem_expr in element_exprs {
            let val = self.eval_expr(elem_expr)?;
            if matches!(val, Value::Unit) {
                return Err(self.error(
                    elem_expr,
                    InterpretErrorKind::ExpectedValue,
                    Some("Array elements cannot be unit values"),
                    None,
                ));
            }
            elements.push(val);
        }

        // Verify homogeneous types.
        if elements.len() > 1 {
            let first_type = elements[0].type_name();
            for (i, elem) in elements.iter().enumerate().skip(1) {
                if elem.type_name() != first_type {
                    return Err(self.error(
                        &element_exprs[i],
                        InterpretErrorKind::ArrayElementTypeMismatch,
                        Some(&format!(
                            "Expected type '{}', got '{}'",
                            first_type,
                            elem.type_name()
                        )),
                        None,
                    ));
                }
            }
        }

        Ok(Value::Array(elements))
    }

    fn eval_array_fill(
        &mut self,
        expr: &Expr,
        type_id: &TypeId,
        length_expr: &Expr,
    ) -> Result<Value, InterpretError> {
        let length_val = self.eval_expr(length_expr)?;

        let Value::Number(n) = length_val else {
            return Err(self.error(
                length_expr,
                InterpretErrorKind::IndexNotInteger,
                Some("Array fill length must be a non-negative integer"),
                None,
            ));
        };

        if n < 0.0 || n.fract() != 0.0 {
            return Err(self.error(
                length_expr,
                InterpretErrorKind::IndexNotInteger,
                Some("Array fill length must be a non-negative integer"),
                None,
            ));
        }

        #[allow(clippy::cast_precision_loss)] // We know the array size is limited.
        if n > MAX_ARRAY_SIZE as f64 {
            #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
            return Err(self.error(
                length_expr,
                InterpretErrorKind::ArraySizeLimitExceeded {
                    requested: n as usize,
                    limit: MAX_ARRAY_SIZE,
                },
                Some(&format!("Array size limit is {MAX_ARRAY_SIZE}")),
                None,
            ));
        }

        let len = f64_to_usize(n);
        let default_value = match type_id {
            TypeId::Boolean => Value::Boolean(false),
            TypeId::Number => Value::Number(0.0),
            TypeId::String => Value::String(String::new()),
            _ => {
                return Err(self.error(
                    expr,
                    InterpretErrorKind::ArrayElementTypeMismatch,
                    Some(&format!(
                        "Cannot create a zero-filled array of type '{type_id:?}'"
                    )),
                    None,
                ));
            }
        };

        let elements = vec![default_value; len];
        Ok(Value::Array(elements))
    }

    /// Evaluate a zero-filled array from a type annotation (deferred init).
    /// Reuses the same validation logic as `eval_array_fill`.
    fn eval_array_fill_from_type(
        &mut self,
        elem_type: &TypeId,
        length_expr: &Expr,
    ) -> Result<Value, InterpretError> {
        let length_val = self.eval_expr(length_expr)?;

        let Value::Number(n) = length_val else {
            return Err(self.error(
                length_expr,
                InterpretErrorKind::IndexNotInteger,
                Some("Array size must be a non-negative integer"),
                None,
            ));
        };

        if n < 0.0 || n.fract() != 0.0 {
            return Err(self.error(
                length_expr,
                InterpretErrorKind::IndexNotInteger,
                Some("Array size must be a non-negative integer"),
                None,
            ));
        }

        #[allow(clippy::cast_precision_loss)] // We know the array size is limited.
        if n > MAX_ARRAY_SIZE as f64 {
            #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
            return Err(self.error(
                length_expr,
                InterpretErrorKind::ArraySizeLimitExceeded {
                    requested: n as usize,
                    limit: MAX_ARRAY_SIZE,
                },
                Some(&format!("Array size limit is {MAX_ARRAY_SIZE}")),
                None,
            ));
        }

        let len = f64_to_usize(n);
        let default_value = match elem_type {
            TypeId::Boolean => Value::Boolean(false),
            TypeId::Number => Value::Number(0.0),
            TypeId::String => Value::String(String::new()),
            _ => Value::Uninitialized,
        };

        Ok(Value::Array(vec![default_value; len]))
    }

    /// Check that an array value matches the size constraint of a sized array type.
    /// Returns Ok(()) for unsized types or when the size matches.
    fn check_array_size(
        &mut self,
        value: &Value,
        type_id: &TypeId,
        error_expr: &Expr,
    ) -> Result<(), InterpretError> {
        if let (Value::Array(elements), TypeId::Array(_, Some(length_expr))) = (value, type_id) {
            let length_val = self.eval_expr(length_expr)?;

            if let Value::Number(n) = length_val
                && n >= 0.0
                && n.fract() == 0.0
            {
                let expected = f64_to_usize(n);
                if elements.len() != expected {
                    return Err(self.error(
                        error_expr,
                        InterpretErrorKind::ArraySizeMismatch {
                            expected,
                            got: elements.len(),
                        },
                        Some(&format!(
                            "Expected array of {expected} element(s), got {}",
                            elements.len()
                        )),
                        None,
                    ));
                }
            }
        }

        Ok(())
    }

    fn eval_index(
        &mut self,
        _expr: &Expr,
        array_expr: &Expr,
        index_expr: &Expr,
    ) -> Result<Value, InterpretError> {
        let array_val = self.eval_expr(array_expr)?;

        let Value::Array(ref elements) = array_val else {
            return Err(self.error(
                array_expr,
                InterpretErrorKind::IndexOnNonArray(array_val.type_name().to_owned()),
                Some("Only arrays can be indexed"),
                None,
            ));
        };

        let index_val = self.eval_expr(index_expr)?;

        let Value::Number(n) = index_val else {
            return Err(self.error(
                index_expr,
                InterpretErrorKind::IndexNotInteger,
                Some("Array index must be a non-negative integer"),
                None,
            ));
        };

        if n < 0.0 || n.fract() != 0.0 {
            return Err(self.error(
                index_expr,
                InterpretErrorKind::IndexNotInteger,
                Some("Array index must be a non-negative integer"),
                None,
            ));
        }

        let idx = f64_to_usize(n);
        if idx >= elements.len() {
            return Err(self.error(
                index_expr,
                InterpretErrorKind::IndexOutOfBounds {
                    index: usize_to_i64(idx),
                    length: elements.len(),
                },
                Some(&format!(
                    "Index {} is out of bounds for array of length {}",
                    idx,
                    elements.len()
                )),
                None,
            ));
        }

        Ok(elements[idx].clone())
    }

    fn eval_fn_decl(
        &mut self,
        name: &str,
        params: &[Parameter],
        return_type: Option<&TypeId>,
        body: &Expr,
        loc: &Location,
    ) -> Result<(), InterpretError> {
        self.reject_node_type_shadow(name, loc, name.len() + 3)?;

        let value = Value::Function {
            name: name.to_owned(),
            params: params.to_vec(),
            return_type: return_type.cloned(),
            body: Box::new(body.clone()),
        };

        let displaced = self.env.define(
            name.to_owned(),
            Binding::variable(value, None, false).with_loc(loc.with_column_offset(3)),
        );

        if let Some(old) = displaced
            && !old.used
            && !name.starts_with('_')
            && let Some(old_loc) = old.loc
        {
            self.push_warning(
                &old_loc,
                Some(name.len()),
                InterpretWarningKind::UnusedValue,
            );
        }

        Ok(())
    }

    /// Error if there is an existing `Value::NodeType` binding for `name` anywhere in scope.
    ///
    /// `node type Foo` reserves `Foo` globally; `let`/`const`/`fn`/`node type` redeclarations
    /// over it are all errors regardless of scope (see design D12).
    ///
    fn reject_node_type_shadow(
        &self,
        name: &str,
        loc: &Location,
        length: usize,
    ) -> Result<(), InterpretError> {
        if let Some(binding) = self.env.get(name)
            && matches!(binding.value, Value::NodeType(_))
        {
            return Err(self.error_at(
                loc,
                Some(length),
                InterpretErrorKind::DuplicateDeclaration(name.to_owned()),
                Some("A node type with this name is already declared"),
                None,
            ));
        }
        Ok(())
    }

    /// Evaluate an initializer expression, forwarding the binding name when the expression is a
    /// direct `node TypeName` or `group { ... }` so the result inherits that name.
    fn eval_init_expr_with_binding(
        &mut self,
        expr: &Expr,
        binding_name: &str,
    ) -> Result<Value, InterpretError> {
        match expr {
            Expr::Node { type_name, loc } => {
                self.instantiate_node(loc, type_name, Some(binding_name.to_owned()))
            }
            Expr::Group { stmts, .. } => {
                self.eval_group_expr(expr, stmts, Some(binding_name.to_owned()))
            }
            _ => self.eval_expr(expr),
        }
    }

    /// Allocate a new unique `NodeId`.
    fn new_node_id(&mut self) -> NodeId {
        let id = NodeId(self.next_instance_id);
        self.next_instance_id += 1;
        id
    }

    /// Look up a node type schema by name.
    ///
    /// Errors with `UndefinedIdentifier` if the name is not bound, or `NotANodeType` if the
    /// binding exists but does not hold a node type schema.
    ///
    fn resolve_node_type(
        &self,
        type_name: &str,
        loc: &Location,
    ) -> Result<Arc<NodeTypeSchema>, InterpretError> {
        match self.env.get(type_name) {
            Some(binding) => match &binding.value {
                Value::NodeType(schema) => Ok(Arc::clone(schema)),
                _ => Err(self.error_at(
                    loc,
                    Some(type_name.len() + 5), // "node " + name.
                    InterpretErrorKind::NotANodeType(type_name.to_owned()),
                    Some("Only names declared with 'node type' can be instantiated with 'node'"),
                    None,
                )),
            },
            None => Err(self.error_at(
                loc,
                Some(type_name.len() + 5),
                InterpretErrorKind::UndefinedIdentifier(type_name.to_owned()),
                Some("This node type has not been declared"),
                None,
            )),
        }
    }

    /// Instantiate a node of the given declared type.
    ///
    /// When `binding_name` is `Some`, uses it as the instance's source-level name. When `None`,
    /// generates an anonymous `node_<N>` name from the interpreter's counter.
    ///
    fn instantiate_node(
        &mut self,
        loc: &Location,
        type_name: &str,
        binding_name: Option<String>,
    ) -> Result<Value, InterpretError> {
        let schema = self.resolve_node_type(type_name, loc)?;
        let id = self.new_node_id();
        let name = binding_name.unwrap_or_else(|| format!("node_{}", id.0));
        Ok(Value::Node { id, name, schema })
    }

    fn eval_node_type_decl(
        &mut self,
        name: &str,
        ports: &[PortSpec],
        loc: &Location,
    ) -> Result<(), InterpretError> {
        if self.env.contains(name) {
            return Err(self.error_at(
                loc,
                Some(name.len() + 10), // "node type " + name.
                InterpretErrorKind::DuplicateDeclaration(name.to_owned()),
                Some("A declaration with this name already exists"),
                None,
            ));
        }

        let schema = NodeTypeSchema {
            name: name.to_owned(),
            ports: ports
                .iter()
                .map(|p| (p.name.clone(), p.direction))
                .collect(),
        };

        self.env.define(
            name.to_owned(),
            Binding::variable(Value::NodeType(Arc::new(schema)), None, false),
        );

        Ok(())
    }

    #[allow(clippy::too_many_lines)] // Cutting up this method would create an unlogical seam.
    fn eval_use(&mut self, name: &str, loc: &Location) -> Result<(), InterpretError> {
        let filename = format!("{name}.nxs");

        // Resolve: try source_dir first, then each search_path.
        let canonical = self
            .source_dir
            .iter()
            .chain(self.search_paths.iter())
            .find_map(|dir| std::fs::canonicalize(dir.join(&filename)).ok())
            .ok_or_else(|| {
                self.error_at(
                    loc,
                    Some("use".len()),
                    InterpretErrorKind::ImportError(filename.clone()),
                    Some(&format!("File '{filename}' not found in any search path")),
                    None,
                )
            })?;

        // Skip if already loaded (dedup / cycle prevention).
        if self.loaded_files.contains(&canonical) {
            return Ok(());
        }

        self.loaded_files.insert(canonical.clone());

        // Load source text.
        let code = std::fs::read_to_string(&canonical).map_err(|e| {
            self.error_at(
                loc,
                Some("use".len()),
                InterpretErrorKind::ImportError(canonical.display().to_string()),
                Some(&e.to_string()),
                None,
            )
        })?;

        let path_str = canonical.display().to_string();
        let source = Source::new_with_name(code.as_str(), path_str.as_str());

        // Scan; wrap error as related diagnostic anchored at the `use` statement.
        let tokens = Scanner::new(source).scan().map_err(|e| {
            self.error_at(
                loc,
                Some("use".len()),
                InterpretErrorKind::ImportError(path_str.clone()),
                Some("Scan error in imported file"),
                None,
            )
            .with_related(e)
        })?;

        // Parse; same wrapping.
        let ast = Parser::new(source, tokens).parse().map_err(|e| {
            self.error_at(
                loc,
                Some("use".len()),
                InterpretErrorKind::ImportError(path_str.clone()),
                Some("Parse error in imported file"),
                None,
            )
            .with_related(e)
        })?;

        // Build and run child interpreter, handing off instance-ID counter so NodeIds stay unique.
        let child_source_dir = canonical.parent().map(PathBuf::from);
        let child_loaded_files = self.loaded_files.clone();
        let child_search_paths = self.search_paths.clone();
        let starting_id = self.next_instance_id;

        let mut child = Interpreter {
            source,
            ast,
            env: Environment::new(),
            call_depth: 0,
            block_depth: 0,
            eval_depth: 0,
            loop_iterations: 0,
            next_instance_id: starting_id,
            connections: Vec::new(),
            connection_stack: Vec::new(),
            member_stack: Vec::new(),
            port_schema_stack: Vec::new(),
            claimed_node_ids: HashMap::new(),
            claimed_group_ids: HashMap::new(),
            source_dir: child_source_dir,
            loaded_files: child_loaded_files,
            search_paths: child_search_paths,
            warnings: Vec::new(),
        };

        let child_ast = std::mem::take(&mut child.ast);
        for stmt in &child_ast {
            // Runtime errors propagate as-is; they carry NamedSource for the imported file.
            child.eval_stmt(stmt)?;
        }

        // Surface child warnings to the parent.
        self.warnings.append(&mut child.warnings);

        // Absorb child state back into parent.
        self.next_instance_id = child.next_instance_id;
        self.loaded_files.extend(child.loaded_files);
        self.connections.extend(child.connections);

        // Flat-merge global bindings; check for name collisions.
        let bindings: Vec<(String, Binding)> = child
            .env
            .iter_global()
            .map(|(k, v)| (k.clone(), v.clone()))
            .collect();

        for (bname, mut binding) in bindings {
            if self.env.contains_global(&bname) {
                return Err(self.error_at(
                    loc,
                    Some("use".len()),
                    InterpretErrorKind::ImportNameCollision(bname),
                    None,
                    None,
                ));
            }

            // Mark as used so the parent doesn't re-warn about imported symbols.
            binding.used = true;
            self.env.define_global(bname, binding);
        }

        Ok(())
    }

    fn eval_return_stmt(
        &mut self,
        stmt: &Stmt,
        expr: Option<&Expr>,
    ) -> Result<Value, InterpretError> {
        let val = if let Some(e) = expr {
            self.eval_expr(e)?
        } else {
            Value::Unit
        };

        let Stmt::Return { loc, .. } = stmt else {
            unreachable!()
        };

        Err(InterpretError::new(
            self.source.into(),
            loc,
            InterpretErrorKind::ReturnSignal(val),
            None::<String>,
            None::<String>,
            Some(6), // "return"
        ))
    }

    fn eval_closure(
        &mut self,
        params: &[Parameter],
        return_type: Option<&TypeId>,
        body: &Expr,
    ) -> Value {
        let mut bound: HashSet<String> = params.iter().map(|p| p.name.clone()).collect();
        let free_vars = collect_free_vars(body, &mut bound);

        let mut captured = HashMap::new();
        for name in &free_vars {
            if let Some(binding) = self.env.get_mut(name) {
                binding.used = true;
                captured.insert(name.clone(), binding.clone());
            } else if let Some(members) = self.member_stack.last()
                && let Some(value) = members.get(name)
            {
                // Closure defined in a group body captures member via bare name.
                captured.insert(name.clone(), Binding::variable(value.clone(), None, false));
            }
        }

        Value::Closure {
            params: params.to_vec(),
            return_type: return_type.cloned(),
            body: Box::new(body.clone()),
            captured,
        }
    }

    fn eval_call(
        &mut self,
        call_expr: &Expr,
        callee: &Expr,
        arg_exprs: &[Expr],
    ) -> Result<Value, InterpretError> {
        let callee_val = self.eval_expr(callee)?;

        match callee_val {
            Value::Function {
                name,
                params,
                return_type,
                body,
            } => {
                let spec = CallableSpec {
                    display_name: name.clone(),
                    params: params.clone(),
                    return_type: return_type.clone(),
                    body: (*body).clone(),
                    captured: None,
                };
                self.eval_callable(call_expr, callee, arg_exprs, &spec)
            }
            Value::Closure {
                params,
                return_type,
                body,
                captured,
            } => {
                let spec = CallableSpec {
                    display_name: "<closure>".to_owned(),
                    params: params.clone(),
                    return_type: return_type.clone(),
                    body: (*body).clone(),
                    captured: Some(captured.clone()),
                };
                self.eval_callable(call_expr, callee, arg_exprs, &spec)
            }
            _ => Err(self.error(
                callee,
                InterpretErrorKind::NotCallable(callee_val.type_name().to_owned()),
                Some("Only functions and closures can be called"),
                None,
            )),
        }
    }

    fn eval_callable(
        &mut self,
        call_expr: &Expr,
        callee: &Expr,
        arg_exprs: &[Expr],
        spec: &CallableSpec,
    ) -> Result<Value, InterpretError> {
        let CallableSpec {
            display_name,
            params,
            return_type,
            body,
            captured,
        } = spec;

        if arg_exprs.len() != params.len() {
            return Err(self.error(
                call_expr,
                InterpretErrorKind::ArityMismatch {
                    expected: params.len(),
                    got: arg_exprs.len(),
                },
                Some(&format!(
                    "'{}' expects {} argument(s), but {} were provided",
                    display_name,
                    params.len(),
                    arg_exprs.len()
                )),
                None,
            ));
        }

        if self.call_depth >= MAX_CALL_DEPTH {
            return Err(self.error(
                call_expr,
                InterpretErrorKind::CallDepthExceeded(MAX_CALL_DEPTH),
                Some(&format!("Maximum call depth ({MAX_CALL_DEPTH}) exceeded")),
                None,
            ));
        }

        self.call_depth += 1;

        // Evaluate arguments before pushing scopes.
        let arg_values = self.eval_call_args(call_expr, callee, arg_exprs, params)?;

        // For closures: push captured bindings scope first so params can shadow them.
        if let Some(captured) = captured {
            self.env.push_scope();
            for (name, binding) in captured {
                self.env.define(name.clone(), binding.clone());
            }
        }

        // Push parameter scope.
        self.env.push_scope();
        for (param, val) in params.iter().zip(arg_values) {
            self.env.define(
                param.name.clone(),
                Binding::variable(val, Some(param.type_id.clone()), false),
            );
        }

        // Reset per-invocation counters so limits apply within a single call, not accumulated
        // across the entire call chain.
        let saved_block_depth = self.block_depth;
        self.block_depth = 0;
        let saved_eval_depth = self.eval_depth;
        self.eval_depth = 0;
        let saved_loop_iterations = self.loop_iterations;
        self.loop_iterations = 0;

        // Members of the calling group must not leak into the callee body. A function or
        // closure body can open its own group(s); those push fresh entries onto this stack.
        let saved_member_stack = std::mem::take(&mut self.member_stack);

        // Evaluate body, catching ReturnSignal.
        let result = match self.eval_expr(body) {
            Ok(val) => Ok(val),
            Err(e) => match e.kind() {
                InterpretErrorKind::ReturnSignal(val) => Ok(val.clone()),
                _ => Err(e),
            },
        };

        self.env.pop_scope();
        if captured.is_some() {
            self.env.pop_scope();
        }
        self.call_depth -= 1;
        self.block_depth = saved_block_depth;
        self.eval_depth = saved_eval_depth;
        self.loop_iterations = saved_loop_iterations;
        self.member_stack = saved_member_stack;

        let value = result?;

        // Check return type if declared.
        if let Some(ret_type) = return_type
            && !matches!(value, Value::Unit)
            && !value_matches_type(&value, ret_type)
        {
            return Err(self.error(
                call_expr,
                InterpretErrorKind::ReturnTypeMismatch(display_name.clone()),
                Some(&format!(
                    "'{}' declares return type '{ret_type:?}', but returned '{}'",
                    display_name,
                    value.type_name()
                )),
                None,
            ));
        }

        if let Some(ret_type) = return_type {
            self.check_array_size(&value, ret_type, call_expr)?;
        }

        Ok(value)
    }

    /// Evaluate call arguments and type-check them against parameter types.
    fn eval_call_args(
        &mut self,
        _call_expr: &Expr,
        _callee: &Expr,
        arg_exprs: &[Expr],
        params: &[Parameter],
    ) -> Result<Vec<Value>, InterpretError> {
        let mut arg_values = Vec::with_capacity(arg_exprs.len());

        for (i, arg_expr) in arg_exprs.iter().enumerate() {
            let val = self.eval_expr(arg_expr)?;
            let param = &params[i];

            if !value_matches_type(&val, &param.type_id) {
                self.call_depth -= 1;
                return Err(self.error(
                    arg_expr,
                    InterpretErrorKind::TypeMismatch(param.name.clone()),
                    Some(&format!(
                        "Expected type '{:?}' for parameter '{}', got '{}'",
                        param.type_id,
                        param.name,
                        val.type_name()
                    )),
                    None,
                ));
            }

            if let Err(e) = self.check_array_size(&val, &param.type_id, arg_expr) {
                self.call_depth -= 1;
                return Err(e);
            }

            arg_values.push(val);
        }

        Ok(arg_values)
    }

    fn eval_member_access(
        &mut self,
        access_expr: &Expr,
        object: &Expr,
        field: &str,
    ) -> Result<Value, InterpretError> {
        let receiver = self.eval_expr(object)?;

        match receiver {
            Value::Node { id, name, schema } => {
                if schema.ports.iter().any(|(p, _)| p == field) {
                    Ok(Value::PortRef {
                        node_id: id,
                        node_name: name,
                        schema,
                        port_name: field.to_owned(),
                    })
                } else {
                    Err(self.error(
                        access_expr,
                        InterpretErrorKind::UndefinedPort {
                            type_name: schema.name.clone(),
                            port: field.to_owned(),
                        },
                        Some(&format!(
                            "Node type '{}' has no port named '{field}'",
                            schema.name
                        )),
                        None,
                    ))
                }
            }
            Value::Group {
                id, name, schema, ..
            } => {
                if schema.ports.iter().any(|(p, _)| p == field) {
                    Ok(Value::PortRef {
                        node_id: NodeId(id.0),
                        node_name: name,
                        schema,
                        port_name: field.to_owned(),
                    })
                } else {
                    Err(self.error(
                        access_expr,
                        InterpretErrorKind::UndefinedPort {
                            type_name: name.clone(),
                            port: field.to_owned(),
                        },
                        Some(&format!("Group '{name}' has no port named '{field}'")),
                        None,
                    ))
                }
            }
            _ => Err(self.error(
                access_expr,
                InterpretErrorKind::UndefinedField {
                    type_name: receiver.type_name().to_owned(),
                    field: field.to_owned(),
                },
                Some(&format!(
                    "'.{field}' is not valid on a value of type '{}'",
                    receiver.type_name()
                )),
                None,
            )),
        }
    }

    fn eval_connect_stmt(
        &mut self,
        from_expr: &Expr,
        targets: &[Expr],
        loc: &Location,
    ) -> Result<(), InterpretError> {
        let from_value = self.eval_expr(from_expr)?;

        let (from_id, from_name, from_schema, from_port) = match from_value {
            Value::PortRef {
                node_id,
                node_name,
                schema,
                port_name,
            } => (node_id, node_name, schema, port_name),
            other => {
                return Err(self.error(
                    from_expr,
                    InterpretErrorKind::NotAPortRef {
                        side: "Left",
                        type_name: other.type_name().to_owned(),
                    },
                    Some("The left side of '->' must evaluate to a port reference."),
                    None,
                ));
            }
        };

        let from_dir = lookup_port_direction(&from_schema, &from_port);

        for to_expr in targets {
            self.record_connection_target(from_id, &from_name, &from_port, from_dir, to_expr, loc)?;
        }

        Ok(())
    }

    fn record_connection_target(
        &mut self,
        from_id: NodeId,
        from_name: &str,
        from_port: &str,
        from_dir: PortDirection,
        to_expr: &Expr,
        loc: &Location,
    ) -> Result<(), InterpretError> {
        let to_value = self.eval_expr(to_expr)?;

        let (to_id, to_name, to_schema, to_port) = match to_value {
            Value::PortRef {
                node_id,
                node_name,
                schema,
                port_name,
            } => (node_id, node_name, schema, port_name),
            other => {
                return Err(self.error(
                    to_expr,
                    InterpretErrorKind::NotAPortRef {
                        side: "Right",
                        type_name: other.type_name().to_owned(),
                    },
                    Some("The right side of '->' must evaluate to a port reference."),
                    None,
                ));
            }
        };

        let to_dir = lookup_port_direction(&to_schema, &to_port);

        if !is_valid_direction_pair(from_dir, to_dir) {
            return Err(self.error_at(
                loc,
                Some(2),
                InterpretErrorKind::InvalidConnectionDirection {
                    from_node: from_name.to_owned(),
                    from_port: from_port.to_owned(),
                    from_dir: from_dir.to_string(),
                    to_node: to_name,
                    to_port,
                    to_dir: to_dir.to_string(),
                },
                Some(
                    "Legal direction pairs are 'out -> in', 'out -> inout', 'inout -> in', and 'inout -> inout'.",
                ),
                None,
            ));
        }

        // Check for duplicate in the active connection list (group-local or top-level).
        let already_exists = if let Some(group_conns) = self.connection_stack.last() {
            group_conns.iter().any(|c| {
                c.from_node_id == from_id
                    && c.from_port == from_port
                    && c.to_node_id == to_id
                    && c.to_port == to_port
            })
        } else {
            self.connections.iter().any(|c| {
                c.from_node_id == from_id
                    && c.from_port == from_port
                    && c.to_node_id == to_id
                    && c.to_port == to_port
            })
        };

        if already_exists {
            return Err(self.error_at(
                loc,
                Some(2),
                InterpretErrorKind::DuplicateConnection {
                    from_node: from_name.to_owned(),
                    from_port: from_port.to_owned(),
                    to_node: to_name,
                    to_port,
                },
                Some("An identical connection was already declared earlier."),
                None,
            ));
        }

        let conn = InternalConnection {
            from_node_id: from_id,
            from_port: from_port.to_owned(),
            to_node_id: to_id,
            to_port,
        };
        if let Some(group_conns) = self.connection_stack.last_mut() {
            group_conns.push(conn);
        } else {
            self.connections.push(conn);
        }

        Ok(())
    }

    fn eval_group_expr(
        &mut self,
        _group_expr: &Expr,
        stmts: &[Stmt],
        binding_name: Option<String>,
    ) -> Result<Value, InterpretError> {
        let name = binding_name.unwrap_or_else(|| format!("group_{}", self.next_instance_id));

        let id = GroupId(self.next_instance_id);
        self.next_instance_id += 1;

        // Push group-evaluation context.
        self.member_stack.push(HashMap::new());
        self.connection_stack.push(Vec::new());
        self.port_schema_stack.push(Vec::new());
        self.env.push_scope();

        let eval_result: Result<(), InterpretError> = (|| {
            for stmt in stmts {
                self.eval_stmt(stmt)?;
            }
            Ok(())
        })();

        if eval_result.is_ok() {
            self.check_unused_bindings();
        }

        self.env.pop_scope();
        let mut connections = self.connection_stack.pop().unwrap_or_default();
        let members = self.member_stack.pop().unwrap_or_default();
        let port_schema_raw = self.port_schema_stack.pop().unwrap_or_default();

        eval_result?;

        // Generate InternalConnections for each port declaration target, then strip the
        // target lists to produce the plain (name, direction) schema.
        let group_node_id = NodeId(id.0);
        let port_schema: Vec<(String, PortDirection)> = port_schema_raw
            .into_iter()
            .map(|(pname, dir, targets)| {
                for (target_id, target_port) in targets {
                    match dir {
                        PortDirection::In => {
                            connections.push(InternalConnection {
                                from_node_id: group_node_id,
                                from_port: pname.clone(),
                                to_node_id: target_id,
                                to_port: target_port,
                            });
                        }
                        PortDirection::Out => {
                            connections.push(InternalConnection {
                                from_node_id: target_id,
                                from_port: target_port,
                                to_node_id: group_node_id,
                                to_port: pname.clone(),
                            });
                        }
                        PortDirection::Inout => {
                            connections.push(InternalConnection {
                                from_node_id: group_node_id,
                                from_port: pname.clone(),
                                to_node_id: target_id,
                                to_port: target_port.clone(),
                            });
                            connections.push(InternalConnection {
                                from_node_id: target_id,
                                from_port: target_port,
                                to_node_id: group_node_id,
                                to_port: pname.clone(),
                            });
                        }
                    }
                }
                (pname, dir)
            })
            .collect();

        let schema = Arc::new(NodeTypeSchema {
            name: format!("<group:{name}>"),
            ports: port_schema,
        });

        Ok(Value::Group {
            id,
            name,
            members,
            connections,
            schema,
        })
    }

    /// Evaluate `self.NAME = expr;` inside a group body.
    fn eval_member_decl(
        &mut self,
        member_name: &str,
        expr: &Expr,
        loc: &Location,
    ) -> Result<(), InterpretError> {
        // Defined at the top so it precedes all statements (clippy::items_after_statements).
        enum ClaimKind {
            Node(u64),
            Group(u64),
        }

        if self.member_stack.is_empty() {
            return Err(self.error_at(
                loc,
                Some(4), // "self"
                InterpretErrorKind::MemberDeclOutsideGroup {
                    name: member_name.to_owned(),
                },
                Some("Member declarations are only valid inside a 'group { ... }' body."),
                None,
            ));
        }

        // Check for reassignment before evaluating RHS.
        if self
            .member_stack
            .last()
            .is_some_and(|m| m.contains_key(member_name))
        {
            return Err(self.error_at(
                loc,
                Some(4),
                InterpretErrorKind::MemberReassignment {
                    name: member_name.to_owned(),
                },
                Some("Member bindings are immutable; use a different name."),
                None,
            ));
        }

        let value = self.eval_init_expr_with_binding(expr, member_name)?;

        // Validate: RHS must be a Node or Group, and track which map to use.
        let claim = match &value {
            Value::Node { id, .. } => ClaimKind::Node(id.0),
            Value::Group { id, .. } => ClaimKind::Group(id.0),
            other => {
                return Err(self.error_at(
                    loc,
                    None,
                    InterpretErrorKind::InvalidMember {
                        name: member_name.to_owned(),
                        type_name: other.type_name().to_owned(),
                    },
                    Some("Only node instances and group values can be group members."),
                    None,
                ));
            }
        };

        // Parent-claim check: each node/group id may be owned by at most one member binding.
        let owner_label = format!("<group@depth{}>.{}", self.member_stack.len(), member_name);
        let existing = match &claim {
            ClaimKind::Node(raw) => self.claimed_node_ids.get(raw),
            ClaimKind::Group(raw) => self.claimed_group_ids.get(raw),
        };
        if let Some(prev_owner) = existing {
            return Err(self.error_at(
                loc,
                None,
                InterpretErrorKind::DuplicateMembership {
                    name: member_name.to_owned(),
                    existing_owner: prev_owner.clone(),
                },
                Some("A node or group instance can only be bound to one member."),
                None,
            ));
        }
        match claim {
            ClaimKind::Node(raw) => {
                self.claimed_node_ids.insert(raw, owner_label);
            }
            ClaimKind::Group(raw) => {
                self.claimed_group_ids.insert(raw, owner_label);
            }
        }

        self.member_stack
            .last_mut()
            .expect("member_stack non-empty (checked above)")
            .insert(member_name.to_owned(), value);

        Ok(())
    }

    /// Evaluate `port NAME = expr, ...;` inside a group body.
    ///
    /// Evaluates each target expression, validates all are `PortRef`s with consistent
    /// direction, and appends the port entry to `port_schema_stack`. Fan-out connections
    /// are generated in `eval_group_expr` after the group ID is known.
    fn eval_port_decl(
        &mut self,
        name: &str,
        exprs: &[Expr],
        loc: &Location,
    ) -> Result<(), InterpretError> {
        if self.port_schema_stack.is_empty() {
            return Err(self.error_at(
                loc,
                Some(4), // "port"
                InterpretErrorKind::PortDeclOutsideGroup,
                Some("Port declarations are only valid inside a 'group { ... }' body."),
                None,
            ));
        }

        if self
            .port_schema_stack
            .last()
            .is_some_and(|s| s.iter().any(|(p, _, _)| p == name))
        {
            return Err(self.error_at(
                loc,
                None,
                InterpretErrorKind::DuplicatePort {
                    name: name.to_owned(),
                },
                Some(&format!(
                    "Port '{name}' is already declared in this group body."
                )),
                None,
            ));
        }

        let mut targets: Vec<(NodeId, String)> = Vec::new();
        let mut direction: Option<PortDirection> = None;

        for expr in exprs {
            let val = self.eval_expr(expr)?;

            let Value::PortRef {
                node_id,
                schema,
                port_name,
                ..
            } = val
            else {
                return Err(self.error(
                    expr,
                    InterpretErrorKind::PortDeclNotAPortRef {
                        type_name: val.type_name().to_owned(),
                    },
                    Some("Port declaration target must evaluate to a port reference."),
                    None,
                ));
            };

            let dir = schema
                .ports
                .iter()
                .find(|(p, _)| p == &port_name)
                .map(|(_, d)| *d)
                .expect("PortRef port_name is always valid in its own schema");

            if let Some(prev) = direction {
                if prev != dir {
                    return Err(self.error(
                        expr,
                        InterpretErrorKind::PortDeclDirectionMismatch {
                            port: name.to_owned(),
                            expected: prev,
                            got: dir,
                        },
                        Some(&format!(
                            "All targets of port '{name}' must have the same direction."
                        )),
                        None,
                    ));
                }
            } else {
                direction = Some(dir);
            }

            targets.push((node_id, port_name));
        }

        let direction = direction.expect("exprs is non-empty (parser guarantee)");

        self.port_schema_stack
            .last_mut()
            .expect("port_schema_stack non-empty (checked above)")
            .push((name.to_owned(), direction, targets));

        Ok(())
    }

    /// Evaluate `self.field` inside a group body; looks up the field in the innermost
    /// active member map.  Only called from `eval_expr` when it detects the pattern
    /// `Expr::MemberAccess { object: Expr::Self_, .. }`.
    fn eval_self_member_access(
        &mut self,
        field: &str,
        loc: &Location,
    ) -> Result<Value, InterpretError> {
        if self.member_stack.is_empty() {
            return Err(self.error_at(
                loc,
                Some(4), // "self"
                InterpretErrorKind::SelfOutsideGroup,
                Some("'self.NAME' is only valid inside a 'group { ... }' body."),
                None,
            ));
        }

        let members = self.member_stack.last().expect("checked non-empty");
        if let Some(value) = members.get(field) {
            Ok(value.clone())
        } else {
            Err(self.error_at(
                loc,
                None,
                InterpretErrorKind::UndefinedField {
                    type_name: "Group".to_owned(),
                    field: field.to_owned(),
                },
                Some(&format!(
                    "No member '{field}' declared with 'self.{field} = ...' in this group body."
                )),
                None,
            ))
        }
    }

    fn eval_method_call(
        &mut self,
        call_expr: &Expr,
        object: &Expr,
        method: &str,
        arg_exprs: &[Expr],
    ) -> Result<Value, InterpretError> {
        let receiver = self.eval_expr(object)?;

        let mut args = Vec::with_capacity(arg_exprs.len());
        for arg_expr in arg_exprs {
            args.push(self.eval_expr(arg_expr)?);
        }

        builtin_methods::dispatch(&receiver, method, &args).map_err(|e| match e {
            builtin_methods::MethodError::ArityMismatch {
                type_name,
                method,
                expected,
                got,
            } => self.error(
                call_expr,
                InterpretErrorKind::MethodArityMismatch {
                    type_name,
                    method: method.clone(),
                    expected,
                    got,
                },
                Some(&format!("'{method}' takes {expected} argument(s)")),
                None,
            ),
            builtin_methods::MethodError::UndefinedMethod { type_name, method } => self.error(
                call_expr,
                InterpretErrorKind::UndefinedMethod {
                    type_name: type_name.clone(),
                    method: method.clone(),
                },
                Some(&format!("Type '{type_name}' has no method '{method}'")),
                None,
            ),
        })
    }

    fn eval_ref(&mut self, inner: &Expr) -> Result<Value, InterpretError> {
        let Expr::Identifier { name, .. } = inner else {
            return Err(self.error(
                inner,
                InterpretErrorKind::InvalidRefTarget,
                Some("References can only be created to named variables"),
                None,
            ));
        };

        // Verify the variable exists.
        if self.env.get(name).is_none() {
            return Err(self.error(
                inner,
                InterpretErrorKind::UndefinedIdentifier(name.clone()),
                Some("Cannot create a reference to an undefined variable"),
                None,
            ));
        }

        let scope_id = self.env.scope_id_of(name).unwrap();

        Ok(Value::Ref {
            name: name.clone(),
            scope_id,
        })
    }

    /// Resolve a value, following reference chains to the concrete value.
    fn resolve_ref(&self, value: &Value, expr: &Expr) -> Result<Value, InterpretError> {
        let mut current = value.clone();
        let mut depth = 0;

        while let Value::Ref { ref name, scope_id } = current {
            depth += 1;
            if depth > 100 {
                return Err(self.error(
                    expr,
                    InterpretErrorKind::OrphanedReference(name.clone()),
                    Some("Reference chain too deep"),
                    None,
                ));
            }

            let Some(binding) = self.env.get_in_scope(name, scope_id) else {
                return Err(self.error(
                    expr,
                    InterpretErrorKind::OrphanedReference(name.clone()),
                    Some("The referenced variable's scope has been destroyed"),
                    None,
                ));
            };

            current = binding.value.clone();
        }

        Ok(current)
    }

    /// Follow a reference chain to find the ultimate non-ref target's name and scope ID.
    fn resolve_ref_target(
        &self,
        name: &str,
        scope_id: u64,
        expr: &Expr,
    ) -> Result<(String, u64), InterpretError> {
        let mut current_name = name.to_owned();
        let mut current_scope = scope_id;
        let mut depth = 0;

        loop {
            depth += 1;
            if depth > 100 {
                return Err(self.error(
                    expr,
                    InterpretErrorKind::OrphanedReference(current_name.clone()),
                    Some("Reference chain too deep"),
                    None,
                ));
            }

            let Some(binding) = self.env.get_in_scope(&current_name, current_scope) else {
                return Err(self.error(
                    expr,
                    InterpretErrorKind::OrphanedReference(current_name.clone()),
                    Some("The referenced variable's scope has been destroyed"),
                    None,
                ));
            };

            match &binding.value {
                Value::Ref {
                    name: next_name,
                    scope_id: next_scope,
                } => {
                    current_name.clone_from(next_name);
                    current_scope = *next_scope;
                }
                _ => return Ok((current_name, current_scope)),
            }
        }
    }

    fn eval_while(&mut self, condition: &Expr, body: &Expr) -> Result<Value, InterpretError> {
        loop {
            self.check_loop_limit(condition)?;

            let condition_value = self.eval_expr(condition)?;

            let Value::Boolean(cond) = condition_value else {
                return Err(self.error(
                    condition,
                    InterpretErrorKind::ConditionNotBoolean,
                    Some("Expected a boolean expression as condition"),
                    None,
                ));
            };

            if !cond {
                break;
            }

            match self.eval_expr(body) {
                Ok(_) => {}
                Err(e) => match e.kind() {
                    InterpretErrorKind::BreakSignal(val) => return Ok(val.clone()),
                    InterpretErrorKind::ContinueSignal => {} // continue the loop
                    _ => return Err(e),
                },
            }
        }

        Ok(Value::Unit)
    }

    fn eval_loop(&mut self, body: &Expr) -> Result<Value, InterpretError> {
        loop {
            self.check_loop_limit(body)?;

            match self.eval_expr(body) {
                Ok(_) => {}
                Err(e) => match e.kind() {
                    InterpretErrorKind::BreakSignal(val) => return Ok(val.clone()),
                    InterpretErrorKind::ContinueSignal => {} // continue the loop
                    _ => return Err(e),
                },
            }
        }
    }

    fn eval_for(
        &mut self,
        variable: &str,
        var_loc: &Location,
        iterable: &Expr,
        body: &Expr,
    ) -> Result<Value, InterpretError> {
        let iter_val = self.eval_expr(iterable)?;

        let mut var_ever_used = variable == "_";

        let result = match iter_val {
            Value::Range {
                start,
                end,
                inclusive,
            } => {
                let mut current = start;
                while if inclusive {
                    current <= end
                } else {
                    current < end
                } {
                    self.check_loop_limit(body)?;

                    let binding =
                        Binding::variable(Value::Number(current), Some(TypeId::Number), false);
                    let (body_result, used) = self.eval_for_body(variable, binding, body)?;
                    var_ever_used |= used;
                    match body_result {
                        ForBodyResult::Break(val) => return Ok(val),
                        ForBodyResult::Continue | ForBodyResult::Ok => {}
                    }
                    current += 1.0;
                }

                Ok(Value::Unit)
            }

            Value::Array(elements) => {
                for element in elements {
                    self.check_loop_limit(body)?;

                    if matches!(element, Value::Uninitialized) {
                        return Err(self.error(
                            iterable,
                            InterpretErrorKind::UninitializedRead("<array element>".to_owned()),
                            Some("Cannot iterate over an uninitialized array element"),
                            None,
                        ));
                    }

                    let elem_type = Some(value_to_type_id(&element));
                    let binding = Binding::variable(element, elem_type, false);
                    let (body_result, used) = self.eval_for_body(variable, binding, body)?;
                    var_ever_used |= used;
                    match body_result {
                        ForBodyResult::Break(val) => return Ok(val),
                        ForBodyResult::Continue | ForBodyResult::Ok => {}
                    }
                }

                Ok(Value::Unit)
            }

            _ => Err(self.error(
                iterable,
                InterpretErrorKind::ForIterableNotIterable,
                Some("Expected a range or array expression after 'in'"),
                None,
            )),
        };

        if result.is_ok() && !var_ever_used {
            self.push_warning(
                var_loc,
                Some(variable.len()),
                InterpretWarningKind::UnusedValue,
            );
        }

        result
    }

    fn check_loop_limit(&mut self, span: &Expr) -> Result<(), InterpretError> {
        self.loop_iterations += 1;
        if self.loop_iterations > MAX_LOOP_ITERATIONS {
            return Err(self.error(
                span,
                InterpretErrorKind::LoopIterationLimitExceeded(MAX_LOOP_ITERATIONS),
                Some(&format!(
                    "Loop iteration limit ({MAX_LOOP_ITERATIONS}) exceeded"
                )),
                None,
            ));
        }

        Ok(())
    }

    /// Returns `(ForBodyResult, var_was_used)`.
    fn eval_for_body(
        &mut self,
        variable: &str,
        binding: Binding,
        body: &Expr,
    ) -> Result<(ForBodyResult, bool), InterpretError> {
        self.env.push_scope();
        if variable != "_" {
            self.env.define(variable.to_owned(), binding);
        }
        let outcome = self.eval_expr(body);
        let var_used = variable != "_" && self.env.get(variable).is_some_and(|b| b.used);
        match outcome {
            Ok(_) => {
                self.env.pop_scope();
                Ok((ForBodyResult::Ok, var_used))
            }
            Err(e) => match e.kind() {
                InterpretErrorKind::BreakSignal(val) => {
                    let result = val.clone();
                    self.env.pop_scope();
                    Ok((ForBodyResult::Break(result), var_used))
                }
                InterpretErrorKind::ContinueSignal => {
                    self.env.pop_scope();
                    Ok((ForBodyResult::Continue, var_used))
                }
                _ => {
                    self.env.pop_scope();
                    Err(e)
                }
            },
        }
    }

    fn eval_range(
        &mut self,
        start_expr: &Expr,
        end_expr: &Expr,
        inclusive: bool,
    ) -> Result<Value, InterpretError> {
        let start_val = self.eval_expr(start_expr)?;
        let Value::Number(start) = start_val else {
            return Err(self.error(
                start_expr,
                InterpretErrorKind::RangeBoundNotNumeric,
                Some("Range bounds must be numeric"),
                None,
            ));
        };

        let end_val = self.eval_expr(end_expr)?;
        let Value::Number(end) = end_val else {
            return Err(self.error(
                end_expr,
                InterpretErrorKind::RangeBoundNotNumeric,
                Some("Range bounds must be numeric"),
                None,
            ));
        };

        Ok(Value::Range {
            start,
            end,
            inclusive,
        })
    }

    fn eval_unary_expr(&mut self, op: &UnaryOp, factor: &Expr) -> Result<Value, InterpretError> {
        Ok(match op {
            UnaryOp::Plus => {
                let val = self.eval_expr(factor)?;
                if !matches!(val, Value::Number(_)) {
                    return Err(self.error(
                        factor,
                        InterpretErrorKind::InvalidUnaryPlus(val.type_name()),
                        None,
                        None,
                    ));
                }
                val
            }
            UnaryOp::Minus => {
                let val = self.eval_expr(factor)?;
                Value::Number(-1.0)
                    .mul(val)
                    .map_err(|e| self.value_error_to_interpret_error(factor, e))?
            }
            UnaryOp::Not => self
                .eval_expr(factor)?
                .not()
                .map_err(|e| self.value_error_to_interpret_error(factor, e))?,
        })
    }

    fn eval_binary_expr(
        &mut self,
        binary_expr: &Expr,
        op: &BinaryOp,
        lhs: &Expr,
        rhs: &Expr,
    ) -> Result<Value, InterpretError> {
        let left = self.eval_expr(lhs)?;
        let right = self.eval_expr(rhs)?;

        let result = match op {
            BinaryOp::Add => left.add(right),
            BinaryOp::Subtract => left.sub(right),
            BinaryOp::Multiply => left.mul(right),
            BinaryOp::Divide => left.div(right),
            BinaryOp::Modulo => left.rem(right),
            BinaryOp::Eq => left.eq(&right),
            BinaryOp::NotEq => left.neq(&right),
            BinaryOp::Gt => left.gt(&right),
            BinaryOp::GtEq => left.gteq(&right),
            BinaryOp::Lt => left.lt(&right),
            BinaryOp::LtEq => left.lteq(&right),
            BinaryOp::LogicalAnd => left.and(&right),
            BinaryOp::LogicalOr => left.or(&right),
        };

        result.map_err(|e| self.value_error_to_interpret_error(binary_expr, e))
    }

    /// Check that a variable is assignable (not const and either mutable or uninitialized).
    fn check_assignable(
        &self,
        name: &str,
        is_const: bool,
        is_mutable: bool,
        is_uninitialized: bool,
        loc: &Location,
    ) -> Result<(), InterpretError> {
        if is_const {
            return Err(self.error_at(
                loc,
                Some(name.len()),
                InterpretErrorKind::AssignToConstant(name.to_owned()),
                Some("Constants cannot be reassigned"),
                Some("Consider using 'let mut' instead of 'const' if you need to reassign"),
            ));
        }
        if !is_mutable && !is_uninitialized {
            return Err(self.error_at(
                loc,
                Some(name.len()),
                InterpretErrorKind::AssignToImmutable(name.to_owned()),
                Some("Immutable variables cannot be reassigned"),
                Some("Consider using 'let mut' instead of 'let' if you need to reassign"),
            ));
        }
        Ok(())
    }

    /// Write a concrete value through a reference chain to the ultimate referent.
    fn eval_ref_write_through(
        &mut self,
        ref_name: &str,
        ref_scope: u64,
        lhs_name: &str,
        value: Value,
        expr: &Expr,
        loc: &Location,
    ) -> Result<(), InterpretError> {
        let (target_name, target_scope) = self.resolve_ref_target(ref_name, ref_scope, expr)?;

        // Check the referent's mutability.
        let (target_is_const, target_is_mutable, target_is_uninitialized, target_type_id) = {
            let Some(target_binding) = self.env.get_in_scope(&target_name, target_scope) else {
                return Err(self.error(
                    expr,
                    InterpretErrorKind::OrphanedReference(target_name.clone()),
                    Some("The referenced variable's scope has been destroyed"),
                    None,
                ));
            };
            (
                target_binding.is_const,
                target_binding.mutable,
                matches!(target_binding.value, Value::Uninitialized),
                target_binding.type_id.clone(),
            )
        };

        if target_is_const {
            return Err(self.error_at(
                loc,
                Some(lhs_name.len()),
                InterpretErrorKind::AssignToConstant(target_name.clone()),
                Some("Cannot write through reference: target is a constant"),
                None,
            ));
        }
        if !target_is_mutable && !target_is_uninitialized {
            return Err(self.error_at(
                loc,
                Some(lhs_name.len()),
                InterpretErrorKind::AssignToImmutable(target_name.clone()),
                Some("Cannot write through reference: target is immutable"),
                Some("Consider declaring the target variable with 'let mut'"),
            ));
        }

        if let Some(tid) = &target_type_id
            && !value_matches_type(&value, tid)
        {
            return Err(self.error(
                expr,
                InterpretErrorKind::TypeMismatch(target_name.clone()),
                Some(&format!(
                    "Expected type '{tid:?}', got '{}'",
                    value.type_name()
                )),
                None,
            ));
        }

        let target_binding = self
            .env
            .get_mut_in_scope(&target_name, target_scope)
            .unwrap();
        target_binding.value = value;

        if target_binding.type_id.is_none()
            && target_type_id.is_none()
            && !value_has_uninitialized_elements(&target_binding.value)
        {
            target_binding.type_id = Some(value_to_type_id(&target_binding.value));
        }

        Ok(())
    }

    /// Dispatch assignment based on target expression type.
    fn eval_assignment_stmt(
        &mut self,
        target: &Expr,
        expr: &Expr,
        loc: &Location,
    ) -> Result<(), InterpretError> {
        match target {
            Expr::Identifier { name, .. } => self.eval_assignment(name, expr, loc),
            Expr::Index { array, index, .. } => {
                self.eval_index_assignment(target, array, index, expr, loc)
            }
            _ => unreachable!("Parser should only allow Identifier or Index as assignment target"),
        }
    }

    fn eval_assignment(
        &mut self,
        name: &str,
        expr: &Expr,
        loc: &Location,
    ) -> Result<(), InterpretError> {
        // Extract binding info before eval_expr to avoid borrow conflict.
        let (is_const, is_mutable, is_uninitialized, type_id, held_ref) = {
            let Some(binding) = self.env.get(name) else {
                return Err(self.error_at(
                    loc,
                    Some(name.len()),
                    InterpretErrorKind::UndefinedIdentifier(name.to_owned()),
                    Some("Cannot assign to an undefined variable"),
                    None,
                ));
            };
            let held_ref = match &binding.value {
                Value::Ref {
                    name: ref_name,
                    scope_id,
                } => Some((ref_name.clone(), *scope_id)),
                _ => None,
            };
            (
                binding.is_const,
                binding.mutable,
                matches!(binding.value, Value::Uninitialized),
                binding.type_id.clone(),
                held_ref,
            )
        };

        // Evaluate the RHS.
        let value = self.eval_expr(expr)?;

        if matches!(value, Value::Unit) {
            return Err(self.error(
                expr,
                InterpretErrorKind::ExpectedValue,
                Some("Block must produce a value when used in assignment"),
                None,
            ));
        }

        // If the LHS currently holds a reference, handle rebind or write-through.
        if let Some((ref_name, ref_scope)) = held_ref {
            if matches!(value, Value::Ref { .. }) {
                // RHS is a ref: rebind the LHS variable itself.
                self.check_assignable(name, is_const, is_mutable, is_uninitialized, loc)?;
                let binding = self.env.get_mut(name).unwrap();
                binding.value = value;
                return Ok(());
            }

            // RHS is concrete: write through the reference chain.
            return self.eval_ref_write_through(&ref_name, ref_scope, name, value, expr, loc);
        }

        // LHS is not a reference: standard direct assignment.
        self.check_assignable(name, is_const, is_mutable, is_uninitialized, loc)?;

        if let Some(tid) = &type_id
            && !value_matches_type(&value, tid)
            && !matches!(value, Value::Ref { .. })
        {
            return Err(self.error(
                expr,
                InterpretErrorKind::TypeMismatch(name.to_owned()),
                Some(&format!(
                    "Expected type '{tid:?}', got '{}'",
                    value.type_name()
                )),
                None,
            ));
        }

        if let Some(tid) = &type_id {
            self.check_array_size(&value, tid, expr)?;
        }

        let binding = self.env.get_mut(name).unwrap();
        binding.value = value;

        if binding.type_id.is_none()
            && type_id.is_none()
            && !matches!(binding.value, Value::Ref { .. })
            && !value_has_uninitialized_elements(&binding.value)
        {
            binding.type_id = Some(value_to_type_id(&binding.value));
        }

        Ok(())
    }

    /// Evaluate an indexed assignment: `arr[i] = value`.
    fn eval_index_assignment(
        &mut self,
        target_expr: &Expr,
        array_expr: &Expr,
        index_expr: &Expr,
        value_expr: &Expr,
        loc: &Location,
    ) -> Result<(), InterpretError> {
        // Resolve the root identifier name from the index expression.
        let root_name = extract_root_identifier(array_expr);
        let Some(name) = root_name else {
            return Err(self.error(
                target_expr,
                InterpretErrorKind::InvalidRefTarget,
                Some("Array index assignment requires a named variable"),
                None,
            ));
        };

        // Check mutability.
        let (is_const, is_mutable) = {
            let Some(binding) = self.env.get(&name) else {
                return Err(self.error_at(
                    loc,
                    Some(name.len()),
                    InterpretErrorKind::UndefinedIdentifier(name.clone()),
                    Some("Cannot assign to an undefined variable"),
                    None,
                ));
            };
            (binding.is_const, binding.mutable)
        };

        if is_const {
            return Err(self.error_at(
                loc,
                Some(name.len()),
                InterpretErrorKind::AssignToConstant(name.clone()),
                Some("Cannot assign to elements of a constant array"),
                None,
            ));
        }

        if !is_mutable {
            return Err(self.error_at(
                loc,
                Some(name.len()),
                InterpretErrorKind::AssignToImmutable(name.clone()),
                Some("Cannot assign to elements of an immutable array"),
                Some("Consider using 'let mut' instead of 'let'"),
            ));
        }

        // Evaluate index.
        let index_val = self.eval_expr(index_expr)?;
        let Value::Number(n) = index_val else {
            return Err(self.error(
                index_expr,
                InterpretErrorKind::IndexNotInteger,
                Some("Array index must be a non-negative integer"),
                None,
            ));
        };

        if n < 0.0 || n.fract() != 0.0 {
            return Err(self.error(
                index_expr,
                InterpretErrorKind::IndexNotInteger,
                Some("Array index must be a non-negative integer"),
                None,
            ));
        }

        let idx = f64_to_usize(n);

        self.validate_index_target(&name, idx, array_expr, index_expr)?;

        // Evaluate the RHS value.
        let value = self.eval_expr(value_expr)?;

        self.validate_element_type(&name, &value, value_expr)?;

        // Mutate the array element.
        let binding = self.env.get_mut(&name).unwrap();
        let Value::Array(ref mut elements) = binding.value else {
            unreachable!("Already validated as array above");
        };

        elements[idx] = value;
        Ok(())
    }

    fn validate_index_target(
        &self,
        name: &str,
        idx: usize,
        array_expr: &Expr,
        index_expr: &Expr,
    ) -> Result<(), InterpretError> {
        let binding = self.env.get(name).unwrap();
        match &binding.value {
            Value::Array(elements) => {
                if idx >= elements.len() {
                    return Err(self.error(
                        index_expr,
                        InterpretErrorKind::IndexOutOfBounds {
                            index: usize_to_i64(idx),
                            length: elements.len(),
                        },
                        Some(&format!(
                            "Index {} is out of bounds for array of length {}",
                            idx,
                            elements.len()
                        )),
                        None,
                    ));
                }
            }
            other => {
                return Err(self.error(
                    array_expr,
                    InterpretErrorKind::IndexOnNonArray(other.type_name().to_owned()),
                    Some("Only arrays can be indexed for assignment"),
                    None,
                ));
            }
        }

        Ok(())
    }

    fn validate_element_type(
        &self,
        name: &str,
        value: &Value,
        value_expr: &Expr,
    ) -> Result<(), InterpretError> {
        let binding = self.env.get(name).unwrap();

        // Prefer the declared element type from the type annotation; fall back to
        // inferring from the first array element (skipping Uninitialized placeholders).
        let mismatch = if let Some(TypeId::Array(elem_type, _)) = &binding.type_id {
            !value_matches_type(value, elem_type)
        } else if let Value::Array(elements) = &binding.value
            && let Some(first) = elements.first()
            && !matches!(first, Value::Uninitialized)
        {
            first.type_name() != value.type_name()
        } else {
            false
        };

        if mismatch {
            let expected = if let Some(TypeId::Array(elem_type, _)) = &binding.type_id {
                format!("{elem_type:?}")
            } else if let Value::Array(elements) = &binding.value {
                elements[0].type_name().to_owned()
            } else {
                unreachable!()
            };
            return Err(self.error(
                value_expr,
                InterpretErrorKind::ArrayElementTypeMismatch,
                Some(&format!(
                    "Expected type '{expected}', got '{}'",
                    value.type_name()
                )),
                None,
            ));
        }

        Ok(())
    }

    fn eval_const_decl(
        &mut self,
        name: &str,
        type_id: &TypeId,
        expr: &Expr,
        loc: &Location,
    ) -> Result<(), InterpretError> {
        self.reject_node_type_shadow(name, loc, name.len() + 6)?;

        if self.env.contains(name) {
            return Err(self.error_at(
                loc,
                Some(name.len() + 6), // "const " + name.
                InterpretErrorKind::DuplicateDeclaration(name.to_owned()),
                Some("A constant with this name already exists"),
                None,
            ));
        }

        let value = self.eval_init_expr_with_binding(expr, name)?;

        if matches!(value, Value::Unit) {
            return Err(self.error(
                expr,
                InterpretErrorKind::ExpectedValue,
                Some("Block must produce a value when used in initialization"),
                None,
            ));
        }

        if !value_matches_type(&value, type_id) {
            return Err(self.error(
                expr,
                InterpretErrorKind::TypeMismatch(name.to_owned()),
                Some(&format!(
                    "Expected type '{type_id:?}', got '{}'",
                    value_type_name(&value)
                )),
                None,
            ));
        }

        self.check_array_size(&value, type_id, expr)?;

        self.env.define(
            name.to_owned(),
            Binding::constant(value, type_id.clone()).with_loc(*loc),
        );

        Ok(())
    }

    fn eval_var_decl(
        &mut self,
        name: &str,
        mutable: bool,
        type_id: Option<&TypeId>,
        expr: Option<&Expr>,
        loc: &Location,
    ) -> Result<(), InterpretError> {
        if name == "_" {
            if let Some(init_expr) = expr {
                self.eval_expr(init_expr)?;
            }
            return Ok(());
        }

        self.reject_node_type_shadow(name, loc, name.len() + 4)?;

        let (value, inferred_type_id) = self.eval_var_init(name, type_id, expr)?;

        // Shadowing is allowed; warn if the displaced binding was never used.
        let displaced = self.env.define(
            name.to_owned(),
            Binding::variable(value, inferred_type_id, mutable).with_loc(loc.with_column_offset(4)), // Compensate for 'let '.
        );

        if let Some(old) = displaced
            && !old.used
            && !name.starts_with('_')
            && let Some(old_loc) = old.loc
        {
            self.push_warning(
                &old_loc,
                Some(name.len()),
                InterpretWarningKind::UnusedValue,
            );
        }

        Ok(())
    }

    /// Evaluate the initial value for a `let` binding, applying type checks and size enforcement.
    ///
    /// Returns `(value, inferred_type_id)` for use by [`eval_var_decl`].
    fn eval_var_init(
        &mut self,
        name: &str,
        type_id: Option<&TypeId>,
        expr: Option<&Expr>,
    ) -> Result<(Value, Option<TypeId>), InterpretError> {
        if let Some(init_expr) = expr {
            let val = self.eval_init_expr_with_binding(init_expr, name)?;

            // References are transparent in the type system; skip type inference
            // but still validate against explicit type annotations.
            if matches!(val, Value::Ref { .. }) {
                if let Some(id) = type_id {
                    let resolved = self.resolve_ref(&val, init_expr)?;
                    if !value_matches_type(&resolved, id) {
                        return Err(self.error(
                            init_expr,
                            InterpretErrorKind::TypeMismatch(name.to_owned()),
                            Some(&format!(
                                "Expected type '{id:?}', got '{}'",
                                value_type_name(&resolved)
                            )),
                            None,
                        ));
                    }
                }
                return Ok((val, None));
            }

            if matches!(val, Value::Unit) {
                return Err(self.error(
                    init_expr,
                    InterpretErrorKind::ExpectedValue,
                    Some("Block must produce a value when used in initialization"),
                    None,
                ));
            }

            if let Some(id) = type_id
                && !value_matches_type(&val, id)
            {
                return Err(self.error(
                    init_expr,
                    InterpretErrorKind::TypeMismatch(name.to_owned()),
                    Some(&format!(
                        "Expected type '{id:?}', got '{}'",
                        value_type_name(&val)
                    )),
                    None,
                ));
            }

            if let Some(id) = type_id {
                self.check_array_size(&val, id, init_expr)?;
            }

            let inferred = if let Some(id) = type_id {
                id.clone()
            } else {
                if value_contains_untyped_empty_array(&val) {
                    return Err(self.error(
                        init_expr,
                        InterpretErrorKind::TypeMismatch(name.to_owned()),
                        Some(
                            "Cannot infer element type of an array containing empty arrays; add a type annotation like ': [[Type]]'",
                        ),
                        None,
                    ));
                }
                if value_has_uninitialized_elements(&val) {
                    return Err(self.error(
                        init_expr,
                        InterpretErrorKind::TypeMismatch(name.to_owned()),
                        Some(
                            "Cannot infer element type of an array containing uninitialized elements; add a type annotation",
                        ),
                        None,
                    ));
                }
                value_to_type_id(&val)
            };
            Ok((val, Some(inferred)))
        } else if let Some(TypeId::Array(elem_type, Some(length_expr))) = type_id {
            let val = self.eval_array_fill_from_type(elem_type, length_expr)?;
            Ok((
                val,
                Some(TypeId::Array(elem_type.clone(), Some(length_expr.clone()))),
            ))
        } else {
            Ok((Value::Uninitialized, type_id.cloned()))
        }
    }

    fn eval_identifier(&mut self, name: &str, expr: &Expr) -> Result<Value, InterpretError> {
        if let Some(binding) = self.env.get_mut(name) {
            let value = &binding.value;
            if matches!(value, Value::NodeType(_)) {
                return Err(self.error(
                    expr,
                    InterpretErrorKind::NodeTypeNotAValue(name.to_owned()),
                    Some("Node type names refer to schemas, not values"),
                    Some("Use 'node TypeName' to instantiate, or declare a different name"),
                ));
            }
            if matches!(value, Value::Uninitialized) {
                return Err(self.error(
                    expr,
                    InterpretErrorKind::UninitializedRead(name.to_owned()),
                    Some("Assign a value before reading"),
                    None,
                ));
            }
            binding.used = true;
            let value = binding.value.clone();
            let resolved = self.resolve_ref(&value, expr)?;
            if matches!(resolved, Value::Uninitialized) {
                return Err(self.error(
                    expr,
                    InterpretErrorKind::UninitializedRead(name.to_owned()),
                    Some("The variable this reference points to has not been initialized"),
                    None,
                ));
            }
            return Ok(resolved);
        }

        // Fall back to current group's members (implicit `self.`).
        if let Some(members) = self.member_stack.last()
            && let Some(value) = members.get(name)
        {
            return Ok(value.clone());
        }

        Err(self.error(
            expr,
            InterpretErrorKind::UndefinedIdentifier(name.to_owned()),
            Some("This identifier has not been defined"),
            None,
        ))
    }

    fn eval_assert(
        &mut self,
        condition: &Expr,
        message: Option<&Expr>,
        loc: &Location,
    ) -> Result<(), InterpretError> {
        let cond_val = self.eval_expr(condition)?;

        let Value::Boolean(cond) = cond_val else {
            return Err(self.error(
                condition,
                InterpretErrorKind::ConditionNotBoolean,
                Some("Assertion condition must evaluate to a boolean"),
                None,
            ));
        };

        if !cond {
            let msg = if let Some(msg_expr) = message {
                let msg_val = self.eval_expr(msg_expr)?;
                let type_name = value_type_name(&msg_val);
                if let Value::String(s) = msg_val {
                    Some(s)
                } else {
                    return Err(self.error(
                        msg_expr,
                        InterpretErrorKind::TypeMismatch("assert message".to_owned()),
                        Some(&format!(
                            "Assertion message must be a String, got '{type_name}'"
                        )),
                        None,
                    ));
                }
            } else {
                None
            };

            return Err(self.error_at(
                loc,
                Some("assert".len()),
                InterpretErrorKind::AssertionFailed(msg),
                None,
                None,
            ));
        }

        Ok(())
    }

    fn eval_print(&mut self, expr: &Expr) -> Result<(), InterpretError> {
        println!("{}", self.eval_expr(expr)?);
        Ok(())
    }

    /// Convert a `ValueError` to an `InterpretError`.
    fn value_error_to_interpret_error(&self, expr: &Expr, error: ValueError) -> InterpretError {
        let kind = match error {
            ValueError::InvalidAddition(l, r) => InterpretErrorKind::InvalidAddition(l, r),
            ValueError::InvalidSubtraction(l, r) => InterpretErrorKind::InvalidSubtraction(l, r),
            ValueError::InvalidMultiplication(l, r) => {
                InterpretErrorKind::InvalidMultiplication(l, r)
            }
            ValueError::InvalidDivision(l, r) => InterpretErrorKind::InvalidDivision(l, r),
            ValueError::InvalidModulo(l, r) => InterpretErrorKind::InvalidModulo(l, r),
            ValueError::DivisionByZero => InterpretErrorKind::DivisionByZero,
            ValueError::InvalidComparison(l, r) => InterpretErrorKind::InvalidComparison(l, r),
            ValueError::InvalidLogicalNot(t) => InterpretErrorKind::InvalidLogicalNot(t),
            ValueError::InvalidLogicalAnd(l, r) => InterpretErrorKind::InvalidLogicalAnd(l, r),
            ValueError::InvalidLogicalOr(l, r) => InterpretErrorKind::InvalidLogicalOr(l, r),
        };

        self.error(expr, kind, None, None)
    }
}

impl InterpretErrorExt for Interpreter<'_> {
    fn error(
        &self,
        expr: &Expr,
        kind: InterpretErrorKind,
        message: Option<&str>,
        help: Option<&str>,
    ) -> InterpretError {
        let loc = expr.loc();
        let length = calculate_expr_length(expr);

        InterpretError::new(self.source.into(), loc, kind, message, help, length)
    }

    fn error_at(
        &self,
        loc: &Location,
        length: Option<usize>,
        kind: InterpretErrorKind,
        message: Option<&str>,
        help: Option<&str>,
    ) -> InterpretError {
        InterpretError::new(self.source.into(), loc, kind, message, help, length)
    }
}

/// Calculate the approximate length of an expression for error highlighting.
fn calculate_expr_length(expr: &Expr) -> Option<usize> {
    match expr {
        Expr::Literal(Literal::String(s, _)) => Some(s.len() + 2), // +2 for quotes.
        Expr::Literal(Literal::Number(n, _)) => Some(n.to_string().len()),
        Expr::Literal(Literal::Boolean(b, _)) => Some(b.to_string().len()),
        Expr::Identifier { name, .. } => Some(name.len()),
        Expr::Binary { lhs, rhs, .. } => {
            // For binary expressions, calculate from the start of lhs to the end of rhs.
            let lhs_loc = lhs.loc();
            let rhs_loc = rhs.loc();
            let rhs_len = calculate_expr_length(rhs).unwrap_or(1);

            // Calculate the span: from start of lhs to end of rhs
            let start_index = lhs_loc.index;
            let end_index = rhs_loc.index + rhs_len;
            Some(end_index - start_index)
        }
        Expr::Break { value: None, .. } => Some(5), // "break"
        Expr::Array { .. }
        | Expr::ArrayFill { .. }
        | Expr::Break { value: Some(_), .. }
        | Expr::Block { .. }
        | Expr::Call { .. }
        | Expr::Closure { .. }
        | Expr::Continue { .. }
        | Expr::For { .. }
        | Expr::Group { .. }
        | Expr::If { .. }
        | Expr::Index { .. }
        | Expr::MemberAccess { .. }
        | Expr::MethodCall { .. }
        | Expr::Loop { .. }
        | Expr::Node { .. }
        | Expr::Range { .. }
        | Expr::Ref { .. }
        | Expr::Self_ { .. }
        | Expr::While { .. } => None,
        Expr::Unary { factor, .. } => {
            // For unary expressions, add 1 for the operator plus the factor length.
            calculate_expr_length(factor).map(|len| len + 1)
        }
    }
}

/// Check if a value matches a type identifier.
fn value_matches_type(value: &Value, type_id: &TypeId) -> bool {
    match (value, type_id) {
        (Value::Boolean(_), TypeId::Boolean)
        | (Value::Group { .. }, TypeId::Group)
        | (Value::Node { .. }, TypeId::Node)
        | (Value::Number(_), TypeId::Number)
        | (Value::PortRef { .. }, TypeId::PortRef)
        | (Value::Range { .. }, TypeId::Range)
        | (Value::String(_), TypeId::String) => true,
        (Value::Array(elements), TypeId::Array(elem_type, _)) => {
            elements.iter().all(|e| value_matches_type(e, elem_type))
        }
        (
            Value::Function {
                params,
                return_type,
                ..
            }
            | Value::Closure {
                params,
                return_type,
                ..
            },
            TypeId::Fn {
                params: expected_params,
                return_type: expected_return,
            },
        ) => {
            if params.len() != expected_params.len() {
                return false;
            }

            for (param, expected) in params.iter().zip(expected_params.iter()) {
                if !type_id_matches(&param.type_id, expected) {
                    return false;
                }
            }

            match (return_type, expected_return) {
                (None, _) | (_, None) => true,
                (Some(ret), Some(expected_ret)) => type_id_matches(ret, expected_ret),
            }
        }
        _ => false,
    }
}

/// Check if two type IDs are structurally equal.
fn type_id_matches(a: &TypeId, b: &TypeId) -> bool {
    match (a, b) {
        (TypeId::Boolean, TypeId::Boolean)
        | (TypeId::Group, TypeId::Group)
        | (TypeId::Node, TypeId::Node)
        | (TypeId::Number, TypeId::Number)
        | (TypeId::PortRef, TypeId::PortRef)
        | (TypeId::Range, TypeId::Range)
        | (TypeId::String, TypeId::String) => true,
        (TypeId::Array(a_elem, _), TypeId::Array(b_elem, _)) => type_id_matches(a_elem, b_elem),
        (
            TypeId::Fn {
                params: a_params,
                return_type: a_ret,
            },
            TypeId::Fn {
                params: b_params,
                return_type: b_ret,
            },
        ) => {
            a_params.len() == b_params.len()
                && a_params
                    .iter()
                    .zip(b_params.iter())
                    .all(|(a, b)| type_id_matches(a, b))
                && match (a_ret, b_ret) {
                    (None, _) | (_, None) => true,
                    (Some(a_r), Some(b_r)) => type_id_matches(a_r, b_r),
                }
        }
        _ => false,
    }
}

/// Get the type name of a value.
fn value_type_name(value: &Value) -> &'static str {
    match value {
        Value::Array(_) => "Array",
        Value::Boolean(_) => "bool",
        Value::Closure { .. } | Value::Function { .. } => "fn",
        Value::Group { .. } => "Group",
        Value::Node { .. } => "Node",
        Value::Number(_) => "Number",
        Value::NodeType(_) => "NodeType",
        Value::PortRef { .. } => "PortRef",
        Value::Range { .. } => "Range",
        Value::Ref { .. } => "&ref",
        Value::String(_) => "String",
        Value::Unit => "()",
        Value::Uninitialized => "<uninitialized>",
    }
}

/// Returns true if `value` is or contains an empty array at any nesting level,
/// making element-type inference impossible without a type annotation.
fn value_contains_untyped_empty_array(value: &Value) -> bool {
    match value {
        Value::Array(elems) if elems.is_empty() => true,
        Value::Array(elems) => elems.iter().any(value_contains_untyped_empty_array),
        _ => false,
    }
}

/// Returns true if `value` is an array whose first element is `Uninitialized`,
/// making element-type inference impossible without a type annotation.
fn value_has_uninitialized_elements(value: &Value) -> bool {
    match value {
        Value::Array(elems) => elems
            .first()
            .is_some_and(|e| matches!(e, Value::Uninitialized)),
        _ => false,
    }
}

/// Convert a value to its corresponding type ID.
fn value_to_type_id(value: &Value) -> TypeId {
    match value {
        Value::Array(elements) => {
            let first_typed = elements.iter().find(|e| !matches!(e, Value::Uninitialized));
            match first_typed {
                Some(e) => TypeId::Array(Box::new(value_to_type_id(e)), None),
                // Empty or all-uninitialized: element type is unknown.
                None => TypeId::Array(Box::new(TypeId::Number), None),
            }
        }
        Value::Boolean(_) => TypeId::Boolean,
        Value::Closure {
            params,
            return_type,
            ..
        }
        | Value::Function {
            params,
            return_type,
            ..
        } => TypeId::Fn {
            params: params.iter().map(|p| p.type_id.clone()).collect(),
            return_type: return_type.as_ref().map(|t| Box::new(t.clone())),
        },
        Value::Group { .. } => TypeId::Group,
        Value::Node { .. } => TypeId::Node,
        Value::Number(_) => TypeId::Number,
        Value::NodeType(_) => {
            unreachable!("Node type schemas are not usable as values and cannot be typed")
        }
        Value::PortRef { .. } => TypeId::PortRef,
        Value::String(_) => TypeId::String,
        Value::Range { .. } => TypeId::Range,
        Value::Ref { .. } => unreachable!("References should be resolved before type inference"),
        Value::Unit => unreachable!("Cannot determine type of unit value"),
        Value::Uninitialized => unreachable!("Cannot determine type of uninitialized value"),
    }
}

/// Collect free variables from an expression tree.
///
/// `bound` contains names that are already bound (parameters, local declarations).
///
/// Returns The set of identifier names that are referenced but not locally bound.
///
fn collect_free_vars(expr: &Expr, bound: &mut HashSet<String>) -> HashSet<String> {
    let mut free = HashSet::new();
    collect_free_vars_expr(expr, bound, &mut free);
    free
}

#[allow(clippy::too_many_lines)]
fn collect_free_vars_expr(expr: &Expr, bound: &mut HashSet<String>, free: &mut HashSet<String>) {
    match expr {
        Expr::Identifier { name, .. } => {
            if !bound.contains(name) {
                free.insert(name.clone());
            }
        }
        Expr::Literal(_) | Expr::Continue { .. } | Expr::Self_ { .. } => {}
        Expr::Array { elements, .. } => {
            for e in elements {
                collect_free_vars_expr(e, bound, free);
            }
        }
        Expr::ArrayFill { length, .. } => {
            collect_free_vars_expr(length, bound, free);
        }
        Expr::Binary { lhs, rhs, .. }
        | Expr::Range {
            start: lhs,
            end: rhs,
            ..
        } => {
            collect_free_vars_expr(lhs, bound, free);
            collect_free_vars_expr(rhs, bound, free);
        }
        Expr::Unary { factor, .. } | Expr::Ref { expr: factor, .. } => {
            collect_free_vars_expr(factor, bound, free);
        }
        Expr::Block { stmts, result, .. } => {
            let snapshot: HashSet<String> = bound.clone();
            for stmt in stmts {
                collect_free_vars_stmt(stmt, bound, free);
            }
            if let Some(result) = result {
                collect_free_vars_expr(result, bound, free);
            }
            *bound = snapshot;
        }
        Expr::Call { callee, args, .. } => {
            collect_free_vars_expr(callee, bound, free);
            for arg in args {
                collect_free_vars_expr(arg, bound, free);
            }
        }
        Expr::Closure { params, body, .. } => {
            let snapshot: HashSet<String> = bound.clone();
            for p in params {
                bound.insert(p.name.clone());
            }
            collect_free_vars_expr(body, bound, free);
            *bound = snapshot;
        }
        Expr::If {
            condition,
            then_branch,
            else_branch,
            ..
        } => {
            collect_free_vars_expr(condition, bound, free);
            collect_free_vars_expr(then_branch, bound, free);
            if let Some(else_br) = else_branch {
                collect_free_vars_expr(else_br, bound, free);
            }
        }
        Expr::Index { array, index, .. } => {
            collect_free_vars_expr(array, bound, free);
            collect_free_vars_expr(index, bound, free);
        }
        Expr::MemberAccess { object, .. } => {
            collect_free_vars_expr(object, bound, free);
        }
        Expr::MethodCall { object, args, .. } => {
            collect_free_vars_expr(object, bound, free);
            for arg in args {
                collect_free_vars_expr(arg, bound, free);
            }
        }
        Expr::For {
            variable,
            iterable,
            body,
            ..
        } => {
            collect_free_vars_expr(iterable, bound, free);
            let snapshot: HashSet<String> = bound.clone();
            bound.insert(variable.clone());
            collect_free_vars_expr(body, bound, free);
            *bound = snapshot;
        }
        Expr::Loop { body, .. } => {
            collect_free_vars_expr(body, bound, free);
        }
        Expr::While {
            condition, body, ..
        } => {
            collect_free_vars_expr(condition, bound, free);
            collect_free_vars_expr(body, bound, free);
        }
        Expr::Break { value, .. } => {
            if let Some(v) = value {
                collect_free_vars_expr(v, bound, free);
            }
        }
        Expr::Node { type_name, .. } => {
            if !bound.contains(type_name) {
                free.insert(type_name.clone());
            }
        }
        // Group bodies have their own member namespace; only recurse into ordinary stmts.
        Expr::Group { stmts, .. } => {
            let snapshot = bound.clone();
            for stmt in stmts {
                collect_free_vars_stmt(stmt, bound, free);
            }
            *bound = snapshot;
        }
    }
}

fn collect_free_vars_stmt(stmt: &Stmt, bound: &mut HashSet<String>, free: &mut HashSet<String>) {
    match stmt {
        Stmt::VarDecl { name, expr, .. } => {
            if let Some(e) = expr {
                collect_free_vars_expr(e, bound, free);
            }
            bound.insert(name.clone());
        }
        Stmt::ConstDecl { name, expr, .. } => {
            collect_free_vars_expr(expr, bound, free);
            bound.insert(name.clone());
        }
        Stmt::Assignment { target, expr, .. } => {
            collect_free_vars_expr(target, bound, free);
            collect_free_vars_expr(expr, bound, free);
        }
        Stmt::Connect { from, targets, .. } => {
            collect_free_vars_expr(from, bound, free);
            for t in targets {
                collect_free_vars_expr(t, bound, free);
            }
        }
        Stmt::FnDecl { name, body, .. } => {
            bound.insert(name.clone());
            collect_free_vars_expr(body, bound, free);
        }
        Stmt::NodeTypeDecl { .. } | Stmt::UseDecl { .. } => {
            // No sub-expressions; nothing to capture.
        }
        Stmt::Return { expr, .. } => {
            if let Some(e) = expr {
                collect_free_vars_expr(e, bound, free);
            }
        }
        Stmt::Assert {
            condition, message, ..
        } => {
            collect_free_vars_expr(condition, bound, free);
            if let Some(m) = message {
                collect_free_vars_expr(m, bound, free);
            }
        }
        Stmt::PortDecl { exprs, .. } => {
            for e in exprs {
                collect_free_vars_expr(e, bound, free);
            }
        }
        Stmt::MemberDecl { expr, .. } | Stmt::Expr { expr, .. } | Stmt::Print { expr } => {
            collect_free_vars_expr(expr, bound, free);
        }
    }
}

/// Look up a port's direction on a schema.
///
/// The port is assumed to exist; `.port` access (Step 4) validates this before a
/// `PortRef` is ever produced. A miss here would indicate a schema-bookkeeping bug
/// in the interpreter, not a user error.
///
fn lookup_port_direction(schema: &NodeTypeSchema, port_name: &str) -> PortDirection {
    schema
        .ports
        .iter()
        .find(|(p, _)| p == port_name)
        .map(|(_, d)| *d)
        .expect("PortRef references a port that does not exist on its carried schema")
}

/// Check whether a source/destination direction pair is legal for a connection.
///
/// Schema-agnostic: operates on `PortDirection` values alone, so the same validator
/// works for node and group endpoints (D18).
///
fn is_valid_direction_pair(from: PortDirection, to: PortDirection) -> bool {
    matches!(
        (from, to),
        (
            PortDirection::Out | PortDirection::Inout,
            PortDirection::In | PortDirection::Inout
        )
    )
}

/// Extract the root identifier name from a (potentially nested) index expression.
fn extract_root_identifier(expr: &Expr) -> Option<String> {
    match expr {
        Expr::Identifier { name, .. } => Some(name.clone()),
        Expr::Index { array, .. } => extract_root_identifier(array),
        _ => None,
    }
}

/// Convert a validated non-negative whole `f64` to `usize`.
///
/// # Safety (logical)
///
/// The caller must have already validated that `n >= 0.0` and `n.fract() == 0.0`.
///
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn f64_to_usize(n: f64) -> usize {
    debug_assert!(n >= 0.0);
    debug_assert!(n.fract() == 0.0);

    n as usize
}

/// Convert a `usize` to `i64` for error reporting.
#[allow(clippy::cast_possible_wrap)]
fn usize_to_i64(n: usize) -> i64 {
    n as i64
}

/// Set the `name` field of a `Value::Group` or `Value::Node` in-place; no-op for all other types.
fn force_rename(mut value: Value, name: &str) -> Value {
    match &mut value {
        Value::Group { name: n, .. } | Value::Node { name: n, .. } => name.clone_into(n),
        _ => {}
    }
    value
}

/// Build a `Network` from the single program result value and top-level connections.
fn build_network(
    root: Option<(&Value, &str)>,
    top_level_connections: Vec<InternalConnection>,
) -> Network {
    let mut all_connections = top_level_connections;

    let root_node = root.and_then(|(value, name)| {
        collect_connections(value, &mut all_connections);
        value_to_node(value, name)
    });

    let connections = all_connections
        .into_iter()
        .map(|ic| Connection {
            from: PortRef {
                node_id: ic.from_node_id.0,
                port_name: ic.from_port,
            },
            to: PortRef {
                node_id: ic.to_node_id.0,
                port_name: ic.to_port,
            },
        })
        .collect();

    Network {
        root: root_node,
        connections,
    }
}

/// Convert a `Value` to a public `Node`, returning `None` for non-network values.
fn value_to_node(value: &Value, name: &str) -> Option<Node> {
    match value {
        Value::Node { id, schema, .. } => Some(Node {
            id: id.0,
            name: name.to_owned(),
            type_name: schema.name.clone(),
            ports: schema_ports_to_public(&schema.ports),
            children: vec![],
        }),
        Value::Group {
            id,
            members,
            schema,
            ..
        } => {
            let mut children: Vec<Node> = Vec::new();
            let mut member_list: Vec<(&String, &Value)> = members.iter().collect();
            member_list.sort_by_key(|(k, _)| k.as_str());
            for (member_name, member_value) in member_list {
                collect_children(member_value, member_name, &mut children);
            }
            Some(Node {
                id: id.0,
                name: name.to_owned(),
                type_name: "group".to_owned(),
                ports: schema_ports_to_public(&schema.ports),
                children,
            })
        }
        _ => None,
    }
}

/// Recursively collect `Node` children from a value, handling arrays of nodes/groups.
fn collect_children(value: &Value, name: &str, children: &mut Vec<Node>) {
    match value {
        Value::Node { .. } | Value::Group { .. } => {
            if let Some(node) = value_to_node(value, name) {
                children.push(node);
            }
        }
        Value::Array(elements) => {
            for (i, elem) in elements.iter().enumerate() {
                collect_children(elem, &format!("{name}/{i}"), children);
            }
        }
        _ => {}
    }
}

/// Recursively collect all `InternalConnection`s from groups in a value tree.
fn collect_connections(value: &Value, connections: &mut Vec<InternalConnection>) {
    match value {
        Value::Group {
            connections: group_conns,
            members,
            ..
        } => {
            connections.extend(group_conns.iter().cloned());
            for member_value in members.values() {
                collect_connections(member_value, connections);
            }
        }
        Value::Array(elements) => {
            for elem in elements {
                collect_connections(elem, connections);
            }
        }
        _ => {}
    }
}

fn schema_ports_to_public(ports: &[(String, PortDirection)]) -> Vec<Port> {
    ports
        .iter()
        .map(|(name, dir)| Port {
            name: name.clone(),
            direction: *dir,
        })
        .collect()
}
