///|
const MODEL_FETCH_ERROR_BODY_MAX_CHARS = 512

///|
const CODEX_OAUTH_MODELS_URL = "https://chatgpt.com/backend-api/codex/models?client_version=0.1.0"

///|
pub struct FetchedModel {
  id : String
  owned_by : String?
}

///|
pub fn FetchedModel::to_json(self : FetchedModel) -> Json {
  Json::object({
    "id": Json::string(self.id),
    "ownedBy": match self.owned_by {
      Some(value) => Json::string(value)
      None => Json::null()
    },
  })
}

///|
pub fn fetched_models_to_json(models : Array[FetchedModel]) -> Json {
  Json::array(models.map(fn(model) { model.to_json() }))
}

///|
pub async fn fetch_codex_oauth_models_with_token(
  token : String,
  account_id : String,
) -> Result[Array[FetchedModel], String] {
  if token.trim().is_empty() {
    return Err(
      "Codex OAuth token unavailable: access_token is empty or missing",
    )
  }
  if account_id.trim().is_empty() {
    return Err("No ChatGPT account available")
  }
  let target = match models_url_target(CODEX_OAUTH_MODELS_URL) {
    Ok(target) => target
    Err(err) => return Err(err)
  }
  let headers : Map[String, String] = {
    "Authorization": "Bearer \{token}",
    "Accept": "application/json",
    "originator": "moongate",
    "chatgpt-account-id": account_id,
  }
  let client = @http.Client::Client(target.origin, headers~)
  defer client.close()
  let response = client.get(target.path) catch {
    err => return Err("Request failed: \{err}")
  }
  let body = client.read_all().text()
  if response.code is (200..=299) {
    parse_codex_oauth_models_response_body(body)
  } else {
    Err("HTTP \{response.code}: \{truncate_model_fetch_error_body(body)}")
  }
}

///|
pub async fn fetch_models(
  base_url : String,
  api_key : String,
  is_full_url : Bool,
  models_url_override : String?,
  user_agent : String?,
) -> Result[Array[FetchedModel], String] {
  if api_key.is_empty() {
    return Err("API Key is required to fetch models")
  }
  let candidates = match
    build_models_url_candidates(base_url, is_full_url, models_url_override) {
    Ok(candidates) => candidates
    Err(err) => return Err(err)
  }
  let mut last_err : String? = None
  for url in candidates {
    let target = match models_url_target(url) {
      Ok(target) => target
      Err(err) => return Err(err)
    }
    let headers = model_fetch_headers(api_key, user_agent)
    let client = @http.Client::Client(target.origin, headers~)
    defer client.close()
    let response = client.get(target.path) catch {
      err => return Err("Request failed: \{err}")
    }
    let body = client.read_all().text()
    if response.code is (200..=299) {
      match parse_models_response_body(body) {
        Ok(models) => return Ok(models)
        Err(err) => return Err(err)
      }
    } else if response.code == 404 || response.code == 405 {
      last_err = Some(
        "HTTP \{response.code}: \{truncate_model_fetch_error_body(body)}",
      )
    } else {
      return Err(
        "HTTP \{response.code}: \{truncate_model_fetch_error_body(body)}",
      )
    }
  }
  Err("All candidates failed: \{last_err.unwrap_or("no candidates")}")
}

///|
fn model_fetch_headers(
  api_key : String,
  user_agent : String?,
) -> Map[String, String] {
  let headers : Map[String, String] = {
    "Authorization": "Bearer \{api_key}",
    "Accept": "application/json",
  }
  match parse_custom_user_agent(user_agent) {
    Some(value) => headers["User-Agent"] = value
    None => ()
  }
  headers
}

///|
pub fn build_models_url_candidates(
  base_url : String,
  is_full_url : Bool,
  models_url_override : String?,
) -> Result[Array[String], String] {
  match models_url_override {
    Some(raw) => {
      let trimmed = raw.trim().to_owned()
      if !trimmed.is_empty() {
        return Ok([trimmed])
      }
    }
    None => ()
  }

  let trimmed = trim_end_slashes(base_url.trim().to_owned())
  if trimmed.is_empty() {
    return Err("Base URL is empty")
  }

  let candidates : Array[String] = []
  if is_full_url {
    match trimmed.find("/v1/") {
      Some(index) => candidates.push("\{trimmed[:index].to_owned()}/v1/models")
      None =>
        match trimmed.rev_find("/") {
          Some(index) => {
            let root = trimmed[:index].to_owned()
            if root.contains("://") {
              let scheme = root.find("://").unwrap()
              if root.length() > scheme + 3 {
                candidates.push("\{root}/v1/models")
              }
            }
          }
          None => ()
        }
    }
    if candidates.is_empty() {
      return Err("Cannot derive models endpoint from full URL")
    }
    return Ok(candidates)
  }

  if ends_with_version_segment(trimmed) {
    candidates.push("\{trimmed}/models")
    if !trimmed.has_suffix("/v1") {
      candidates.push("\{trimmed}/v1/models")
    }
  } else {
    candidates.push("\{trimmed}/v1/models")
  }

  Ok(unique_strings(candidates))
}

///|
priv struct ModelsUrlTarget {
  origin : String
  path : String
}

///|
fn models_url_target(url : String) -> Result[ModelsUrlTarget, String] {
  let (raw_origin, raw_path) = split_origin_and_path(url)
  let (origin, raw_path) = if raw_path.is_empty() {
    match raw_origin.find("?") {
      Some(index) =>
        (raw_origin[:index].to_owned(), "/\{raw_origin[index:].to_owned()}")
      None => (raw_origin, raw_path)
    }
  } else {
    (raw_origin, raw_path)
  }
  if !origin.contains("://") {
    return Err("Invalid models endpoint URL: \{url}")
  }
  let path = if raw_path.is_empty() { "/" } else { raw_path }
  Ok({ origin, path })
}

