///|
/// A source location used by parser and validation diagnostics.
pub(all) struct Span {
  offset : Int
  line : Int
  column : Int
} derive(Debug, Eq)

///|
/// A human-readable, structured error. FlipSpec returns diagnostics instead of
/// aborting so callers can render them in a CLI, editor, or web application.
pub(all) struct Diagnostic {
  code : String
  message : String
  span : Span
} derive(Debug, Eq)

///|
pub fn Diagnostic::render(self : Diagnostic) -> String {
  "\{self.code} at \{self.span.line}:\{self.span.column}: \{self.message}"
}

///|
/// Scalar values accepted by the FlipSpec DSL.
pub(all) enum Value {
  Integer(Int)
  Boolean(Bool)
  Text(String)
} derive(Debug, Eq)

///|
pub fn Value::render(self : Value) -> String {
  match self {
    Integer(value) => value.to_string()
    Boolean(true) => "true"
    Boolean(false) => "false"
    Text(value) => "\"\{escape_text(value)}\""
  }
}

///|
pub fn Value::plain(self : Value) -> String {
  match self {
    Integer(value) => value.to_string()
    Boolean(true) => "true"
    Boolean(false) => "false"
    Text(value) => value
  }
}

///|
pub fn Value::kind(self : Value) -> String {
  match self {
    Integer(_) => "integer"
    Boolean(_) => "boolean"
    Text(_) => "text"
  }
}

///|
fn escape_text(value : String) -> String {
  let out = StringBuilder::new()
  for ch in value {
    match ch {
      '\\' => out.write_string("\\\\")
      '"' => out.write_string("\\\"")
      '\n' => out.write_string("\\n")
      '\r' => out.write_string("\\r")
      '\t' => out.write_string("\\t")
      _ => out.write_char(ch)
    }
  }
  out.to_string()
}

///|
/// Kleene three-valued truth. Unknown is deliberately preserved: a missing
/// input must not silently become false in policy evaluation.
pub(all) enum Truth {
  Yes
  No
  Unknown
} derive(Debug, Eq)

///|
pub fn Truth::render(self : Truth) -> String {
  match self {
    Yes => "true"
    No => "false"
    Unknown => "unknown"
  }
}

///|
pub fn Truth::not(self : Truth) -> Truth {
  match self {
    Yes => No
    No => Yes
    Unknown => Unknown
  }
}

///|
pub fn Truth::conjunction(self : Truth, other : Truth) -> Truth {
  match (self, other) {
    (No, _) | (_, No) => No
    (Yes, Yes) => Yes
    _ => Unknown
  }
}

///|
pub fn Truth::disjunction(self : Truth, other : Truth) -> Truth {
  match (self, other) {
    (Yes, _) | (_, Yes) => Yes
    (No, No) => No
    _ => Unknown
  }
}

///|
pub(all) enum CompareOp {
  Equal
  NotEqual
  Less
  LessEqual
  Greater
  GreaterEqual
} derive(Debug, Eq)

///|
pub fn CompareOp::render(self : CompareOp) -> String {
  match self {
    Equal => "=="
    NotEqual => "!="
    Less => "<"
    LessEqual => "<="
    Greater => ">"
    GreaterEqual => ">="
  }
}

///|
/// Boolean expression evaluated against a set of named feature values.
pub(all) enum Expr {
  Constant(Truth)
  Compare(String, CompareOp, Value)
  Present(String)
  Not(Expr)
  And(Expr, Expr)
  Or(Expr, Expr)
} derive(Debug, Eq)

///|
pub fn Expr::render(self : Expr) -> String {
  match self {
    Constant(value) => value.render()
    Compare(name, op, value) => "\{name} \{op.render()} \{value.render()}"
    Present(name) => "present(\{name})"
    Not(inner) => "not (\{inner.render()})"
    And(left, right) => "(\{left.render()}) and (\{right.render()})"
    Or(left, right) => "(\{left.render()}) or (\{right.render()})"
  }
}

