///|
/// One option of a `Choice` question: a label the model can return, with an
/// optional description shown to the model but never echoed back.
pub(all) struct ChoiceOption {
  label : String
  description : String?
} derive(Debug, Eq)

///|
/// A labelled option with no description.
pub fn ChoiceOption::bare(label : String) -> ChoiceOption {
  { label, description: None, }
}

///|
/// A labelled option with a description.
pub fn ChoiceOption::described(
  label : String,
  description : String,
) -> ChoiceOption {
  { label, description: Some(description), }
}

///|
/// A typed question about some state.
///
/// These are the three answer shapes Laya is trained to produce. All of them
/// come out of a single forward pass: the model scores one `[MASK]` marker per
/// option and the distribution over those markers is the answer.
pub(all) enum Question {
  /// Pick one label. Returns the top label and a probability per label.
  Choice(instructions~ : String, options~ : Array[ChoiceOption])
  /// Place the state on an ordinal scale, where `levels[i]` describes level
  /// `i`. Returns the probability-weighted level.
  Score(instructions~ : String, levels~ : Array[String])
  /// A calibrated probability that a statement holds. `when_false` and
  /// `when_true` override the default descriptions of the two outcomes.
  Noul(instructions~ : String, when_false~ : String?, when_true~ : String?)
} derive(Debug, Eq)

///|
/// A choice over bare labels.
pub fn Question::choice(
  instructions : String,
  labels : Array[String],
) -> Question {
  Choice(instructions~, options=labels.map(ChoiceOption::bare))
}

///|
/// A choice over `(label, description)` pairs.
pub fn Question::choice_described(
  instructions : String,
  options : Array[(String, String)],
) -> Question {
  Choice(
    instructions~,
    options=options.map(pair => ChoiceOption::described(pair.0, pair.1)),
  )
}

///|
/// An ordinal score over the given level descriptions.
pub fn Question::score(
  instructions : String,
  levels : Array[String],
) -> Question {
  Score(instructions~, levels~)
}

///|
/// A calibrated true/false probability with the default outcome descriptions.
pub fn Question::noul(instructions : String) -> Question {
  Noul(instructions~, when_false=None, when_true=None)
}

///|
/// The question-type index the model's type embedding is keyed by.
pub fn Question::type_index(self : Question) -> Int {
  match self {
    Choice(..) => 0
    Score(..) => 1
    Noul(..) => 2
  }
}

///|
/// The question-type name used in the prompt and in calibration bucket keys.
pub fn Question::type_name(self : Question) -> String {
  match self {
    Choice(..) => "choice"
    Score(..) => "score"
    Noul(..) => "noul"
  }
}

///|
/// The instruction text.
pub fn Question::instructions(self : Question) -> String {
  match self {
    Choice(instructions~, ..)
    | Score(instructions~, ..)
    | Noul(instructions~, ..) => instructions
  }
}

///|
/// The option texts as the model sees them, in the order their markers appear.
///
/// A `Choice` option renders as `"label"` when it has no description and
/// `"label: description"` when it does — only `None` and `""` count as absent,
/// so a description of `"0"` or `"false"` is still rendered. `Score` renders as
/// `"level : "`. `Noul` is always exactly two options.
pub fn Question::rendered_options(self : Question) -> Array[String] {
  match self {
    Choice(options~, ..) =>
      options.map(option => {
        match option.description {
          None | Some("") => option.label
          Some(description) => "\{option.label}: \{description}"
        }
      })
    Score(levels~, ..) =>
      levels.mapi((index, level) => "level \{index}: \{level}")
    Noul(when_false~, when_true~, ..) =>
      [
        "false: " + describe(when_false, "no, the statement does not hold"),
        "true: " + describe(when_true, "yes, the statement holds"),
      ]
  }
}

///|
fn describe(text : String?, fallback : String) -> String {
  match text {
    None | Some("") => fallback
    Some(text) => text
  }
}

///|
/// The labels a `Choice` answer can take, in marker order.
pub fn Question::labels(self : Question) -> Array[String] {
  match self {
    Choice(options~, ..) => options.map(option => option.label)
    Score(levels~, ..) => levels.mapi((index, _) => index.to_string())
    Noul(..) => ["false", "true"]
  }
}

///|
/// Reject a question the model cannot be asked.
pub fn Question::validate(self : Question) -> Unit raise @error.LayaError {
  match self {
    Choice(options~, ..) => {
      @error.expect(
        options.length() > 0,
        "a choice question needs at least one option",
      )
      let seen = Map([])
      for option in options {
        @error.expect(
          !seen.contains(option.label),
          "choice labels must be unique, \{option.label} appears twice",
        )
        seen[option.label] = ()
      }
    }
    Score(levels~, ..) =>
      @error.expect(
        levels.length() > 0,
        "a score question needs at least one level",
      )
    Noul(..) => ()
  }
}