use alloc::{format, string::String, vec, vec::Vec};

use crate::{
    ExprId, SourceForm, SourceId, Span,
    ast::{Ast, BinaryOp, CompareOp, Expr, Field, Literal, UnaryOp},
    constraints::normalize_constraints,
    diagnostic::{Diagnostic, DiagnosticKind, Result},
    embedding::HostValue,
    module::{EmptyLoader, LoadedSource, Module, SourceLoader},
    parse_source_with_source_id,
    runtime::{
        AbstractValue, Binding, ConcreteValue, Constraint, ConstraintEntry, Data, DataField, Env,
        EnvId, ExprRef, FunctionParam, FunctionValue, LiteralValue, ModuleId, ObjectField,
        ObjectValue, PrimitiveType, RuntimeValue, Thunk, ThunkId, ThunkKind, ThunkState,
    },
};

pub struct Engine<L = EmptyLoader> {
    loader: L,
    prelude_env: EnvId,
    modules: Vec<Module>,
    thunks: Vec<Thunk>,
    envs: Vec<Env>,
}

impl Engine<EmptyLoader> {
    pub fn from_parse(ast: Ast, root: ExprId) -> Self {
        let mut this = Self::new(EmptyLoader);
        this.register_parsed(
            String::from("<memory>"),
            String::from("<memory>"),
            SourceId(0),
            ast,
            root,
            SourceForm::Expr,
        );
        this
    }
}

impl<L: SourceLoader> Engine<L> {
    pub fn new(loader: L) -> Self {
        Self {
            loader,
            prelude_env: EnvId(0),
            modules: Vec::new(),
            thunks: Vec::new(),
            envs: vec![Env {
                parent: None,
                bindings: Vec::new(),
            }],
        }
    }

    pub fn bind_global(&mut self, name: impl Into<String>, value: HostValue) -> Result<ThunkId> {
        let value = self.internalize_host_value(value)?;
        let thunk = self.add_value_thunk(value);
        self.bind(self.prelude_env, name.into(), thunk);
        Ok(thunk)
    }

    pub fn bind_global_runtime(&mut self, name: impl Into<String>, value: RuntimeValue) -> ThunkId {
        let thunk = self.add_value_thunk(value);
        self.bind(self.prelude_env, name.into(), thunk);
        thunk
    }

    pub fn add_root_source(
        &mut self,
        key: impl Into<String>,
        name: impl Into<String>,
        source: &str,
    ) -> Result<ModuleId> {
        self.add_source(key.into(), name.into(), source)
    }

    pub fn eval_module(&mut self, module: ModuleId) -> Result<RuntimeValue> {
        let thunk = self.modules[module.0 as usize].root_thunk;
        self.force(thunk)
    }

    pub fn eval_root(&mut self) -> Result<RuntimeValue> {
        self.eval_module(ModuleId(0))
    }

    pub fn materialize(&mut self, value: &RuntimeValue) -> Result<Data> {
        self.materialize_with_path(value, &mut Vec::new())
    }

    pub fn source_name(&self, source: SourceId) -> Option<&str> {
        self.modules
            .iter()
            .find(|module| module.source == source)
            .map(|module| module.name.as_str())
    }

    pub fn format_diagnostic(&self, diagnostic: &Diagnostic) -> String {
        format_diagnostic_with(diagnostic, |source| self.source_name(source))
    }

    fn materialize_with_path(
        &mut self,
        value: &RuntimeValue,
        path: &mut Vec<String>,
    ) -> Result<Data> {
        match value {
            RuntimeValue::Concrete(value) => match value {
                ConcreteValue::String(value) => Ok(Data::String(value.clone())),
                ConcreteValue::Int(value) => Ok(Data::Int(*value)),
                ConcreteValue::Float(value) => Ok(Data::Float(*value)),
                ConcreteValue::Bool(value) => Ok(Data::Bool(*value)),
                ConcreteValue::Array(items) => {
                    let mut data = Vec::new();
                    for item in items {
                        let value = self.force(*item)?;
                        data.push(self.materialize_with_path(&value, path)?);
                    }
                    Ok(Data::Array(data))
                }
                ConcreteValue::Object(object) => {
                    let mut fields = Vec::new();
                    for field in &object.fields {
                        let value = self.force(field.value)?;
                        path.push(field.name.clone());
                        let value = self.materialize_with_path(&value, path);
                        path.pop();
                        fields.push(DataField {
                            name: field.name.clone(),
                            value: value?,
                        });
                    }
                    Ok(Data::Object(fields))
                }
                ConcreteValue::Function(_) => Err(self.with_path_context(
                    Diagnostic::new(
                        DiagnosticKind::Materialize,
                        Span::default(),
                        "cannot materialize function value",
                    ),
                    "materializing",
                    path,
                )),
            },
            RuntimeValue::Abstract(abstract_value) => {
                let Some(default) = abstract_value.default else {
                    let mut diagnostic = Diagnostic::new(
                        DiagnosticKind::Materialize,
                        constraint_primary_span(&abstract_value.constraints),
                        "cannot materialize unresolved abstract value without default",
                    );
                    for constraint in &abstract_value.constraints {
                        diagnostic = diagnostic.with_label(
                            constraint.span,
                            "constraint still requires a concrete value or default",
                        );
                    }
                    return Err(self.with_path_context(diagnostic, "materializing", path));
                };
                let default_span = self.thunk_span(default);
                let value = self.force(default)?;
                self.ensure_satisfies(
                    &value,
                    &abstract_value.constraints,
                    default_span,
                    Some((default_span, "default value checked here")),
                )
                .map_err(|diag| self.with_path_context(diag, "materializing", path))?;
                self.materialize_with_path(&value, path)
            }
        }
    }

    fn add_source(&mut self, key: String, name: String, source: &str) -> Result<ModuleId> {
        if let Some(id) = self.find_module(&key) {
            return Ok(id);
        }
        let source_id = SourceId(self.modules.len() as u32);
        let parsed = parse_source_with_source_id(source_id, source)?;
        Ok(self.register_parsed(
            key,
            name,
            source_id,
            parsed.ast,
            parsed.root,
            parsed.source_form,
        ))
    }

    fn register_parsed(
        &mut self,
        key: String,
        name: String,
        source: SourceId,
        ast: Ast,
        root: ExprId,
        source_form: SourceForm,
    ) -> ModuleId {
        let module = ModuleId(self.modules.len() as u32);
        let root_env = self.new_env(Some(self.prelude_env));
        let root_thunk = if source_form == SourceForm::Fields {
            if let Expr::Object(fields) = ast.get(root).expr.clone() {
                let object = self
                    .build_object(module, &fields, root_env)
                    .expect("module object construction should not fail for parsed fields");
                for field in &object.fields {
                    self.bind(root_env, field.name.clone(), field.value);
                }
                self.add_value_thunk(RuntimeValue::Concrete(ConcreteValue::Object(object)))
            } else {
                self.add_expr_thunk(ExprRef { module, expr: root }, root_env)
            }
        } else {
            self.add_expr_thunk(ExprRef { module, expr: root }, root_env)
        };
        self.modules.push(Module {
            key,
            name,
            source,
            ast,
            root,
            source_form,
            root_env,
            root_thunk,
        });
        module
    }

    fn find_module(&self, key: &str) -> Option<ModuleId> {
        self.modules
            .iter()
            .position(|module| module.key == key)
            .map(|index| ModuleId(index as u32))
    }

    fn load_import(&mut self, current: ModuleId, specifier: &str) -> Result<ModuleId> {
        let current_key = self.modules[current.0 as usize].key.clone();
        let LoadedSource { key, name, source } = self.loader.load(Some(&current_key), specifier)?;
        self.add_source(key, name, &source)
    }

