// elicitation.mbt — upstream `src/elicitation.ts` mirror: the lody extension
// of standard ACP `elicitation/create`, mounted at `_meta.lody.elicitation`.
// The same shape is used at request, property, and enum-option scope; decode
// does not distinguish scopes — consumers read only the fields meaningful at
// their scope (upstream source doc comment).

///|
/// `LodyElicitationAnswer` — `string | string[]`: a single choice or a
/// multi-select choice set.
pub(all) enum LodyElicitationAnswer {
  Single(String)
  Multi(Array[String])
} derive(Eq, Debug)

///|
pub fn LodyElicitationAnswer::to_json(self : LodyElicitationAnswer) -> Json {
  match self {
    Single(answer) => Json::string(answer)
    Multi(answers) =>
      Json::array(answers.map(fn(answer) { Json::string(answer) }))
  }
}

///|
pub fn LodyElicitationAnswer::from_json(
  raw : Json,
) -> Result[LodyElicitationAnswer, String] {
  let ctx = "lody.elicitation.answer"
  match raw {
    Json::String(answer) => Ok(Single(answer))
    Json::Array(items) => {
      let answers : Array[String] = []
      for item in items {
        match item {
          Json::String(answer) => answers.push(answer)
          _ => return Err("\{ctx}: every multi-select answer must be a string")
        }
      }
      Ok(Multi(answers))
    }
    _ => Err("\{ctx} must be a string or an array of strings")
  }
}

///|
/// `LodyElicitationOption` — one enum option (label required).
pub(all) struct LodyElicitationOption {
  label : String
  description : String?
  preview : String?
} derive(Eq, Debug)

///|
pub fn LodyElicitationOption::to_json(self : LodyElicitationOption) -> Json {
  let pairs : Array[(String, Json)] = [("label", Json::string(self.label))]
  match self.description {
    Some(v) => pairs.push(("description", Json::string(v)))
    None => ()
  }
  match self.preview {
    Some(v) => pairs.push(("preview", Json::string(v)))
    None => ()
  }
  Json::object(Map::from_array(pairs))
}

///|
pub fn LodyElicitationOption::from_json(
  raw : Json,
) -> Result[LodyElicitationOption, String] {
  try {
    let ctx = "lody.elicitation.option"
    let fields = object_fields(raw, ctx)
    Ok(LodyElicitationOption::{
      label: req_string(fields, ctx, "label"),
      description: opt_string(fields, ctx, "description"),
      preview: opt_string(fields, ctx, "preview"),
    })
  } catch {
    DecodeError::Msg(message) => Err(message)
  }
}

///|
/// `LodyElicitationQuestion` — one question with its option set.
pub(all) struct LodyElicitationQuestion {
  id : String?
  question : String
  header : String
  options : Array[LodyElicitationOption]
  multi_select : Bool
  allow_custom_answer : Bool?
  is_secret : Bool?
} derive(Eq, Debug)

///|
pub fn LodyElicitationQuestion::to_json(self : LodyElicitationQuestion) -> Json {
  let pairs : Array[(String, Json)] = []
  match self.id {
    Some(v) => pairs.push(("id", Json::string(v)))
    None => ()
  }
  pairs.push(("question", Json::string(self.question)))
  pairs.push(("header", Json::string(self.header)))
  pairs.push(
    ("options", Json::array(self.options.map(fn(option) { option.to_json() }))),
  )
  pairs.push(("multiSelect", Json::boolean(self.multi_select)))
  match self.allow_custom_answer {
    Some(v) => pairs.push(("allowCustomAnswer", Json::boolean(v)))
    None => ()
  }
  match self.is_secret {
    Some(v) => pairs.push(("isSecret", Json::boolean(v)))
    None => ()
  }
  Json::object(Map::from_array(pairs))
}

///|
pub fn LodyElicitationQuestion::from_json(
  raw : Json,
) -> Result[LodyElicitationQuestion, String] {
  try {
    let ctx = "lody.elicitation.question"
    let fields = object_fields(raw, ctx)
    let options : Array[LodyElicitationOption] = req_array(
      fields, ctx, "options",
    ).map(item => {
      match LodyElicitationOption::from_json(item) {
        Ok(option) => option
        Err(message) => raise DecodeError::Msg(message)
      }
    })
    Ok(LodyElicitationQuestion::{
      id: opt_string(fields, ctx, "id"),
      question: req_string(fields, ctx, "question"),
      header: req_string(fields, ctx, "header"),
      options,
      multi_select: req_bool(fields, ctx, "multiSelect"),
      allow_custom_answer: opt_bool(fields, ctx, "allowCustomAnswer"),
      is_secret: opt_bool(fields, ctx, "isSecret"),
    })
  } catch {
    DecodeError::Msg(message) => Err(message)
  }
}