///|
fn parse_models_response_body(
  body : String,
) -> Result[Array[FetchedModel], String] {
  let parsed = @json.parse(body) catch {
    err => return Err("Failed to parse response: \{err}")
  }
  parse_models_response(parsed)
}

///|
fn parse_models_response(value : Json) -> Result[Array[FetchedModel], String] {
  guard value is Object(obj) else {
    return Err("Failed to parse response: expected object")
  }
  let models : Array[FetchedModel] = []
  match obj.get("data") {
    Some(Array(entries)) =>
      for entry in entries {
        match parse_model_entry(entry) {
          Ok(model) => models.push(model)
          Err(err) => return Err(err)
        }
      }
    Some(_) => return Err("Failed to parse response: data must be an array")
    None => ()
  }
  models.sort_by(fn(left, right) { ascii_lex_compare(left.id, right.id) })
  Ok(models)
}

///|
fn parse_model_entry(entry : Json) -> Result[FetchedModel, String] {
  guard entry is Object(obj) else {
    return Err("Failed to parse response: model entry must be an object")
  }
  let id = match obj.get("id") {
    Some(String(value)) => value
    _ => return Err("Failed to parse response: model id is required")
  }
  let owned_by = match obj.get("owned_by") {
    Some(String(value)) => Some(value)
    Some(Null) | None => None
    _ => return Err("Failed to parse response: owned_by must be a string")
  }
  Ok({ id, owned_by })
}

///|
fn parse_codex_oauth_models_response_body(
  body : String,
) -> Result[Array[FetchedModel], String] {
  let parsed = @json.parse(body) catch {
    err => return Err("Failed to parse response: \{err}")
  }
  Ok(parse_codex_oauth_models_response(parsed))
}

///|
fn parse_codex_oauth_models_response(value : Json) -> Array[FetchedModel] {
  let models : Array[FetchedModel] = []
  match value {
    Array(entries) =>
      for entry in entries {
        push_codex_oauth_model_entry(models, entry, None)
      }
    Object(obj) => {
      match first_codex_oauth_models_array(obj) {
        Some(entries) =>
          for entry in entries {
            push_codex_oauth_model_entry(models, entry, None)
          }
        None => ()
      }
      match obj.get("models") {
        Some(Object(model_map)) =>
          for key in model_map.keys() {
            match model_map.get(key) {
              Some(entry) =>
                push_codex_oauth_model_entry(models, entry, Some(key))
              None => ()
            }
          }
        _ => ()
      }
    }
    _ => ()
  }
  models.sort_by(fn(left, right) { ascii_lex_compare(left.id, right.id) })
  dedupe_fetched_models_by_id(models)
}

///|
fn first_codex_oauth_models_array(obj : Map[String, Json]) -> Array[Json]? {
  for key in ["data", "models", "items"] {
    match obj.get(key) {
      Some(Array(entries)) => return Some(entries)
      _ => ()
    }
  }
  None
}

///|
fn push_codex_oauth_model_entry(
  models : Array[FetchedModel],
  entry : Json,
  fallback_id : String?,
) -> Unit {
  match entry {
    String(value) => {
      let id = value.trim().to_owned()
      if !id.is_empty() {
        models.push({ id, owned_by: Some("Codex") })
      }
    }
    Object(obj) => {
      let id = match
        json_string_from_map_any(obj, ["slug", "id", "model", "name"]) {
        Some(value) => Some(value)
        None =>
          match fallback_id {
            Some(value) => Some(value.trim().to_owned())
            None => None
          }
      }
      match id {
        Some(id) if !id.is_empty() => {
          let owned_by = match
            json_string_from_map_any(obj, [
              "owned_by", "ownedBy", "provider", "vendor", "category", "owner",
            ]) {
            Some(value) => Some(value)
            None => Some("Codex")
          }
          models.push({ id, owned_by })
        }
        _ => ()
      }
    }
    _ =>
      match fallback_id {
        Some(value) => {
          let id = value.trim().to_owned()
          if !id.is_empty() {
            models.push({ id, owned_by: Some("Codex") })
          }
        }
        None => ()
      }
  }
}

///|
fn dedupe_fetched_models_by_id(
  models : Array[FetchedModel],
) -> Array[FetchedModel] {
  let unique : Array[FetchedModel] = []
  for model in models {
    let mut seen = false
    for existing in unique {
      if existing.id == model.id {
        seen = true
      }
    }
    if !seen {
      unique.push(model)
    }
  }
  unique
}

///|
fn ends_with_version_segment(url : String) -> Bool {
  let last = match url.rev_find("/") {
    Some(index) => url[index + 1:].to_owned()
    None => url
  }
  if !last.has_prefix("v") || last.length() <= 1 {
    return false
  }
  for c in last[1:] {
    if !c.is_ascii_digit() {
      return false
    }
  }
  true
}

///|
fn unique_strings(values : Array[String]) -> Array[String] {
  let unique : Array[String] = []
  for value in values {
    let mut seen = false
    for existing in unique {
      if existing == value {
        seen = true
      }
    }
    if !seen {
      unique.push(value)
    }
  }
  unique
}

///|
fn truncate_model_fetch_error_body(body : String) -> String {
  if body.length() <= MODEL_FETCH_ERROR_BODY_MAX_CHARS {
    body
  } else {
    body[:MODEL_FETCH_ERROR_BODY_MAX_CHARS].to_owned() + "…"
  }
}