// Session setup: the modes and configuration options a client shows in its picker, and the session
// identifiers the rest of the protocol keys on.

///|
/// One mode the agent can work in.
#derive(Eq)
pub(all) struct SessionMode {
  id : String
  name : String
  description : String?
  meta : Json?
}

///|
pub extend SessionMode with Eq::{equal, not_equal}

///|
pub extend SessionMode with @json.ToJson::{to_json}

///|
pub impl ToJson for SessionMode with fn to_json(self) {
  json_of([
    ("id", Some(Json::string(self.id))),
    ("name", Some(Json::string(self.name))),
    ("description", json_string(self.description)),
    ("_meta", self.meta),
  ])
}

///|
pub extend SessionMode with @json.FromJson::{from_json}

///|
pub impl @json.FromJson for SessionMode with fn from_json(json, path) {
  let fields = expect_object(json, path)
  {
    id: required_string(fields, "id", path),
    name: required_string(fields, "name", path),
    description: string_member(fields, "description", path),
    meta: meta_of(fields, path),
  }
}

///|
/// The modes on offer and the one in force.
#derive(Eq)
pub(all) struct SessionModes {
  current_mode_id : String
  available_modes : Array[SessionMode]
  meta : Json?
}

///|
pub extend SessionModes with Eq::{equal, not_equal}

///|
pub extend SessionModes with @json.ToJson::{to_json}

///|
pub impl ToJson for SessionModes with fn to_json(self) {
  json_of([
    ("currentModeId", Some(Json::string(self.current_mode_id))),
    ("availableModes", Some(Json::array(self.available_modes.map(write)))),
    ("_meta", self.meta),
  ])
}

///|
pub extend SessionModes with @json.FromJson::{from_json}

///|
pub impl @json.FromJson for SessionModes with fn from_json(json, path) {
  let fields = expect_object(json, path)
  {
    current_mode_id: required_string(fields, "currentModeId", path),
    available_modes: required_objects(fields, "availableModes", path),
    meta: meta_of(fields, path),
  }
}

///|
/// What a configuration option is about, so a client can place it. Unknown categories are kept.
#derive(Eq)
pub(all) enum ConfigCategory {
  Mode
  Model
  ModelConfig
  ThoughtLevel
  Other(String)
}

///|
pub extend ConfigCategory with Eq::{equal, not_equal}

///|
/// The name the category travels as.
pub fn ConfigCategory::to_string(self : ConfigCategory) -> String {
  match self {
    Mode => "mode"
    Model => "model"
    ModelConfig => "model_config"
    ThoughtLevel => "thought_level"
    Other(name) => name
  }
}

///|
/// The category with that name; unknown names are kept as they came.
pub fn ConfigCategory::of(name : String) -> ConfigCategory {
  match name {
    "mode" => Mode
    "model" => Model
    "model_config" => ModelConfig
    "thought_level" => ThoughtLevel
    other => Other(other)
  }
}

///|
pub extend ConfigCategory with @json.ToJson::{to_json}

///|
pub impl ToJson for ConfigCategory with fn to_json(self) {
  Json::string(self.to_string())
}

///|
pub extend ConfigCategory with @json.FromJson::{from_json}

///|
pub impl @json.FromJson for ConfigCategory with fn from_json(json, path) {
  ConfigCategory::of(text_of(json, path))
}

///|
/// One value of a dropdown.
#derive(Eq)
pub(all) struct ConfigChoice {
  value : String
  name : String
  description : String?
  meta : Json?
}

///|
pub extend ConfigChoice with Eq::{equal, not_equal}

///|
pub extend ConfigChoice with @json.ToJson::{to_json}

///|
pub impl ToJson for ConfigChoice with fn to_json(self) {
  json_of([
    ("value", Some(Json::string(self.value))),
    ("name", Some(Json::string(self.name))),
    ("description", json_string(self.description)),
    ("_meta", self.meta),
  ])
}

///|
pub extend ConfigChoice with @json.FromJson::{from_json}

///|
pub impl @json.FromJson for ConfigChoice with fn from_json(json, path) {
  let fields = expect_object(json, path)
  {
    value: required_string(fields, "value", path),
    name: required_string(fields, "name", path),
    description: string_member(fields, "description", path),
    meta: meta_of(fields, path),
  }
}

///|
/// Choices under a heading.
#derive(Eq)
pub(all) struct ConfigGroup {
  group : String
  name : String
  options : Array[ConfigChoice]
  meta : Json?
}

///|
pub extend ConfigGroup with Eq::{equal, not_equal}

///|
pub extend ConfigGroup with @json.ToJson::{to_json}

///|
pub impl ToJson for ConfigGroup with fn to_json(self) {
  json_of([
    ("group", Some(Json::string(self.group))),
    ("name", Some(Json::string(self.name))),
    ("options", Some(Json::array(self.options.map(write)))),
    ("_meta", self.meta),
  ])
}

///|
pub extend ConfigGroup with @json.FromJson::{from_json}

///|
pub impl @json.FromJson for ConfigGroup with fn from_json(json, path) {
  let fields = expect_object(json, path)
  {
    group: required_string(fields, "group", path),
    name: required_string(fields, "name", path),
    options: required_objects(fields, "options", path),
    meta: meta_of(fields, path),
  }
}

///|
/// The choices of a dropdown, flat or grouped. The two are told apart by whether the first entry
/// carries `group`.
#derive(Eq)
pub(all) enum ConfigChoices {
  Flat(Array[ConfigChoice])
  Grouped(Array[ConfigGroup])
}