    fn eval_expr(&mut self, reference: ExprRef, env: EnvId) -> Result<RuntimeValue> {
        let span = self.expr_span(reference);
        let expr = self.expr(reference).clone();
        match expr {
            Expr::Literal(literal) => Ok(RuntimeValue::Concrete(literal_to_concrete(literal))),
            Expr::Ident(name) => self.eval_ident(&name, env, span),
            Expr::Object(fields) => self
                .build_object(reference.module, &fields, env)
                .map(|object| RuntimeValue::Concrete(ConcreteValue::Object(object))),
            Expr::Array(items) => {
                let thunks = items
                    .into_iter()
                    .map(|item| {
                        self.add_expr_thunk(
                            ExprRef {
                                module: reference.module,
                                expr: item,
                            },
                            env,
                        )
                    })
                    .collect();
                Ok(RuntimeValue::Concrete(ConcreteValue::Array(thunks)))
            }
            Expr::Let { bindings, body } => {
                let let_env = self.new_env(Some(env));
                for binding in bindings {
                    let name = field_name(&binding)?;
                    let thunk = self.add_expr_thunk(
                        ExprRef {
                            module: reference.module,
                            expr: binding.value,
                        },
                        let_env,
                    );
                    self.bind(let_env, name, thunk);
                }
                self.eval_expr(
                    ExprRef {
                        module: reference.module,
                        expr: body,
                    },
                    let_env,
                )
            }
            Expr::Import(specifier) => {
                let module = self.load_import(reference.module, &specifier)?;
                self.eval_module(module)
            }
            Expr::Path { base, field } => {
                let base = self.eval_expr(
                    ExprRef {
                        module: reference.module,
                        expr: base,
                    },
                    env,
                )?;
                let RuntimeValue::Concrete(ConcreteValue::Object(object)) = base else {
                    return Err(Diagnostic::new(
                        DiagnosticKind::TypeMismatch,
                        span,
                        "path base is not an object",
                    ));
                };
                let Some(thunk) = object
                    .fields
                    .iter()
                    .find(|item| item.name == field)
                    .map(|item| item.value)
                else {
                    return Err(Diagnostic::new(
                        DiagnosticKind::UnresolvedIdentifier,
                        span,
                        format!("unknown field `{field}`"),
                    ));
                };
                self.force(thunk)
            }
            Expr::Call { callee, args } => self.eval_call(
                ExprRef {
                    module: reference.module,
                    expr: callee,
                },
                args,
                reference.module,
                env,
                span,
            ),
            Expr::Function { params, body } => {
                let params = params
                    .into_iter()
                    .map(|param| FunctionParam {
                        name: param.name,
                        constraint: param.constraint.map(|expr| ExprRef {
                            module: reference.module,
                            expr,
                        }),
                    })
                    .collect();
                Ok(RuntimeValue::Concrete(ConcreteValue::Function(
                    FunctionValue {
                        params,
                        body: ExprRef {
                            module: reference.module,
                            expr: body,
                        },
                        env,
                    },
                )))
            }
            Expr::Match { scrutinee, arms } => {
                let value = self.eval_expr(
                    ExprRef {
                        module: reference.module,
                        expr: scrutinee,
                    },
                    env,
                )?;
                for arm in arms {
                    let pattern = ExprRef {
                        module: reference.module,
                        expr: arm.pattern,
                    };
                    if self.matches_pattern(&value, pattern, env)? {
                        return self.eval_expr(
                            ExprRef {
                                module: reference.module,
                                expr: arm.body,
                            },
                            env,
                        );
                    }
                }
                Err(Diagnostic::new(
                    DiagnosticKind::MatchFailure,
                    span,
                    "no match arm matched",
                ))
            }
            Expr::Unary { op, expr } => {
                let value = self.eval_expr(
                    ExprRef {
                        module: reference.module,
                        expr,
                    },
                    env,
                )?;
                match op {
                    UnaryOp::Neg => negate_number(value, span),
                    UnaryOp::Not => negate_bool(value, span),
                }
            }
            Expr::Binary { op, lhs, rhs } => {
                let lhs_value = self.eval_expr(
                    ExprRef {
                        module: reference.module,
                        expr: lhs,
                    },
                    env,
                )?;
                match op {
                    BinaryOp::LogicalAnd => {
                        if !bool_from_runtime(lhs_value, span)? {
                            return Ok(RuntimeValue::Concrete(ConcreteValue::Bool(false)));
                        }
                        let rhs_value = self.eval_expr(
                            ExprRef {
                                module: reference.module,
                                expr: rhs,
                            },
                            env,
                        )?;
                        Ok(RuntimeValue::Concrete(ConcreteValue::Bool(
                            bool_from_runtime(rhs_value, span)?,
                        )))
                    }
                    BinaryOp::LogicalOr => {
                        if bool_from_runtime(lhs_value, span)? {
                            return Ok(RuntimeValue::Concrete(ConcreteValue::Bool(true)));
                        }
                        let rhs_value = self.eval_expr(
                            ExprRef {
                                module: reference.module,
                                expr: rhs,
                            },
                            env,
                        )?;
                        Ok(RuntimeValue::Concrete(ConcreteValue::Bool(
                            bool_from_runtime(rhs_value, span)?,
                        )))
                    }
                    _ => {
                        let rhs_value = self.eval_expr(
                            ExprRef {
                                module: reference.module,
                                expr: rhs,
                            },
                            env,
                        )?;
                        match op {
                            BinaryOp::Add => {
                                arithmetic(lhs_value, rhs_value, ArithmeticOp::Add, span)
                            }
                            BinaryOp::Sub => {
                                arithmetic(lhs_value, rhs_value, ArithmeticOp::Sub, span)
                            }
                            BinaryOp::Mul => {
                                arithmetic(lhs_value, rhs_value, ArithmeticOp::Mul, span)
                            }
                            BinaryOp::Div => {
                                arithmetic(lhs_value, rhs_value, ArithmeticOp::Div, span)
                            }
                            BinaryOp::Concat => concat_arrays(lhs_value, rhs_value, span),
                            BinaryOp::Equal => {
                                compare_expr(lhs_value, rhs_value, CompareExprOp::Equal, span)
                            }
                            BinaryOp::NotEqual => {
                                compare_expr(lhs_value, rhs_value, CompareExprOp::NotEqual, span)
                            }
                            BinaryOp::Greater => {
                                compare_expr(lhs_value, rhs_value, CompareExprOp::Greater, span)
                            }
                            BinaryOp::GreaterEqual => compare_expr(
                                lhs_value,
                                rhs_value,
                                CompareExprOp::GreaterEqual,
                                span,
                            ),
                            BinaryOp::Less => {
                                compare_expr(lhs_value, rhs_value, CompareExprOp::Less, span)
                            }
                            BinaryOp::LessEqual => {
                                compare_expr(lhs_value, rhs_value, CompareExprOp::LessEqual, span)
                            }
                            BinaryOp::And => self.compose_and(lhs_value, rhs_value, span),
                            BinaryOp::Patch => self.patch(lhs_value, rhs_value),
                            BinaryOp::LogicalAnd | BinaryOp::LogicalOr => unreachable!(),
                        }
                    }
                }
            }
            Expr::Default { base, fallback } => {
                let base = self.eval_expr(
                    ExprRef {
                        module: reference.module,
                        expr: base,
                    },
                    env,
                )?;
                match base {
                    RuntimeValue::Abstract(mut abstract_value) => {
                        abstract_value.default = Some(self.add_expr_thunk(
                            ExprRef {
                                module: reference.module,
                                expr: fallback,
                            },
                            env,
                        ));
                        Ok(RuntimeValue::Abstract(abstract_value))
                    }
                    concrete @ RuntimeValue::Concrete(_) => Ok(concrete),
                }
            }
            Expr::CompareConstraint { op, value } => {
                let value_ref = ExprRef {
                    module: reference.module,
                    expr: value,
                };
                let value = self.eval_expr(value_ref, env)?;
                let value = literal_value_from_runtime(&value).ok_or_else(|| {
                    Diagnostic::new(
                        DiagnosticKind::TypeMismatch,
                        span,
                        "comparison constraint operand must be a literal",
                    )
                })?;
                Ok(RuntimeValue::Abstract(AbstractValue {
                    constraints: vec![ConstraintEntry {
                        constraint: Constraint::Compare(op, value),
                        span,
                    }],
                    default: None,
                }))
            }
            Expr::RegexConstraint(pattern) => Ok(RuntimeValue::Abstract(AbstractValue {
                constraints: vec![ConstraintEntry {
                    constraint: Constraint::Regex(pattern),
                    span,
                }],
                default: None,
            })),
            Expr::Wildcard => Err(Diagnostic::new(
                DiagnosticKind::UnsupportedFeature,
                span,
                "wildcard is only valid as a match pattern",
            )),
        }
    }

