// Signal adapter via `signal-cli` JSON-RPC 2.0.
//
// Mirrors Hermes `plugins/platforms/signal/adapter.py`: every operation is a
// `POST {http_url}/api/v1/rpc` with `method: "send"` / `"sendTyping"`, and the
// account (phone number) is supplied in `params`. Group chats are addressed
// with a `group:` prefix on the chat id.

///|
/// signal-cli HTTP root (`SIGNAL_HTTP_URL`, e.g. `http://127.0.0.1:8080`).
pub fn signal_http_url(cfg : PlatformConfig) -> String {
  let url = cfg.extra_str("http_url", "")
  if url.has_suffix("/") {
    url[:url.length() - 1].to_owned()
  } else {
    url
  }
}

///|
/// Account phone number (`SIGNAL_ACCOUNT`).
pub fn signal_account(cfg : PlatformConfig) -> String? {
  cfg.extra_first(["account", "number"])
}

///|
/// Add the `recipient`/`groupId` routing key for ``chat_id`` to ``params``.
pub fn signal_with_target(params : Map[String, Json], chat_id : String) -> Unit {
  if chat_id.has_prefix("group:") {
    params.set("groupId", Json::string(chat_id[6:].to_owned()))
  } else {
    let recipients = Json::array([Json::string(chat_id)])
    params.set("recipient", recipients)
  }
}

///|
/// JSON-RPC body for `send`.
pub fn signal_send_params(
  account : String,
  chat_id : String,
  text : String,
) -> Json {
  let params : Map[String, Json] = Map([])
  params.set("account", Json::string(account))
  signal_with_target(params, chat_id)
  params.set("message", Json::string(text))
  json_obj([
    ("jsonrpc", Json::string("2.0")),
    ("method", Json::string("send")),
    ("params", Json::object(params)),
    ("id", Json::string("send_\{now_ms()}")),
  ])
}

///|
/// JSON-RPC body for `sendTyping`.
pub fn signal_typing_params(account : String, chat_id : String) -> Json {
  let params : Map[String, Json] = Map([])
  params.set("account", Json::string(account))
  signal_with_target(params, chat_id)
  json_obj([
    ("jsonrpc", Json::string("2.0")),
    ("method", Json::string("sendTyping")),
    ("params", Json::object(params)),
    ("id", Json::string("typing_\{now_ms()}")),
  ])
}

///|
/// One JSON-RPC call; returns the `result` object or an error string.
async fn signal_rpc(
  cfg : PlatformConfig,
  payload : Json,
) -> Result[Json, SendResult] {
  let url = "\{signal_http_url(cfg)}/api/v1/rpc"
  match post_json(url, payload, json_headers()) {
    Err(e) => Err(send_failure("signal rpc", e))
    Ok(j) =>
      match json_field_obj(j, "error") {
        Some(err) =>
          Err(
            send_body_failure(
              "signal rpc",
              json_field_string(err, "message").unwrap_or(err.stringify()),
            ),
          )
        None => Ok(json_field_obj(j, "result").unwrap_or(Json::null()))
      }
  }
}

///|
/// Send ``content``, chunked at Signal's 8000-char limit.
pub async fn signal_send(
  cfg : PlatformConfig,
  chat_id : String,
  content : String,
  reply_to? : String,
) -> SendResult {
  ignore(reply_to)
  match signal_account(cfg) {
    None => missing_config(Signal, "SIGNAL_ACCOUNT")
    Some(account) =>
      if signal_http_url(cfg).is_empty() {
        missing_config(Signal, "SIGNAL_HTTP_URL")
      } else if content.is_empty() {
        SendResult::ok()
      } else {
        let mut last = SendResult::ok()
        for chunk in chunk_text(content, Platform::max_message_length(Signal)) {
          let r = send_with_retry(() => {
            match signal_rpc(cfg, signal_send_params(account, chat_id, chunk)) {
              Err(e) => e
              Ok(result) =>
                SendResult::ok(
                  message_id?=json_field_int(result, "timestamp").map(x => {
                    x.to_string()
                  }),
                  raw=result.stringify(),
                )
            }
          })
          if !r.success {
            return r
          }
          last = r
        }
        last
      }
  }
}

///|
/// Typing indicator (`method: sendTyping`), failures ignored.
pub async fn signal_typing(cfg : PlatformConfig, chat_id : String) -> Unit {
  match signal_account(cfg) {
    None => ()
    Some(account) =>
      if signal_http_url(cfg).is_empty() {
        ()
      } else {
        ignore(signal_rpc(cfg, signal_typing_params(account, chat_id)))
      }
  }
}

///|
/// Parse a `signal-cli` `dataMessage` envelope into a `MessageEvent`.
pub fn signal_parse_envelope(envelope : Json) -> MessageEvent? {
  let data = match json_field_obj(envelope, "dataMessage") {
    Some(d) => d
    None => return None
  }
  let source = or_opt(
    json_field_string(envelope, "sourceNumber"),
    json_field_string(envelope, "source"),
  ).unwrap_or("")
  if source.is_empty() {
    return None
  }
  let chat_id = match json_field_obj(data, "groupInfo") {
    Some(g) =>
      match json_field_string(g, "groupId") {
        Some(gid) => "group:\{gid}"
        None => source
      }
    None => source
  }
  let text = json_field_string(data, "message").unwrap_or("")
  let media : Array[MediaAttachment] = []
  for att in json_field_array(data, "attachments").unwrap_or([]) {
    let content_type = json_field_string(att, "contentType").unwrap_or("")
    let kind = if content_type.has_prefix("image/") {
      "photo"
    } else if content_type.has_prefix("audio/") {
      "voice"
    } else if content_type.has_prefix("video/") {
      "video"
    } else {
      "document"
    }
    media.push({ url: json_field_string(att, "id").unwrap_or(""), kind, })
  }
  let reply = match json_field_obj(data, "quote") {
    Some(q) => json_id(q, "id")
    None => None
  }
  let mtype = if text.has_prefix("/") { Command } else { Text }
  Some(
    MessageEvent::new(
      text,
      mtype,
      Signal,
      chat_id,
      user_id?=json_field_string(envelope, "sourceUuid"),
      user_name?=json_field_string(envelope, "sourceName"),
      message_id?=json_field_int(data, "timestamp").map(x => x.to_string()),
      reply_to_message_id?=reply,
      media~,
      timestamp=now_ms(),
    ),
  )
}

///|
/// Parse the array returned by `GET /v1/receive/{account}` (or a JSON-RPC
/// receive result) into events.
pub fn signal_parse_receive(body : String) -> Array[MessageEvent] {
  let out : Array[MessageEvent] = []
  let j = @json.parse(body) catch { _ => return out }
  let items = match j {
    Array(a) => a
    Object(_) =>
      match json_field_array(j, "envelopes") {
        Some(a) => a
        None => []
      }
    _ => []
  }
  for item in items {
    let envelope = json_field_obj(item, "envelope").unwrap_or(item)
    match signal_parse_envelope(envelope) {
      Some(ev) => out.push(ev)
      None => ()
    }
  }
  out
}