///|
pub extend ConfigChoices with Eq::{equal, not_equal}

///|
pub extend ConfigChoices with @json.ToJson::{to_json}

///|
pub impl ToJson for ConfigChoices with fn to_json(self) {
  match self {
    Flat(choices) => Json::array(choices.map(write))
    Grouped(groups) => Json::array(groups.map(write))
  }
}

///|
pub extend ConfigChoices with @json.FromJson::{from_json}

///|
pub impl @json.FromJson for ConfigChoices with fn from_json(json, path) {
  let items = array_of(json, path)
  if items.length() > 0 &&
    (match items[0] {
      Object(fields) => fields.contains("group")
      _ => false
    }) {
    Grouped(decode_list(json, path, (item, at) => read(item, at)))
  } else {
    Flat(decode_list(json, path, (item, at) => read(item, at)))
  }
}

///|
/// What a configuration option holds: a picked value or a switch.
#derive(Eq)
pub(all) enum ConfigValue {
  Select(String, ConfigChoices)
  Boolean(Bool)
}

///|
pub extend ConfigValue with Eq::{equal, not_equal}

///|
pub extend ConfigValue with @json.ToJson::{to_json}

///|
pub impl ToJson for ConfigValue with fn to_json(self) {
  match self {
    Select(current_value, options) =>
      json_of([
        ("currentValue", Some(Json::string(current_value))),
        ("options", Some(write(options))),
      ])
    Boolean(value) => json_of([("currentValue", Some(Json::boolean(value)))])
  }
}

///|
/// One entry of the session configuration picker.
#derive(Eq)
pub(all) struct ConfigOption {
  id : String
  name : String
  description : String?
  category : ConfigCategory?
  value : ConfigValue
  meta : Json?
}

///|
pub extend ConfigOption with Eq::{equal, not_equal}

///|
pub extend ConfigOption with @json.ToJson::{to_json}

///|
pub impl ToJson for ConfigOption with fn to_json(self) {
  let (tag, body) = match self.value {
    Select(_, _) => ("select", write(self.value))
    Boolean(_) => ("boolean", write(self.value))
  }
  let base = json_of([
    ("id", Some(Json::string(self.id))),
    ("name", Some(Json::string(self.name))),
    ("description", json_string(self.description)),
    ("category", self.category.map(write)),
    ("_meta", self.meta),
  ])
  json_tagged(merge_json(base, body), "type", tag)
}

///|
pub extend ConfigOption with @json.FromJson::{from_json}

///|
pub impl @json.FromJson for ConfigOption with fn from_json(json, path) {
  let fields = expect_object(json, path)
  let id = required_string(fields, "id", path)
  let name = required_string(fields, "name", path)
  let description = string_member(fields, "description", path)
  let category = enum_member(fields, "category", path, name => {
    Some(ConfigCategory::of(name))
  })
  let meta = meta_of(fields, path)
  let value = match required_string(fields, "type", path) {
    "select" =>
      Select(
        required_string(fields, "currentValue", path),
        required_object(fields, "options", path),
      )
    "boolean" =>
      Boolean(
        bool_of(
          required(fields, "currentValue", path),
          path.add_key("currentValue"),
        ),
      )
    other =>
      raise @json.JsonDecodeError(
        (path.add_key("type"), "unknown config option \{other}"),
      )
  }
  { id, name, description, category, value, meta, }
}

///|
/// What `session/new` and its siblings answer with.
#derive(Eq)
pub(all) struct SessionStarted {
  session_id : String
  modes : SessionModes?
  config_options : Array[ConfigOption]?
  meta : Json?
}

///|
pub extend SessionStarted with Eq::{equal, not_equal}

///|
pub extend SessionStarted with @json.ToJson::{to_json}

///|
pub impl ToJson for SessionStarted with fn to_json(self) {
  json_of([
    ("sessionId", Some(Json::string(self.session_id))),
    ("modes", self.modes.map(write)),
    ("configOptions", json_list_of(self.config_options)),
    ("_meta", self.meta),
  ])
}

///|
pub extend SessionStarted with @json.FromJson::{from_json}

///|
pub impl @json.FromJson for SessionStarted with fn from_json(json, path) {
  let fields = expect_object(json, path)
  {
    session_id: required_string(fields, "sessionId", path),
    modes: object_member(fields, "modes", path),
    config_options: list_member(fields, "configOptions", path, (value, at) => {
      read(value, at)
    }),
    meta: meta_of(fields, path),
  }
}

///|
/// The whole option list after one of them changed.
#derive(Eq)
pub(all) struct SetConfigOptions {
  config_options : Array[ConfigOption]
  meta : Json?
}

///|
pub extend SetConfigOptions with Eq::{equal, not_equal}

///|
pub extend SetConfigOptions with @json.ToJson::{to_json}

///|
pub impl ToJson for SetConfigOptions with fn to_json(self) {
  json_of([
    ("configOptions", Some(Json::array(self.config_options.map(write)))),
    ("_meta", self.meta),
  ])
}

///|
pub extend SetConfigOptions with @json.FromJson::{from_json}

///|
pub impl @json.FromJson for SetConfigOptions with fn from_json(json, path) {
  let fields = expect_object(json, path)
  {
    config_options: required_objects(fields, "configOptions", path),
    meta: meta_of(fields, path),
  }
}