    fn eval_ident(&mut self, name: &str, env: EnvId, span: Span) -> Result<RuntimeValue> {
        if let Some(primitive) = primitive_type(name) {
            return Ok(RuntimeValue::Abstract(AbstractValue {
                constraints: vec![ConstraintEntry {
                    constraint: Constraint::Type(primitive),
                    span,
                }],
                default: None,
            }));
        }
        if let Some(thunk) = self.lookup(env, name) {
            return self.force(thunk);
        }
        if name.chars().next().is_some_and(char::is_uppercase) {
            return Ok(RuntimeValue::Abstract(AbstractValue {
                constraints: vec![ConstraintEntry {
                    constraint: Constraint::BuiltinPredicate(String::from(name)),
                    span,
                }],
                default: None,
            }));
        }
        Err(Diagnostic::new(
            DiagnosticKind::UnresolvedIdentifier,
            span,
            format!("unknown identifier `{name}`"),
        ))
    }

    fn build_object(
        &mut self,
        module: ModuleId,
        fields: &[Field],
        env: EnvId,
    ) -> Result<ObjectValue> {
        let mut object = ObjectValue { fields: Vec::new() };
        for field in fields {
            self.insert_field(
                &mut object,
                module,
                &field.path,
                field.value,
                env,
                field.span,
            )?;
        }
        Ok(object)
    }

    fn insert_field(
        &mut self,
        object: &mut ObjectValue,
        module: ModuleId,
        path: &[String],
        expr: ExprId,
        env: EnvId,
        span: Span,
    ) -> Result<()> {
        if path.is_empty() {
            return Err(Diagnostic::new(
                DiagnosticKind::Syntax,
                span,
                "empty field path",
            ));
        }
        if path.len() == 1 {
            let value = self.add_expr_thunk_with_span(ExprRef { module, expr }, env, span);
            if object.fields.iter().any(|field| field.name == path[0]) {
                return Err(Diagnostic::new(
                    DiagnosticKind::Conflict,
                    span,
                    format!("duplicate field `{}`", path[0]),
                ));
            }
            object.fields.push(ObjectField {
                name: path[0].clone(),
                value,
                span,
            });
            return Ok(());
        }

        let name = &path[0];
        if let Some(index) = object.fields.iter().position(|field| field.name == *name) {
            let existing = self.force(object.fields[index].value)?;
            let RuntimeValue::Concrete(ConcreteValue::Object(mut nested)) = existing else {
                return Err(Diagnostic::new(
                    DiagnosticKind::Conflict,
                    span,
                    format!("field `{name}` is already defined as a non-object"),
                ));
            };
            self.insert_field(&mut nested, module, &path[1..], expr, env, span)?;
            object.fields[index].value = self.add_value_thunk_with_span(
                RuntimeValue::Concrete(ConcreteValue::Object(nested)),
                span,
            );
            return Ok(());
        }

        let mut nested = ObjectValue { fields: Vec::new() };
        self.insert_field(&mut nested, module, &path[1..], expr, env, span)?;
        object.fields.push(ObjectField {
            name: name.clone(),
            value: self.add_value_thunk_with_span(
                RuntimeValue::Concrete(ConcreteValue::Object(nested)),
                span,
            ),
            span,
        });
        Ok(())
    }

    fn eval_call(
        &mut self,
        callee: ExprRef,
        args: Vec<ExprId>,
        caller_module: ModuleId,
        caller_env: EnvId,
        span: Span,
    ) -> Result<RuntimeValue> {
        let callee = self.eval_expr(callee, caller_env)?;
        let RuntimeValue::Concrete(ConcreteValue::Function(function)) = callee else {
            return Err(Diagnostic::new(
                DiagnosticKind::TypeMismatch,
                span,
                "callee is not a function",
            ));
        };
        if args.len() != function.params.len() {
            return Err(Diagnostic::new(
                DiagnosticKind::TypeMismatch,
                span,
                "function call argument count mismatch",
            ));
        }
        let call_env = self.new_env(Some(function.env));
        for (param, arg) in function.params.iter().zip(args) {
            let arg_ref = ExprRef {
                module: caller_module,
                expr: arg,
            };
            let arg_thunk = if let Some(constraint) = param.constraint {
                self.add_constrained_thunk(constraint, function.env, arg_ref, caller_env)
            } else {
                self.add_expr_thunk(arg_ref, caller_env)
            };
            self.bind(call_env, param.name.clone(), arg_thunk);
        }
        self.eval_expr(function.body, call_env)
    }

    fn matches_pattern(
        &mut self,
        value: &RuntimeValue,
        pattern: ExprRef,
        env: EnvId,
    ) -> Result<bool> {
        match self.expr(pattern).clone() {
            Expr::Wildcard => Ok(true),
            Expr::CompareConstraint { .. } | Expr::RegexConstraint(_) | Expr::Ident(_) => {
                let constraint = self.eval_expr(pattern, env)?;
                match constraint {
                    RuntimeValue::Abstract(abstract_value) => self
                        .ensure_satisfies(
                            value,
                            &abstract_value.constraints,
                            self.expr_span(pattern),
                            None,
                        )
                        .map(|_| true)
                        .or_else(|diag| match diag.kind {
                            DiagnosticKind::ConstraintViolation
                            | DiagnosticKind::UnsupportedFeature => Ok(false),
                            _ => Err(diag),
                        }),
                    other => Ok(&other == value),
                }
            }
            _ => {
                let pattern_value = self.eval_expr(pattern, env)?;
                Ok(&pattern_value == value)
            }
        }
    }

    fn compose_and(
        &mut self,
        lhs: RuntimeValue,
        rhs: RuntimeValue,
        span: Span,
    ) -> Result<RuntimeValue> {
        match (lhs, rhs) {
            (RuntimeValue::Abstract(mut lhs), RuntimeValue::Abstract(rhs)) => {
                lhs.constraints.extend(rhs.constraints);
                lhs.constraints = normalize_constraints(lhs.constraints, span)?;
                lhs.default = self.merge_default(lhs.default, rhs.default, span)?;
                Ok(RuntimeValue::Abstract(lhs))
            }
            (RuntimeValue::Abstract(abstract_value), concrete @ RuntimeValue::Concrete(_))
            | (concrete @ RuntimeValue::Concrete(_), RuntimeValue::Abstract(abstract_value)) => {
                self.ensure_satisfies(
                    &concrete,
                    &abstract_value.constraints,
                    span,
                    Some((span, "concrete value being composed here")),
                )?;
                Ok(concrete)
            }
            (
                RuntimeValue::Concrete(ConcreteValue::Object(lhs)),
                RuntimeValue::Concrete(ConcreteValue::Object(rhs)),
            ) => self.compose_objects(lhs, rhs, span),
            (RuntimeValue::Concrete(lhs), RuntimeValue::Concrete(rhs)) => {
                if concrete_scalar_eq(&lhs, &rhs) {
                    Ok(RuntimeValue::Concrete(lhs))
                } else {
                    Err(Diagnostic::new(
                        DiagnosticKind::Conflict,
                        span,
                        "concrete values conflict",
                    ))
                }
            }
        }
    }