///|
/// A feature includes its current value, its feasible domain, and an integer
/// edit cost. Locked features participate in evaluation but never in repairs.
pub(all) struct Feature {
  name : String
  current : Value
  domain : Array[Value]
  cost : Int
  locked : Bool
} derive(Debug, Eq)

///|
pub fn feature(
  name : String,
  current : Value,
  domain : Array[Value],
  cost? : Int = 1,
  locked? : Bool = false,
) -> Feature {
  { name, current, domain, cost, locked }
}

///|
pub(all) struct Policy {
  name : String
  features : Array[Feature]
  goal : Expr
} derive(Debug, Eq)

///|
pub fn policy(name : String, features : Array[Feature], goal : Expr) -> Policy {
  { name, features, goal }
}

///|
/// A lightweight ordered environment. Arrays keep reports deterministic across
/// compilation targets and are sufficient for the small policy models FlipSpec
/// is designed to explain.
pub(all) struct Binding {
  name : String
  value : Value
} derive(Debug, Eq)

///|
pub type Environment = Array[Binding]

///|
pub fn Policy::environment(self : Policy) -> Environment {
  self.features.map(feature => Binding::{
    name: feature.name,
    value: feature.current,
  })
}

///|
pub fn environment_get(environment : Environment, name : String) -> Value? {
  for binding in environment {
    if binding.name == name {
      return Some(binding.value)
    }
  }
  None
}

///|
pub fn environment_set(
  environment : Environment,
  name : String,
  value : Value,
) -> Environment {
  let result : Environment = []
  let mut found = false
  for binding in environment {
    if binding.name == name {
      result.push({ name, value })
      found = true
    } else {
      result.push(binding)
    }
  }
  if !found {
    result.push({ name, value })
  }
  result
}

///|
pub fn Policy::find_feature(self : Policy, name : String) -> Feature? {
  for item in self.features {
    if item.name == name {
      return Some(item)
    }
  }
  None
}

///|
fn same_value_kind(left : Value, right : Value) -> Bool {
  match (left, right) {
    (Integer(_), Integer(_)) => true
    (Boolean(_), Boolean(_)) => true
    (Text(_), Text(_)) => true
    _ => false
  }
}

///|
/// Checks structural invariants before evaluation or planning.
pub fn Policy::validate(self : Policy) -> Array[Diagnostic] {
  let errors : Array[Diagnostic] = []
  let zero = Span::{ offset: 0, line: 1, column: 1 }
  if self.name.is_empty() {
    errors.push({
      code: "E_MODEL_NAME",
      message: "spec name must not be empty",
      span: zero,
    })
  }
  if self.features.is_empty() {
    errors.push({
      code: "E_MODEL_EMPTY",
      message: "at least one feature is required",
      span: zero,
    })
  }
  for index, item in self.features {
    if item.name.is_empty() {
      errors.push({
        code: "E_FEATURE_NAME",
        message: "feature #\{index + 1} has an empty name",
        span: zero,
      })
    }
    for previous in 0.. Unit {
  match expression {
    Constant(_) => ()
    Present(_) => ()
    Compare(name, op, expected) =>
      match model.find_feature(name) {
        None =>
          errors.push({
            code: "E_UNKNOWN_FEATURE",
            message: "goal references unknown feature '\{name}'",
            span,
          })
        Some(item) => {
          if !same_value_kind(item.current, expected) {
            errors.push({
              code: "E_TYPE_MISMATCH",
              message: "feature '\{name}' is \{item.current.kind()} but goal compares it with \{expected.kind()}",
              span,
            })
          }
          match (op, expected) {
            (Less | LessEqual | Greater | GreaterEqual, Boolean(_)) =>
              errors.push({
                code: "E_BOOLEAN_ORDER",
                message: "boolean feature '\{name}' only supports == and !=",
                span,
              })
            _ => ()
          }
        }
      }
    Not(inner) => collect_expression_errors(inner, model, errors, span)
    And(left, right) | Or(left, right) => {
      collect_expression_errors(left, model, errors, span)
      collect_expression_errors(right, model, errors, span)
    }
  }
}