// ============================================================
// Blackboard schema and validation
//
// Real integrations usually have a contract for the keys that a tree reads.
// Schema keeps that contract executable: missing context and type mismatches
// become explicit diagnostics instead of silent fallback behavior.
// ============================================================

///|
/// The four value categories supported by Blackboard.
pub(all) enum ValueKind {
  BoolKind
  IntKind
  DoubleKind
  StringKind
} derive(Eq)

///|
/// Describe one required or optional blackboard field.
pub struct FieldSpec {
  key : String
  kind : ValueKind
  required : Bool
  default_value : Value?
}

///|
/// Create a required field specification.
pub fn FieldSpec::required(key : String, kind : ValueKind) -> FieldSpec {
  { key, kind, required: true, default_value: None }
}

///|
/// Create an optional field specification.
pub fn FieldSpec::optional(key : String, kind : ValueKind) -> FieldSpec {
  { key, kind, required: false, default_value: None }
}

///|
/// Create a field with a value to install when absent.
pub fn FieldSpec::with_default(
  key : String,
  kind : ValueKind,
  default_value : Value,
) -> FieldSpec {
  { key, kind, required: false, default_value: Some(default_value) }
}

///|
/// Return a stable name for a value category.
pub fn ValueKind::to_string(self : ValueKind) -> String {
  match self {
    ValueKind::BoolKind => "bool"
    ValueKind::IntKind => "int"
    ValueKind::DoubleKind => "double"
    ValueKind::StringKind => "string"
  }
}

///|
/// Return the category of a concrete value.
pub fn Value::kind(self : Value) -> ValueKind {
  match self {
    Value::Bool(_) => ValueKind::BoolKind
    Value::Int(_) => ValueKind::IntKind
    Value::Double(_) => ValueKind::DoubleKind
    Value::Str(_) => ValueKind::StringKind
  }
}

///|
/// A single schema validation finding.
pub enum ValidationIssue {
  MissingField(String)
  WrongType(String, ValueKind, ValueKind)
} derive(Eq)

///|
/// Format a validation finding for logs and test reports.
pub fn ValidationIssue::to_string(self : ValidationIssue) -> String {
  match self {
    ValidationIssue::MissingField(key) => "missing field: \{key}"
    ValidationIssue::WrongType(key, expected, actual) =>
      "wrong type for \{key}: expected \{expected.to_string()}, got \{actual.to_string()}"
  }
}

///|
/// A collection of field contracts for a behavior tree.
pub struct BlackboardSchema {
  fields : Array[FieldSpec]
}

///|
/// Create an empty schema.
pub fn BlackboardSchema::new() -> BlackboardSchema {
  { fields: [] }
}

///|
/// Add a field contract and return the same schema for fluent setup.
pub fn BlackboardSchema::add(
  self : BlackboardSchema,
  field : FieldSpec,
) -> BlackboardSchema {
  self.fields.push(field)
  self
}

///|
/// Number of fields in the schema.
pub fn BlackboardSchema::size(self : BlackboardSchema) -> Int {
  self.fields.length()
}

///|
/// Validate all fields without mutating the blackboard.
pub fn BlackboardSchema::validate(
  self : BlackboardSchema,
  bb : Blackboard,
) -> Array[ValidationIssue] {
  let issues : Array[ValidationIssue] = []
  for field in self.fields {
    match bb.get_value(field.key) {
      None =>
        if field.required {
          issues.push(ValidationIssue::MissingField(field.key))
        }
      Some(value) =>
        if value.kind() != field.kind {
          issues.push(
            ValidationIssue::WrongType(field.key, field.kind, value.kind()),
          )
        }
    }
  }
  issues
}

///|
/// Return whether the blackboard satisfies every schema field.
pub fn BlackboardSchema::is_valid(
  self : BlackboardSchema,
  bb : Blackboard,
) -> Bool {
  self.validate(bb).length() == 0
}

///|
/// Install declared defaults without overwriting caller state.
pub fn BlackboardSchema::apply_defaults(
  self : BlackboardSchema,
  bb : Blackboard,
) -> Int {
  let mut applied = 0
  for field in self.fields {
    match field.default_value {
      None => ()
      Some(value) =>
        if bb.set_if_absent(field.key, value) {
          applied = applied + 1
        }
    }
  }
  applied
}