    fn compose_objects(
        &mut self,
        mut lhs: ObjectValue,
        rhs: ObjectValue,
        span: Span,
    ) -> Result<RuntimeValue> {
        for rhs_field in rhs.fields {
            if let Some(index) = lhs
                .fields
                .iter()
                .position(|lhs_field| lhs_field.name == rhs_field.name)
            {
                let lhs_field_span = lhs.fields[index].span;
                let lhs_value = self.force(lhs.fields[index].value)?;
                let rhs_value = self.force(rhs_field.value)?;
                let value = self
                    .compose_and(lhs_value, rhs_value, span)
                    .map_err(|diag| {
                        diag.with_label(span, format!("while composing field `{}`", rhs_field.name))
                            .with_label(lhs_field_span, format!("left field `{}`", rhs_field.name))
                            .with_label(rhs_field.span, format!("right field `{}`", rhs_field.name))
                    })?;
                lhs.fields[index].value = self.add_value_thunk_with_span(value, rhs_field.span);
            } else {
                lhs.fields.push(rhs_field);
            }
        }
        Ok(RuntimeValue::Concrete(ConcreteValue::Object(lhs)))
    }

    fn patch(&mut self, lhs: RuntimeValue, rhs: RuntimeValue) -> Result<RuntimeValue> {
        match (lhs, rhs) {
            (
                RuntimeValue::Concrete(ConcreteValue::Object(lhs)),
                RuntimeValue::Concrete(ConcreteValue::Object(rhs)),
            ) => self.patch_objects(lhs, rhs),
            (_, rhs) => Ok(rhs),
        }
    }

    fn patch_objects(&mut self, mut lhs: ObjectValue, rhs: ObjectValue) -> Result<RuntimeValue> {
        for rhs_field in rhs.fields {
            if let Some(index) = lhs
                .fields
                .iter()
                .position(|lhs_field| lhs_field.name == rhs_field.name)
            {
                let lhs_value = self.force(lhs.fields[index].value)?;
                let rhs_value = self.force(rhs_field.value)?;
                let value = self.patch(lhs_value, rhs_value)?;
                lhs.fields[index].value = self.add_value_thunk_with_span(value, rhs_field.span);
                lhs.fields[index].span = rhs_field.span;
            } else {
                lhs.fields.push(rhs_field);
            }
        }
        Ok(RuntimeValue::Concrete(ConcreteValue::Object(lhs)))
    }

    fn ensure_satisfies(
        &mut self,
        value: &RuntimeValue,
        constraints: &[ConstraintEntry],
        span: Span,
        value_label: Option<(Span, &'static str)>,
    ) -> Result<()> {
        for constraint in constraints {
            self.satisfies(value, constraint, span, value_label)?;
        }
        Ok(())
    }

    fn satisfies(
        &mut self,
        value: &RuntimeValue,
        constraint: &ConstraintEntry,
        span: Span,
        value_label: Option<(Span, &'static str)>,
    ) -> Result<()> {
        let constraint_value = &constraint.constraint;
        match constraint_value {
            Constraint::Type(primitive) => {
                if value_matches_primitive(value, *primitive) {
                    Ok(())
                } else {
                    Err(self.constraint_violation(
                        constraint,
                        span,
                        value_label,
                        "value does not satisfy primitive type constraint",
                    ))
                }
            }
            Constraint::Compare(op, expected) => compare_value(value, *op, expected)
                .then_some(())
                .ok_or_else(|| {
                    self.constraint_violation(
                        constraint,
                        span,
                        value_label,
                        "value does not satisfy comparison constraint",
                    )
                }),
            Constraint::Regex(pattern) => {
                satisfies_regex(value, pattern, constraint.span).map_err(|diag| {
                    let diag = diag.with_label(constraint.span, "regex constraint declared here");
                    if let Some((span, message)) = value_label {
                        diag.with_label(span, message)
                    } else {
                        diag
                    }
                })
            }
            Constraint::BuiltinPredicate(name) => Err(Diagnostic::new(
                DiagnosticKind::UnsupportedFeature,
                constraint.span,
                format!("builtin predicate `{name}` is not implemented"),
            )
            .with_label(span, "constraint checked during this operation")),
        }
    }

    fn constraint_violation(
        &self,
        constraint: &ConstraintEntry,
        operation_span: Span,
        value_label: Option<(Span, &'static str)>,
        message: &'static str,
    ) -> Diagnostic {
        let mut diagnostic = Diagnostic::new(
            DiagnosticKind::ConstraintViolation,
            constraint.span,
            message,
        )
        .with_label(constraint.span, "constraint declared here")
        .with_label(operation_span, "constraint checked during this operation");
        if let Some((span, message)) = value_label {
            diagnostic = diagnostic.with_label(span, message);
        }
        diagnostic
    }

    fn merge_default(
        &self,
        lhs: Option<ThunkId>,
        rhs: Option<ThunkId>,
        span: Span,
    ) -> Result<Option<ThunkId>> {
        match (lhs, rhs) {
            (None, None) => Ok(None),
            (Some(value), None) | (None, Some(value)) => Ok(Some(value)),
            (Some(lhs), Some(rhs)) if lhs == rhs => Ok(Some(lhs)),
            (Some(lhs), Some(rhs)) => Err(Diagnostic::new(
                DiagnosticKind::DefaultConflict,
                span,
                "conflicting defaults",
            )
            .with_label(self.thunk_span(lhs), "left default value")
            .with_label(self.thunk_span(rhs), "right default value")),
        }
    }

    fn internalize_host_value(&mut self, value: HostValue) -> Result<RuntimeValue> {
        match value {
            HostValue::String(value) => Ok(RuntimeValue::Concrete(ConcreteValue::String(value))),
            HostValue::Int(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Int(value))),
            HostValue::Float(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Float(value))),
            HostValue::Bool(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Bool(value))),
            HostValue::Array(items) => {
                let mut thunks = Vec::new();
                for item in items {
                    let value = self.internalize_host_value(item)?;
                    thunks.push(self.add_value_thunk(value));
                }
                Ok(RuntimeValue::Concrete(ConcreteValue::Array(thunks)))
            }
            HostValue::Object(fields) => {
                let mut object = ObjectValue { fields: Vec::new() };
                for field in fields {
                    if object
                        .fields
                        .iter()
                        .any(|existing| existing.name == field.name)
                    {
                        return Err(Diagnostic::new(
                            DiagnosticKind::Conflict,
                            Span::default(),
                            format!("duplicate host object field `{}`", field.name),
                        ));
                    }
                    let value = self.internalize_host_value(field.value)?;
                    let value = self.add_value_thunk(value);
                    object.fields.push(ObjectField {
                        name: field.name,
                        value,
                        span: Span::default(),
                    });
                }
                Ok(RuntimeValue::Concrete(ConcreteValue::Object(object)))
            }
            HostValue::Abstract {
                constraints,
                default,
            } => {
                let default = if let Some(default) = default {
                    let value = self.internalize_host_value(*default)?;
                    Some(self.add_value_thunk(value))
                } else {
                    None
                };
                let constraints = constraints
                    .into_iter()
                    .map(|constraint| ConstraintEntry {
                        constraint,
                        span: Span::default(),
                    })
                    .collect();
                let constraints = normalize_constraints(constraints, Span::default())?;
                Ok(RuntimeValue::Abstract(AbstractValue {
                    constraints,
                    default,
                }))
            }
        }
    }

