// Per-chat session state and slash-command handling.
//
// Mirrors Hermes' conversation session handling: each `(platform, chat_id)`
// pair keeps a sliding history window plus the routing state needed for
// follow-up sends (`last_message_id`, `thread_id`, `context_token`). Slash
// commands (`/help`, `/reset`, `/status`, `/new`) are answered locally without
// invoking the agent.

///|
/// One conversation. `history` and `state` are reference types, so mutating a
/// session obtained from a `SessionStore` mutates the stored session.
pub(all) struct Session {
  platform : Platform
  chat_id : String
  /// Sliding window of `(role, content)` turns.
  history : Array[(String, String)]
  /// Routing state: `last_message_id`, `thread_id`, `context_token`, `user_name`.
  state : Map[String, String]
  /// History window size.
  max_history : Int32
} derive(Debug)

///|
/// Session registry keyed by `platform:chat_id`.
pub(all) struct SessionStore {
  sessions : Map[String, Session]
  max_history : Int32
} derive(Debug)

///|
pub fn SessionStore::new(max_history~ : Int32 = 24) -> SessionStore {
  { sessions: Map([]), max_history, }
}

///|
/// Fetch (or create) the session for a chat.
pub fn SessionStore::session(
  self : SessionStore,
  platform : Platform,
  chat_id : String,
) -> Session {
  let key = "\{platform.value()}:\{chat_id}"
  match self.sessions.get(key) {
    Some(s) => s
    None => {
      let history : Array[(String, String)] = []
      let s : Session = {
        platform,
        chat_id,
        history,
        state: Map([]),
        max_history: self.max_history,
      }
      self.sessions.set(key, s)
      s
    }
  }
}

///|
/// Number of tracked sessions.
pub fn SessionStore::len(self : SessionStore) -> Int32 {
  self.sessions.length()
}

///|
/// Append a turn, trimming the window to `max_history`.
pub fn Session::push(self : Session, role : String, content : String) -> Unit {
  self.history.push((role, content))
  while self.history.length() > self.max_history {
    ignore(self.history.remove(0))
  }
}

///|
/// Drop all history for this session.
pub fn Session::reset(self : Session) -> Unit {
  self.history.clear()
  self.state.clear()
}

///|
/// Record the id of the last message this gateway sent (reply target).
pub fn Session::set_last_message_id(self : Session, id : String) -> Unit {
  self.state.set("last_message_id", id)
}

///|
/// The last message id this gateway sent, if any.
pub fn Session::last_message_id(self : Session) -> String? {
  self.state.get("last_message_id")
}

///|
/// Remember a thread id for follow-up sends.
pub fn Session::set_thread_id(self : Session, id : String) -> Unit {
  self.state.set("thread_id", id)
}

///|
/// The stored thread id, if any.
pub fn Session::thread_id(self : Session) -> String? {
  self.state.get("thread_id")
}

///|
/// Remember the WeChat iLink `context_token` so replies stay in session.
pub fn Session::set_context_token(self : Session, token : String) -> Unit {
  self.state.set("context_token", token)
}

///|
/// The stored iLink `context_token`, if any.
pub fn Session::context_token(self : Session) -> String? {
  self.state.get("context_token")
}

///|
/// Render the history as an OpenAI-style `messages` array.
pub fn Session::messages_json(self : Session) -> Json {
  let items : Array[Json] = []
  for turn in self.history {
    items.push(
      json_obj([
        ("role", Json::string(turn.0)),
        ("content", Json::string(turn.1)),
      ]),
    )
  }
  Json::array(items)
}

///|
/// Parsed slash command: `(name, argument tail)`, lower-cased name and no
/// leading slash.
pub fn parse_command(text : String) -> (String, String)? {
  if !text.has_prefix("/") {
    return None
  }
  let token = match text.split_once(" ") {
    Some((head, tail)) => (head.to_owned(), tail.to_owned())
    None => (text, "")
  }
  let raw = token.0[1:].to_owned()
  let name = match raw.split_once("@") {
    Some((head, _)) => head.to_owned().to_lower()
    None => raw.to_lower()
  }
  if name.is_empty() {
    return None
  }
  Some((name, token.1.trim().to_owned()))
}

///|
/// Answer a built-in slash command locally. Returns `None` for commands the
/// agent should handle (and for plain messages).
pub fn command_reply(
  command : (String, String),
  cfg : GatewayConfig,
  session : Session,
) -> String? {
  match command.0 {
    "help" | "commands" =>
      Some(
        "imrelay commands:\n" +
        "/help — show this message\n" +
        "/reset — clear this chat's context\n" +
        "/new — start a fresh conversation\n" +
        "/status — show the session state\n" +
        "/platforms — list configured platforms",
      )
    "reset" | "new" | "clear" => {
      session.reset()
      Some("Context cleared.")
    }
    "status" =>
      Some(
        "platform: \{session.platform.value()}\n" +
        "chat: \{session.chat_id}\n" +
        "turns: \{session.history.length()}/\{session.max_history}\n" +
        "thread: \{session.thread_id().unwrap_or("-")}",
      )
    "platforms" => {
      let sb = StringBuilder()
      sb.write_string("Configured platforms:\n")
      for p in cfg.enabled_platforms() {
        sb.write_string("- \{p.name()} (\{p.value()})\n")
      }
      Some(sb.to_string())
    }
    _ => None
  }
}

///|
/// Hermes' silence contract: a reply equal to one of these markers is
/// suppressed instead of sent (§"silent" tokens). Case-insensitive.
pub fn is_silent_reply(text : String) -> Bool {
  let t = text.trim().to_lower()
  t == "[silent]" ||
  t == "no_reply" ||
  t == "no reply" ||
  t == "[no reply]" ||
  t.is_empty()
}