// BlueBubbles (iMessage) adapter.
//
// Mirrors Hermes `plugins/platforms/bluebubbles/adapter.py`: the BlueBubbles
// server exposes a local REST API at `{server_url}/api/v1`, authenticated with
// a password query parameter, and pushes inbound messages to a webhook as
// `{"type":"new-message","data":{…}}`.

///|
/// API root (`{server_url}/api/v1`).
pub fn bluebubbles_api_base(cfg : PlatformConfig) -> String {
  let url = cfg.extra_str("server_url", "")
  let trimmed = if url.has_suffix("/") {
    url[:url.length() - 1].to_owned()
  } else {
    url
  }
  "\{trimmed}/api/v1"
}

///|
/// Append the password query parameter to an endpoint.
pub fn bluebubbles_url(cfg : PlatformConfig, path : String) -> String {
  let password = cfg.token_of().unwrap_or("")
  if password.is_empty() {
    "\{bluebubbles_api_base(cfg)}\{path}"
  } else {
    "\{bluebubbles_api_base(cfg)}\{path}?password=\{url_quote(password)}"
  }
}

///|
/// `POST /message/text` payload.
pub fn bluebubbles_send_payload(
  chat_guid : String,
  message : String,
  reply_to : String?,
) -> Json {
  let payload = json_obj([
    ("chatGuid", Json::string(chat_guid)),
    ("tempGuid", Json::string("temp-\{random_hex(12)}")),
    ("message", Json::string(message)),
    ("method", Json::string("apple-script")),
  ])
  obj_with_opt(payload, "selectedMessageGuid", reply_to)
}

///|
/// Map a BlueBubbles `{status, message, data}` response onto a `SendResult`.
fn bluebubbles_result(j : Json, prefix : String) -> SendResult {
  let status = json_field_int(j, "status").unwrap_or(200)
  if status >= 200 && status < 300 {
    let mid = match json_field_obj(j, "data") {
      Some(d) => json_field_string(d, "guid")
      None => json_field_string(j, "guid")
    }
    send_ok(mid, j)
  } else {
    let msg = json_field_string(j, "message").unwrap_or(j.stringify())
    SendResult::failed(
      "\{prefix}: \{msg}",
      retryable=status == 503 || status == 500,
      kind=classify_send_error(msg),
      raw=j.stringify(),
    )
  }
}

///|
/// One BlueBubbles call.
async fn bluebubbles_call(
  cfg : PlatformConfig,
  meth : String,
  path : String,
  payload : Json,
) -> SendResult {
  let url = bluebubbles_url(cfg, path)
  let res = match meth {
    "GET" => get_json(url, json_headers())
    "POST" => post_json(url, payload, json_headers())
    _ => post_json(url, payload, json_headers())
  }
  match res {
    Err(e) => send_failure("bluebubbles", e)
    Ok(j) => bluebubbles_result(j, "bluebubbles")
  }
}

///|
/// Send ``content``, chunked at iMessage's practical 4000-char limit.
pub async fn bluebubbles_send(
  cfg : PlatformConfig,
  chat_id : String,
  content : String,
  reply_to? : String,
) -> SendResult {
  if bluebubbles_api_base(cfg) == "/api/v1" {
    return missing_config(Bluebubbles, "BLUEBUBBLES_SERVER_URL")
  }
  if content.is_empty() {
    return SendResult::ok()
  }
  let mut first = true
  let mut last = SendResult::ok()
  let mut first_id : String? = None
  for chunk in chunk_text(content, Platform::max_message_length(Bluebubbles)) {
    let reply = if first { reply_to } else { None }
    let r = send_with_retry(() => {
      bluebubbles_call(
        cfg,
        "POST",
        "/message/text",
        bluebubbles_send_payload(chat_id, chunk, reply),
      )
    })
    first = false
    if !r.success {
      return r
    }
    if first_id is None {
      first_id = r.message_id
    }
    last = r
  }
  { ..last, message_id: first_id, }
}

///|
/// Typing indicator (`POST /chat/{guid}/typing`).
pub async fn bluebubbles_typing(cfg : PlatformConfig, chat_id : String) -> Unit {
  ignore(
    bluebubbles_call(
      cfg,
      "POST",
      "/chat/\{url_quote(chat_id)}/typing",
      Json::null(),
    ),
  )
}