    fn force(&mut self, id: ThunkId) -> Result<RuntimeValue> {
        let index = id.0 as usize;
        match self.thunks[index].state.clone() {
            ThunkState::Evaluated(value) => return Ok(value),
            ThunkState::Evaluating => {
                self.thunks[index].state = ThunkState::Error;
                return Err(Diagnostic::new(
                    DiagnosticKind::Cycle,
                    Span::default(),
                    "cyclic thunk dependency",
                ));
            }
            ThunkState::Error => {
                return Err(Diagnostic::new(
                    DiagnosticKind::Cycle,
                    Span::default(),
                    "thunk previously failed",
                ));
            }
            ThunkState::Unevaluated => {}
        }

        self.thunks[index].state = ThunkState::Evaluating;
        let kind = self.thunks[index].kind.clone();
        let result = match kind {
            ThunkKind::Expr { expr, env } => self.eval_expr(expr, env),
            ThunkKind::Constrained {
                constraint,
                constraint_env,
                value,
                value_env,
            } => {
                let span = self.expr_span(value);
                let constraint = self.eval_expr(constraint, constraint_env)?;
                let value = self.eval_expr(value, value_env)?;
                self.compose_and(constraint, value, span)
            }
            ThunkKind::Value(value) => Ok(value),
        };
        match result {
            Ok(value) => {
                self.thunks[index].state = ThunkState::Evaluated(value.clone());
                Ok(value)
            }
            Err(error) => {
                self.thunks[index].state = ThunkState::Error;
                Err(error)
            }
        }
    }

    fn add_expr_thunk(&mut self, expr: ExprRef, env: EnvId) -> ThunkId {
        let span = self.expr_span(expr);
        self.add_expr_thunk_with_span(expr, env, span)
    }

    fn add_expr_thunk_with_span(&mut self, expr: ExprRef, env: EnvId, span: Span) -> ThunkId {
        self.add_thunk(ThunkKind::Expr { expr, env }, span)
    }

    fn add_constrained_thunk(
        &mut self,
        constraint: ExprRef,
        constraint_env: EnvId,
        value: ExprRef,
        value_env: EnvId,
    ) -> ThunkId {
        let span = self.expr_span(value);
        self.add_thunk(
            ThunkKind::Constrained {
                constraint,
                constraint_env,
                value,
                value_env,
            },
            span,
        )
    }

    fn add_value_thunk(&mut self, value: RuntimeValue) -> ThunkId {
        self.add_value_thunk_with_span(value, Span::default())
    }

    fn add_value_thunk_with_span(&mut self, value: RuntimeValue, span: Span) -> ThunkId {
        self.add_thunk(ThunkKind::Value(value), span)
    }

    fn add_thunk(&mut self, kind: ThunkKind, span: Span) -> ThunkId {
        let id = ThunkId(self.thunks.len() as u32);
        self.thunks.push(Thunk {
            kind,
            state: ThunkState::Unevaluated,
            span,
        });
        id
    }

    fn new_env(&mut self, parent: Option<EnvId>) -> EnvId {
        let id = EnvId(self.envs.len() as u32);
        self.envs.push(Env {
            parent,
            bindings: Vec::new(),
        });
        id
    }

    fn bind(&mut self, env: EnvId, name: String, value: ThunkId) {
        self.envs[env.0 as usize]
            .bindings
            .push(Binding { name, value });
    }

    fn lookup(&self, env: EnvId, name: &str) -> Option<ThunkId> {
        let mut current = Some(env);
        while let Some(env) = current {
            let frame = &self.envs[env.0 as usize];
            if let Some(binding) = frame
                .bindings
                .iter()
                .rev()
                .find(|binding| binding.name == name)
            {
                return Some(binding.value);
            }
            current = frame.parent;
        }
        None
    }

    fn expr(&self, reference: ExprRef) -> &Expr {
        &self.modules[reference.module.0 as usize]
            .ast
            .get(reference.expr)
            .expr
    }

    fn expr_span(&self, reference: ExprRef) -> Span {
        self.modules
            .get(reference.module.0 as usize)
            .map(|module| module.ast.span(reference.expr))
            .unwrap_or_default()
    }

    fn thunk_span(&self, thunk: ThunkId) -> Span {
        self.thunks[thunk.0 as usize].span
    }

    fn with_path_context(
        &self,
        diagnostic: Diagnostic,
        phase: &'static str,
        path: &[String],
    ) -> Diagnostic {
        if path.is_empty() {
            diagnostic
        } else {
            let span = diagnostic.span;
            diagnostic.with_label(span, format!("while {phase} `{}`", path.join(".")))
        }
    }
}

fn field_name(field: &Field) -> Result<String> {
    if field.path.len() == 1 {
        Ok(field.path[0].clone())
    } else {
        Err(Diagnostic::new(
            DiagnosticKind::UnsupportedFeature,
            field.span,
            "nested let binding names are not supported",
        ))
    }
}

fn primitive_type(name: &str) -> Option<PrimitiveType> {
    match name {
        "String" => Some(PrimitiveType::String),
        "Int" => Some(PrimitiveType::Int),
        "Float" => Some(PrimitiveType::Float),
        "Bool" => Some(PrimitiveType::Bool),
        "Array" => Some(PrimitiveType::Array),
        _ => None,
    }
}

pub fn format_diagnostic_with<'a>(
    diagnostic: &Diagnostic,
    mut source_name: impl FnMut(SourceId) -> Option<&'a str>,
) -> String {
    let mut out = format!(
        "{:?} at {}:{}..{}: {}",
        diagnostic.kind,
        format_source(diagnostic.span.source, &mut source_name),
        diagnostic.span.start,
        diagnostic.span.end,
        diagnostic.message,
    );
    for label in &diagnostic.labels {
        out.push_str(&format!(
            "\nnote at {}:{}..{}: {}",
            format_source(label.span.source, &mut source_name),
            label.span.start,
            label.span.end,
            label.message,
        ));
    }
    out
}

fn format_source<'a>(
    source: SourceId,
    source_name: &mut impl FnMut(SourceId) -> Option<&'a str>,
) -> String {
    source_name(source)
        .map(String::from)
        .unwrap_or_else(|| format!("source {}", source.0))
}

fn constraint_primary_span(constraints: &[ConstraintEntry]) -> Span {
    constraints
        .first()
        .map(|constraint| constraint.span)
        .unwrap_or_default()
}

fn literal_to_concrete(literal: Literal) -> ConcreteValue {
    match literal {
        Literal::String(value) => ConcreteValue::String(value),
        Literal::Int(value) => ConcreteValue::Int(value),
        Literal::Float(value) => ConcreteValue::Float(value),
        Literal::Bool(value) => ConcreteValue::Bool(value),
    }
}

fn literal_value_from_runtime(value: &RuntimeValue) -> Option<LiteralValue> {
    match value {
        RuntimeValue::Concrete(ConcreteValue::String(value)) => {
            Some(LiteralValue::String(value.clone()))
        }
        RuntimeValue::Concrete(ConcreteValue::Int(value)) => Some(LiteralValue::Int(*value)),
        RuntimeValue::Concrete(ConcreteValue::Float(value)) => Some(LiteralValue::Float(*value)),
        RuntimeValue::Concrete(ConcreteValue::Bool(value)) => Some(LiteralValue::Bool(*value)),
        _ => None,
    }
}