///|
/// `LodyElicitationMeta` — the `_meta.lody.elicitation` value. Mounts at
/// request, property, and enum-option scope alike.
pub(all) struct LodyElicitationMeta {
  version : Int
  auto_resolve_after_seconds : Int64?
  auto_resolve_at_epoch_seconds : Int64?
  custom_answer_for : String?
  secret : Bool?
  preview : String?
  questions : Array[LodyElicitationQuestion]?
  answers : Map[String, LodyElicitationAnswer]?
} derive(Eq, Debug)

///|
pub fn LodyElicitationMeta::to_json(self : LodyElicitationMeta) -> Json {
  let pairs : Array[(String, Json)] = [
    ("version", Json::number(self.version.to_double())),
  ]
  match self.auto_resolve_after_seconds {
    Some(v) =>
      pairs.push(("autoResolveAfterSeconds", Json::number(v.to_double())))
    None => ()
  }
  match self.auto_resolve_at_epoch_seconds {
    Some(v) =>
      pairs.push(("autoResolveAtEpochSeconds", Json::number(v.to_double())))
    None => ()
  }
  match self.custom_answer_for {
    Some(v) => pairs.push(("customAnswerFor", Json::string(v)))
    None => ()
  }
  match self.secret {
    Some(v) => pairs.push(("secret", Json::boolean(v)))
    None => ()
  }
  match self.preview {
    Some(v) => pairs.push(("preview", Json::string(v)))
    None => ()
  }
  match self.questions {
    Some(questions) =>
      pairs.push(
        (
          "questions",
          Json::array(questions.map(fn(question) { question.to_json() })),
        ),
      )
    None => ()
  }
  match self.answers {
    Some(answers) => {
      let answer_pairs : Array[(String, Json)] = []
      for question_id, answer in answers {
        answer_pairs.push((question_id, answer.to_json()))
      }
      pairs.push(("answers", Json::object(Map::from_array(answer_pairs))))
    }
    None => ()
  }
  Json::object(Map::from_array(pairs))
}

///|
pub fn LodyElicitationMeta::from_json(
  raw : Json,
) -> Result[LodyElicitationMeta, String] {
  try {
    let ctx = "lody.elicitation"
    let fields = object_fields(raw, ctx)
    let version = req_int(fields, ctx, "version")
    guard version == 1 else {
      raise DecodeError::Msg(
        "\{ctx}: unsupported version \{version} (expected 1)",
      )
    }
    let questions : Array[LodyElicitationQuestion]? = match
      fields.get("questions") {
      None => None
      Some(Json::Null) => None
      Some(Json::Array(_)) =>
        Some(
          req_array(fields, ctx, "questions").map(item => {
            match LodyElicitationQuestion::from_json(item) {
              Ok(question) => question
              Err(message) => raise DecodeError::Msg(message)
            }
          }),
        )
      Some(_) =>
        raise DecodeError::Msg("\{ctx}: field \"questions\" must be an array")
    }
    let answers : Map[String, LodyElicitationAnswer]? = match
      opt_object(fields, ctx, "answers") {
      None => None
      Some(answer_fields) => {
        let parsed : Array[(String, LodyElicitationAnswer)] = []
        for question_id, answer in answer_fields {
          match LodyElicitationAnswer::from_json(answer) {
            Ok(value) => parsed.push((question_id, value))
            Err(message) => raise DecodeError::Msg(message)
          }
        }
        Some(Map::from_array(parsed))
      }
    }
    Ok(LodyElicitationMeta::{
      version,
      auto_resolve_after_seconds: opt_int64(
        fields, ctx, "autoResolveAfterSeconds",
      ),
      auto_resolve_at_epoch_seconds: opt_int64(
        fields, ctx, "autoResolveAtEpochSeconds",
      ),
      custom_answer_for: opt_string(fields, ctx, "customAnswerFor"),
      secret: opt_bool(fields, ctx, "secret"),
      preview: opt_string(fields, ctx, "preview"),
      questions,
      answers,
    })
  } catch {
    DecodeError::Msg(message) => Err(message)
  }
}