///|
/// Last path segment of a local file path.
fn basename(path : String) -> String {
  let mut name = path
  for part in path.split("/") {
    let p = part.to_owned()
    if !p.is_empty() {
      name = p
    }
  }
  name
}

///|
/// Send a media attachment by local file path.
pub async fn bluebubbles_send_attachment(
  cfg : PlatformConfig,
  chat_id : String,
  attachment_path : String,
) -> SendResult {
  let payload = json_obj([
    ("chatGuid", Json::string(chat_id)),
    ("tempGuid", Json::string("temp-\{random_hex(12)}")),
    ("name", Json::string(basename(attachment_path))),
    ("path", Json::string(attachment_path)),
    ("method", Json::string("apple-script")),
  ])
  bluebubbles_call(cfg, "POST", "/message/attachment", payload)
}

///|
/// Server health (`GET /ping`) — also validates the password.
pub async fn bluebubbles_ping(cfg : PlatformConfig) -> Bool {
  match get_json(bluebubbles_url(cfg, "/ping"), json_headers()) {
    Err(_) => false
    Ok(j) => json_field_int(j, "status").unwrap_or(200) == 200
  }
}

///|
/// Chat metadata (`POST /chat/query` filtered by guid).
pub async fn bluebubbles_get_chat(
  cfg : PlatformConfig,
  chat_id : String,
) -> ChatInfo? {
  let payload = json_obj([
    ("limit", Json::number(1.0)),
    ("offset", Json::number(0.0)),
    ("with", Json::array([Json::string("participants")])),
  ])
  match
    post_json(bluebubbles_url(cfg, "/chat/query"), payload, json_headers()) {
    Err(_) => None
    Ok(j) =>
      match json_field_array(j, "data") {
        Some(items) =>
          if items.length() > 0 {
            let c = items[0]
            Some({
              name: or_opt(
                json_field_string(c, "displayName"),
                json_field_string(c, "guid"),
              ).unwrap_or(chat_id),
              chat_type: if json_field_array(c, "participants")
                .unwrap_or([])
                .length() >
                1 {
                "group"
              } else {
                "dm"
              },
              chat_id,
            })
          } else {
            None
          }
        None => None
      }
  }
}

///|
/// Webhook credential from a BlueBubbles POST: the `password` query parameter
/// (BlueBubbles appends it to registered webhook URLs) or the `x-password`
/// header (Hermes `_webhook_token`).
pub fn bluebubbles_webhook_token(query : String, header : String?) -> String? {
  match query_param(query, "password") {
    Some(p) => if !p.is_empty() { return Some(p) }
    None => ()
  }
  match header {
    Some(h) => if !h.is_empty() { Some(h) } else { None }
    None => None
  }
}

///|
/// Parse a BlueBubbles `new-message` webhook into events. Self-sent messages
/// (`isFromMe`) are filtered to avoid echo loops.
pub fn bluebubbles_parse_event(body : String) -> Array[MessageEvent] {
  let out : Array[MessageEvent] = []
  let j = @json.parse(body) catch { _ => return out }
  if json_field_string(j, "type") is Some(t) {
    if t != "new-message" {
      return out
    }
  }
  let data = match json_field_obj(j, "data") {
    Some(d) => d
    None => j
  }
  if json_field_bool(data, "isFromMe").unwrap_or(false) {
    return out
  }
  let chat_guid = match json_field_array(data, "chats") {
    Some(chats) =>
      if chats.length() > 0 {
        json_field_string(chats[0], "guid")
      } else {
        None
      }
    None => None
  }
  let chat_id = or_opt(chat_guid, json_field_string(data, "chatGuid")).unwrap_or(
    "",
  )
  if chat_id.is_empty() {
    return out
  }
  let text = json_field_string(data, "text").unwrap_or("")
  let handle = json_field_obj(data, "handle").unwrap_or(Json::null())
  let has_attachment = json_field_array(data, "attachments")
    .unwrap_or([])
    .length() >
    0
  let mtype = if text.has_prefix("/") {
    Command
  } else if has_attachment {
    Photo
  } else {
    Text
  }
  out.push(
    MessageEvent::new(
      text,
      mtype,
      Bluebubbles,
      chat_id,
      user_id?=json_field_string(handle, "address"),
      message_id?=json_field_string(data, "guid"),
      media=[],
      timestamp=now_ms(),
    ),
  )
  out
}