fn value_matches_primitive(value: &RuntimeValue, primitive: PrimitiveType) -> bool {
    matches!(
        (value, primitive),
        (
            RuntimeValue::Concrete(ConcreteValue::String(_)),
            PrimitiveType::String
        ) | (
            RuntimeValue::Concrete(ConcreteValue::Int(_)),
            PrimitiveType::Int
        ) | (
            RuntimeValue::Concrete(ConcreteValue::Float(_)),
            PrimitiveType::Float
        ) | (
            RuntimeValue::Concrete(ConcreteValue::Bool(_)),
            PrimitiveType::Bool
        ) | (
            RuntimeValue::Concrete(ConcreteValue::Array(_)),
            PrimitiveType::Array
        )
    )
}

#[cfg(feature = "regex")]
fn satisfies_regex(value: &RuntimeValue, pattern: &str, span: Span) -> Result<()> {
    let RuntimeValue::Concrete(ConcreteValue::String(value)) = value else {
        return Err(Diagnostic::new(
            DiagnosticKind::ConstraintViolation,
            span,
            "regex constraints require a string value",
        ));
    };
    let regex = regex::Regex::new(pattern).map_err(|error| {
        Diagnostic::new(
            DiagnosticKind::Syntax,
            span,
            format!("invalid regex constraint `{pattern}`: {error}"),
        )
    })?;
    if regex.is_match(value) {
        Ok(())
    } else {
        Err(Diagnostic::new(
            DiagnosticKind::ConstraintViolation,
            span,
            "value does not satisfy regex constraint",
        ))
    }
}

#[cfg(not(feature = "regex"))]
fn satisfies_regex(_value: &RuntimeValue, _pattern: &str, span: Span) -> Result<()> {
    Err(Diagnostic::new(
        DiagnosticKind::UnsupportedFeature,
        span,
        "regex constraints require the regex feature",
    ))
}

#[derive(Debug, Clone, Copy)]
enum ArithmeticOp {
    Add,
    Sub,
    Mul,
    Div,
}

#[derive(Debug, Clone, Copy)]
enum CompareExprOp {
    Equal,
    NotEqual,
    Greater,
    GreaterEqual,
    Less,
    LessEqual,
}

#[derive(Debug, Clone, Copy)]
enum Number {
    Int(i64),
    Float(f64),
}

fn negate_number(value: RuntimeValue, span: Span) -> Result<RuntimeValue> {
    match value {
        RuntimeValue::Concrete(ConcreteValue::Int(value)) => value
            .checked_neg()
            .map(|value| RuntimeValue::Concrete(ConcreteValue::Int(value)))
            .ok_or_else(|| arithmetic_error(span, "integer negation overflow")),
        RuntimeValue::Concrete(ConcreteValue::Float(value)) => {
            Ok(RuntimeValue::Concrete(ConcreteValue::Float(-value)))
        }
        _ => Err(Diagnostic::new(
            DiagnosticKind::TypeMismatch,
            span,
            "unary '-' expects a numeric value",
        )),
    }
}

fn negate_bool(value: RuntimeValue, span: Span) -> Result<RuntimeValue> {
    Ok(RuntimeValue::Concrete(ConcreteValue::Bool(
        !bool_from_runtime(value, span)?,
    )))
}

fn bool_from_runtime(value: RuntimeValue, span: Span) -> Result<bool> {
    match value {
        RuntimeValue::Concrete(ConcreteValue::Bool(value)) => Ok(value),
        _ => Err(Diagnostic::new(
            DiagnosticKind::TypeMismatch,
            span,
            "logical operators expect boolean values",
        )),
    }
}

fn compare_expr(
    lhs: RuntimeValue,
    rhs: RuntimeValue,
    op: CompareExprOp,
    span: Span,
) -> Result<RuntimeValue> {
    let result = match op {
        CompareExprOp::Equal => scalar_equal(&lhs, &rhs, span)?,
        CompareExprOp::NotEqual => !scalar_equal(&lhs, &rhs, span)?,
        CompareExprOp::Greater => compare_numbers(lhs, rhs, |lhs, rhs| lhs > rhs, span)?,
        CompareExprOp::GreaterEqual => compare_numbers(lhs, rhs, |lhs, rhs| lhs >= rhs, span)?,
        CompareExprOp::Less => compare_numbers(lhs, rhs, |lhs, rhs| lhs < rhs, span)?,
        CompareExprOp::LessEqual => compare_numbers(lhs, rhs, |lhs, rhs| lhs <= rhs, span)?,
    };
    Ok(RuntimeValue::Concrete(ConcreteValue::Bool(result)))
}

fn scalar_equal(lhs: &RuntimeValue, rhs: &RuntimeValue, span: Span) -> Result<bool> {
    match (lhs, rhs) {
        (
            RuntimeValue::Concrete(ConcreteValue::String(lhs)),
            RuntimeValue::Concrete(ConcreteValue::String(rhs)),
        ) => Ok(lhs == rhs),
        (
            RuntimeValue::Concrete(ConcreteValue::Bool(lhs)),
            RuntimeValue::Concrete(ConcreteValue::Bool(rhs)),
        ) => Ok(lhs == rhs),
        (
            RuntimeValue::Concrete(ConcreteValue::Int(lhs)),
            RuntimeValue::Concrete(ConcreteValue::Int(rhs)),
        ) => Ok(lhs == rhs),
        (
            RuntimeValue::Concrete(ConcreteValue::Float(lhs)),
            RuntimeValue::Concrete(ConcreteValue::Float(rhs)),
        ) => Ok(lhs == rhs),
        (
            RuntimeValue::Concrete(ConcreteValue::Int(lhs)),
            RuntimeValue::Concrete(ConcreteValue::Float(rhs)),
        ) => Ok(*lhs as f64 == *rhs),
        (
            RuntimeValue::Concrete(ConcreteValue::Float(lhs)),
            RuntimeValue::Concrete(ConcreteValue::Int(rhs)),
        ) => Ok(*lhs == *rhs as f64),
        _ => Err(Diagnostic::new(
            DiagnosticKind::TypeMismatch,
            span,
            "equality operators expect comparable scalar values",
        )),
    }
}

fn compare_numbers(
    lhs: RuntimeValue,
    rhs: RuntimeValue,
    compare: impl FnOnce(f64, f64) -> bool,
    span: Span,
) -> Result<bool> {
    let lhs = number_from_runtime(lhs).ok_or_else(|| comparison_type_error(span))?;
    let rhs = number_from_runtime(rhs).ok_or_else(|| comparison_type_error(span))?;
    Ok(compare(number_to_f64(lhs), number_to_f64(rhs)))
}

fn comparison_type_error(span: Span) -> Diagnostic {
    Diagnostic::new(
        DiagnosticKind::TypeMismatch,
        span,
        "ordering operators expect numeric values",
    )
}

fn concat_arrays(lhs: RuntimeValue, rhs: RuntimeValue, span: Span) -> Result<RuntimeValue> {
    match (lhs, rhs) {
        (
            RuntimeValue::Concrete(ConcreteValue::Array(mut lhs)),
            RuntimeValue::Concrete(ConcreteValue::Array(rhs)),
        ) => {
            lhs.extend(rhs);
            Ok(RuntimeValue::Concrete(ConcreteValue::Array(lhs)))
        }
        _ => Err(Diagnostic::new(
            DiagnosticKind::TypeMismatch,
            span,
            "'++' expects array values",
        )),
    }
}

fn arithmetic(
    lhs: RuntimeValue,
    rhs: RuntimeValue,
    op: ArithmeticOp,
    span: Span,
) -> Result<RuntimeValue> {
    let lhs = number_from_runtime(lhs).ok_or_else(|| arithmetic_type_error(span))?;
    let rhs = number_from_runtime(rhs).ok_or_else(|| arithmetic_type_error(span))?;
    match (lhs, rhs) {
        (Number::Int(lhs), Number::Int(rhs)) => arithmetic_int(lhs, rhs, op, span),
        (lhs, rhs) => arithmetic_float(number_to_f64(lhs), number_to_f64(rhs), op, span),
    }
}

fn arithmetic_int(lhs: i64, rhs: i64, op: ArithmeticOp, span: Span) -> Result<RuntimeValue> {
    match op {
        ArithmeticOp::Add => lhs
            .checked_add(rhs)
            .map(|value| RuntimeValue::Concrete(ConcreteValue::Int(value)))
            .ok_or_else(|| arithmetic_error(span, "integer addition overflow")),
        ArithmeticOp::Sub => lhs
            .checked_sub(rhs)
            .map(|value| RuntimeValue::Concrete(ConcreteValue::Int(value)))
            .ok_or_else(|| arithmetic_error(span, "integer subtraction overflow")),
        ArithmeticOp::Mul => lhs
            .checked_mul(rhs)
            .map(|value| RuntimeValue::Concrete(ConcreteValue::Int(value)))
            .ok_or_else(|| arithmetic_error(span, "integer multiplication overflow")),
        ArithmeticOp::Div => {
            if rhs == 0 {
                return Err(arithmetic_error(span, "division by zero"));
            }
            Ok(RuntimeValue::Concrete(ConcreteValue::Float(
                lhs as f64 / rhs as f64,
            )))
        }
    }
}

fn arithmetic_float(lhs: f64, rhs: f64, op: ArithmeticOp, span: Span) -> Result<RuntimeValue> {
    if matches!(op, ArithmeticOp::Div) && rhs == 0.0 {
        return Err(arithmetic_error(span, "division by zero"));
    }
    let value = match op {
        ArithmeticOp::Add => lhs + rhs,
        ArithmeticOp::Sub => lhs - rhs,
        ArithmeticOp::Mul => lhs * rhs,
        ArithmeticOp::Div => lhs / rhs,
    };
    Ok(RuntimeValue::Concrete(ConcreteValue::Float(value)))
}

fn number_from_runtime(value: RuntimeValue) -> Option<Number> {
    match value {
        RuntimeValue::Concrete(ConcreteValue::Int(value)) => Some(Number::Int(value)),
        RuntimeValue::Concrete(ConcreteValue::Float(value)) => Some(Number::Float(value)),
        _ => None,
    }
}

fn number_to_f64(value: Number) -> f64 {
    match value {
        Number::Int(value) => value as f64,
        Number::Float(value) => value,
    }
}

fn arithmetic_type_error(span: Span) -> Diagnostic {
    Diagnostic::new(
        DiagnosticKind::TypeMismatch,
        span,
        "arithmetic operators expect numeric values",
    )
}

fn arithmetic_error(span: Span, message: &'static str) -> Diagnostic {
    Diagnostic::new(DiagnosticKind::Conflict, span, message)
}

fn compare_value(value: &RuntimeValue, op: CompareOp, expected: &LiteralValue) -> bool {
    match (value, expected) {
        (RuntimeValue::Concrete(ConcreteValue::Int(actual)), LiteralValue::Int(expected)) => {
            compare_f64(*actual as f64, op, *expected as f64)
        }
        (RuntimeValue::Concrete(ConcreteValue::Float(actual)), LiteralValue::Float(expected)) => {
            compare_f64(*actual, op, *expected)
        }
        (RuntimeValue::Concrete(ConcreteValue::Int(actual)), LiteralValue::Float(expected)) => {
            compare_f64(*actual as f64, op, *expected)
        }
        (RuntimeValue::Concrete(ConcreteValue::Float(actual)), LiteralValue::Int(expected)) => {
            compare_f64(*actual, op, *expected as f64)
        }
        _ => false,
    }
}

fn compare_f64(actual: f64, op: CompareOp, expected: f64) -> bool {
    match op {
        CompareOp::Gt => actual > expected,
        CompareOp::Gte => actual >= expected,
        CompareOp::Lt => actual < expected,
        CompareOp::Lte => actual <= expected,
        CompareOp::Eq => actual == expected,
    }
}

fn concrete_scalar_eq(lhs: &ConcreteValue, rhs: &ConcreteValue) -> bool {
    match (lhs, rhs) {
        (ConcreteValue::String(lhs), ConcreteValue::String(rhs)) => lhs == rhs,
        (ConcreteValue::Int(lhs), ConcreteValue::Int(rhs)) => lhs == rhs,
        (ConcreteValue::Float(lhs), ConcreteValue::Float(rhs)) => lhs == rhs,
        (ConcreteValue::Bool(lhs), ConcreteValue::Bool(rhs)) => lhs == rhs,
        _ => false,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{LoadedSource, parse_source};

    fn eval_data(source: &str) -> Data {
        let parsed = parse_source(source).unwrap();
        let mut engine = Engine::from_parse(parsed.ast, parsed.root);
        let value = engine.eval_root().unwrap();
        engine.materialize(&value).unwrap()
    }

    #[test]
    fn constraint_violation_reports_related_spans() {
        let parsed = parse_source("port = Int & > 443 default 80;").unwrap();
        let mut engine = Engine::from_parse(parsed.ast, parsed.root);
        let value = engine.eval_root().unwrap();
        let error = engine.materialize(&value).unwrap_err();
        assert_eq!(error.kind, DiagnosticKind::ConstraintViolation);
        assert!(
            error
                .labels
                .iter()
                .any(|label| label.message.contains("constraint declared here"))
        );
        assert!(
            error
                .labels
                .iter()
                .any(|label| label.message.contains("default value checked here"))
        );
    }

    #[test]
    fn object_composition_conflict_reports_both_fields() {
        let parsed = parse_source("{ port = 80; } & { port = 443; }").unwrap();
        let mut engine = Engine::from_parse(parsed.ast, parsed.root);
        let error = engine.eval_root().unwrap_err();
        assert_eq!(error.kind, DiagnosticKind::Conflict);
        assert!(
            error
                .labels
                .iter()
                .any(|label| label.message == "left field `port`")
        );
        assert!(
            error
                .labels
                .iter()
                .any(|label| label.message == "right field `port`")
        );
    }

    #[test]
    fn materialize_error_reports_field_path() {
        let parsed = parse_source("service.port = Int;").unwrap();
        let mut engine = Engine::from_parse(parsed.ast, parsed.root);
        let value = engine.eval_root().unwrap();
        let error = engine.materialize(&value).unwrap_err();
        assert_eq!(error.kind, DiagnosticKind::Materialize);
        assert!(
            error
                .labels
                .iter()
                .any(|label| label.message.contains("while materializing `service.port`"))
        );
    }

    #[test]
    fn materializes_default() {
        let data = eval_data("port = Int & >= 1 default 8080;");
        assert!(matches!(data, Data::Object(_)));
    }

    #[test]
    fn evaluates_arithmetic_expressions() {
        let data = eval_data(
            r#"
            {
                sum = 1 + 2 * 3;
                diff = 10 - 4;
                product = (2 + 3) * 4;
                quotient = 5 / 2;
                negative = -3 + 1;
            }
            "#,
        );
        let Data::Object(fields) = data else { panic!() };
        assert_eq!(fields[0].value, Data::Int(7));
        assert_eq!(fields[1].value, Data::Int(6));
        assert_eq!(fields[2].value, Data::Int(20));
        assert_eq!(fields[3].value, Data::Float(2.5));
        assert_eq!(fields[4].value, Data::Int(-2));
    }

    #[test]
    fn arithmetic_can_feed_constraints() {
        let data = eval_data("port = Int & > 4000 + 42 default 8080;");
        let Data::Object(fields) = data else { panic!() };
        assert_eq!(fields[0].value, Data::Int(8080));
    }

    #[test]
    fn rejects_division_by_zero() {
        let parsed = parse_source("1 / 0").unwrap();
        let mut engine = Engine::from_parse(parsed.ast, parsed.root);
        assert!(engine.eval_root().is_err());
    }

    #[test]
    fn evaluates_array_concat() {
        let data = eval_data(
            r#"
            [1, 2] ++ [3, 4]
            "#,
        );
        assert_eq!(
            data,
            Data::Array(vec![Data::Int(1), Data::Int(2), Data::Int(3), Data::Int(4)])
        );
    }

    #[test]
    fn array_concat_has_lower_precedence_than_arithmetic() {
        let data = eval_data("[1 + 1] ++ [2 * 2]");
        assert_eq!(data, Data::Array(vec![Data::Int(2), Data::Int(4)]));
    }

    #[test]
    fn rejects_invalid_array_concat_operands() {
        let parsed = parse_source("[1] ++ 2").unwrap();
        let mut engine = Engine::from_parse(parsed.ast, parsed.root);
        assert!(engine.eval_root().is_err());
    }

    #[test]
    fn evaluates_logical_and_comparison_expressions() {
        let data = eval_data(
            r#"
            {
                enabled = true && !false;
                fallback = false || true;
                compare = 9000 + 443 > 9442;
                equality = 1 == 1.0;
                inequality = "prod" != "dev";
            }
            "#,
        );
        let Data::Object(fields) = data else { panic!() };
        assert_eq!(fields[0].value, Data::Bool(true));
        assert_eq!(fields[1].value, Data::Bool(true));
        assert_eq!(fields[2].value, Data::Bool(true));
        assert_eq!(fields[3].value, Data::Bool(true));
        assert_eq!(fields[4].value, Data::Bool(true));
    }

    #[test]
    fn logical_operators_short_circuit() {
        let false_and_missing = eval_data("false && missing_identifier");
        assert_eq!(false_and_missing, Data::Bool(false));

        let true_or_missing = eval_data("true || missing_identifier");
        assert_eq!(true_or_missing, Data::Bool(true));
    }

    #[test]
    fn rejects_invalid_logical_operands() {
        let parsed = parse_source("true && 1").unwrap();
        let mut engine = Engine::from_parse(parsed.ast, parsed.root);
        assert!(engine.eval_root().is_err());
    }

    #[test]
    fn rejects_invalid_comparison_operands() {
        let parsed = parse_source("\"a\" < \"b\"").unwrap();
        let mut engine = Engine::from_parse(parsed.ast, parsed.root);
        assert!(engine.eval_root().is_err());
    }

    #[test]
    fn composes_schema_and_value() {
        let data = eval_data(
            r#"
            let
                MyConfig = { port = Int & > 443 default 8080; };
            in
                MyConfig & { port = 8000; }
            "#,
        );
        let Data::Object(fields) = data else { panic!() };
        assert_eq!(fields[0].name, "port");
        assert_eq!(fields[0].value, Data::Int(8000));
    }

    #[test]
    fn detects_constraint_violation() {
        let parsed = parse_source("{ port = Int & > 443; } & { port = 80; }").unwrap();
        let mut engine = Engine::from_parse(parsed.ast, parsed.root);
        assert!(engine.eval_root().is_err());
    }

    #[derive(Default)]
    struct MapLoader {
        sources: Vec<(String, String)>,
    }

    impl SourceLoader for MapLoader {
        fn load(&mut self, _current_key: Option<&str>, specifier: &str) -> Result<LoadedSource> {
            let source = self
                .sources
                .iter()
                .find(|(key, _)| key == specifier)
                .map(|(_, source)| source.clone())
                .ok_or_else(|| {
                    Diagnostic::new(DiagnosticKind::Import, Span::default(), "missing source")
                })?;
            Ok(LoadedSource {
                key: specifier.into(),
                name: specifier.into(),
                source,
            })
        }
    }

    #[test]
    fn imports_module_on_demand() {
        let mut engine = Engine::new(MapLoader {
            sources: vec![(
                String::from("dep"),
                String::from("schema = { port = Int default 8080; }"),
            )],
        });
        let module = engine
            .add_root_source(
                "main",
                "main",
                r#"(import "dep").schema & { port = 9000; }"#,
            )
            .unwrap();
        let value = engine.eval_module(module).unwrap();
        let data = engine.materialize(&value).unwrap();
        let Data::Object(fields) = data else { panic!() };
        assert_eq!(fields[0].value, Data::Int(9000));
    }

    #[test]
    fn top_level_fields_are_recursive_module_scope() {
        let mut engine = Engine::new(EmptyLoader);
        let module = engine
            .add_root_source(
                "main",
                "main",
                "schema = { port = Int default 8080; }; result = schema;",
            )
            .unwrap();
        let value = engine.eval_module(module).unwrap();
        let data = engine.materialize(&value).unwrap();
        let Data::Object(fields) = data else { panic!() };
        assert_eq!(fields[1].name, "result");
        assert!(matches!(fields[1].value, Data::Object(_)));
    }

    #[test]
    fn host_prelude_provides_abstract_object() {
        let mut engine = Engine::new(EmptyLoader);
        engine
            .bind_global(
                "Service",
                HostValue::object([
                    ("name", HostValue::string_type()),
                    (
                        "port",
                        HostValue::int_type().gt(443).default_int(8443).unwrap(),
                    ),
                    (
                        "enabled",
                        HostValue::bool_type().default_bool(true).unwrap(),
                    ),
                ]),
            )
            .unwrap();
        let module = engine
            .add_root_source(
                "main",
                "main",
                r#"Service & { name = "api"; port = 9443; }"#,
            )
            .unwrap();
        let value = engine.eval_module(module).unwrap();
        let data = engine.materialize(&value).unwrap();
        let Data::Object(fields) = data else { panic!() };
        assert_eq!(fields[0].value, Data::String(String::from("api")));
        assert_eq!(fields[1].value, Data::Int(9443));
        assert_eq!(fields[2].value, Data::Bool(true));
    }

    #[test]
    fn module_binding_shadows_host_prelude() {
        let mut engine = Engine::new(EmptyLoader);
        engine.bind_global("Service", HostValue::int(1)).unwrap();
        let module = engine
            .add_root_source("main", "main", r#"Service = "local"; result = Service;"#)
            .unwrap();
        let value = engine.eval_module(module).unwrap();
        let data = engine.materialize(&value).unwrap();
        let Data::Object(fields) = data else { panic!() };
        assert_eq!(fields[1].value, Data::String(String::from("local")));
    }

    #[cfg(feature = "regex")]
    #[test]
    fn regex_constraint_matches_concrete_string() {
        let data = eval_data(r#"value = String & /^api-[0-9]+$/ default "api-123";"#);
        let Data::Object(fields) = data else { panic!() };
        assert_eq!(fields[0].value, Data::String(String::from("api-123")));
    }

    #[cfg(feature = "regex")]
    #[test]
    fn regex_constraint_rejects_non_matching_string() {
        let parsed =
            crate::parse_source(r#"value = String & /^api-[0-9]+$/ default "web-123";"#).unwrap();
        let mut engine = Engine::from_parse(parsed.ast, parsed.root);
        let value = engine.eval_root().unwrap();
        assert!(engine.materialize(&value).is_err());
    }
}
