///|
pub(all) suberror StacError {
  Invalid(String)
} derive(Debug)

///|
pub(all) struct Asset {
  key : String
  href : String
} derive(Debug, Eq)

///|
pub(all) struct Item {
  id : String
  collection : String?
  datetime : String?
  geometry : Json
  bbox : Json?
  properties : Json
  assets : Array[Asset]
} derive(Debug, Eq)

///|
pub(all) struct NextLink {
  href : String
  http_method : String
  body : Json?
  headers : Json?
  merge : Bool
} derive(Debug, Eq)

///|
pub(all) struct ItemPage {
  items : Array[Item]
  next : NextLink?
} derive(Debug, Eq)

///|
fn field(v : Json, name : String) -> Json? {
  match v {
    Object(fields) => fields.get(name)
    _ => None
  }
}

///|
fn string_field(v : Json, name : String) -> String raise StacError {
  match field(v, name) {
    Some(String(s)) if s != "" => s
    _ => raise Invalid("invalid " + name)
  }
}

///|
fn optional_string(v : Json, name : String) -> String? raise StacError {
  match field(v, name) {
    None | Some(Null) => None
    Some(String(s)) => Some(s)
    _ => raise Invalid("invalid " + name)
  }
}

///|
pub fn parse_item_json(v : Json) -> Item raise StacError {
  let version = string_field(v, "stac_version")
  if version != "1.0.0" && version != "1.1.0" {
    raise Invalid("unsupported stac_version")
  }
  if string_field(v, "type") != "Feature" {
    raise Invalid("expected Feature")
  }
  let id = string_field(v, "id")
  let collection = optional_string(v, "collection")
  validate_item_shape(v, collection)
  let properties = match field(v, "properties") {
    Some(Object(m)) => Json::object(m)
    _ => raise Invalid("invalid properties")
  }
  let datetime = optional_string(properties, "datetime")
  let geometry = match field(v, "geometry") {
    Some(value) => value
    None => raise Invalid("missing geometry")
  }
  let bbox = field(v, "bbox")
  let entries = match field(v, "assets") {
    Some(Object(m)) => m
    _ => raise Invalid("invalid assets")
  }
  let assets : Array[Asset] = []
  for key, data in entries {
    assets.push({ key, href: string_field(data, "href") })
  }
  { id, collection, datetime, geometry, bbox, properties, assets }
}

///|
pub fn parse_item(text : String) -> Item raise StacError {
  let v = @json.parse(text) catch { _ => raise Invalid("invalid JSON") }
  parse_item_json(v)
}

///|
pub fn parse_page(text : String) -> ItemPage raise StacError {
  let v = @json.parse(text) catch { _ => raise Invalid("invalid JSON") }
  if string_field(v, "type") != "FeatureCollection" {
    raise Invalid("expected FeatureCollection")
  }
  let features = match field(v, "features") {
    Some(Array(xs)) => xs
    _ => raise Invalid("invalid features")
  }
  let items : Array[Item] = []
  for feature in features {
    items.push(parse_item_json(feature))
  }
  ignore(validate_links(v, "page"))
  let mut next : NextLink? = None
  match field(v, "links") {
    Some(Array(links)) =>
      for link in links {
        if optional_string(link, "rel") == Some("next") {
          if next != None {
            raise Invalid("multiple next links")
          }
          let http_method = optional_string(link, "method").unwrap_or("GET")
          if http_method != "GET" && http_method != "POST" {
            raise Invalid("unsupported next method")
          }
          let body = field(link, "body")
          match body {
            None | Some(Object(_)) => ()
            _ => raise Invalid("invalid next body")
          }
          let headers = field(link, "headers")
          match headers {
            None | Some(Object(_)) => ()
            _ => raise Invalid("invalid next headers")
          }
          let merge = match field(link, "merge") {
            None | Some(False) => false
            Some(True) => true
            _ => raise Invalid("invalid next merge")
          }
          next = Some({
            href: string_field(link, "href"),
            http_method,
            body,
            headers,
            merge,
          })
        }
      }
    None => ()
    _ => raise Invalid("invalid links")
  }
  { items, next }
}

///|
pub fn search_body(
  bbox : Array[Double],
  datetime : String?,
  collections : Array[String],
  limit : Int,
) -> String raise StacError {
  if limit < 1 || limit > 1000 {
    raise Invalid("invalid search limit")
  }
  validate_search_bbox(bbox)
  let nums : Array[Json] = []
  for n in bbox {
    nums.push(Json::number(n))
  }
  let names : Array[Json] = []
  for name in collections {
    names.push(Json::string(name))
  }
  let out : Map[String, Json] = Map([])
  out["bbox"] = Json::array(nums)
  out["limit"] = Json::number(limit.to_double())
  if names.length() > 0 {
    out["collections"] = Json::array(names)
  }
  match datetime {
    Some(s) if s != "" => out["datetime"] = Json::string(s)
    _ => ()
  }
  Json::object(out).stringify()
}

///|
pub fn search_request(input : String) -> String {
  search_request_checked(input) catch {
    Invalid(message) => error_json(message)
  }
}

///|
fn search_request_checked(input : String) -> String raise StacError {
  let v = @json.parse(input) catch { _ => raise Invalid("invalid JSON") }
  let out : Map[String, Json] = Map([])
  match field(v, "bbox") {
    None => ()
    Some(Array(raw_bbox)) => {
      let bbox : Array[Double] = []
      for value in raw_bbox {
        match value {
          Number(n, ..) => bbox.push(n)
          _ => raise Invalid("invalid bbox value")
        }
      }
      validate_search_bbox(bbox)
      out["bbox"] = Json::array(raw_bbox)
    }
    _ => raise Invalid("invalid bbox")
  }
  let collections : Array[String] = []
  match field(v, "collections") {
    None => ()
    Some(Array(xs)) =>
      for value in xs {
        match value {
          String(name) if name != "" => collections.push(name)
          _ => raise Invalid("invalid collection")
        }
      }
    _ => raise Invalid("invalid collections")
  }
  if collections.length() > 0 {
    let names : Array[Json] = []
    for name in collections {
      names.push(Json::string(name))
    }
    out["collections"] = Json::array(names)
  }
  match field(v, "ids") {
    None => ()
    Some(Array(values)) => {
      let ids : Array[Json] = []
      for value in values {
        match value {
          String(id) if id != "" => ids.push(Json::string(id))
          _ => raise Invalid("invalid item id")
        }
      }
      if ids.length() == 0 {
        raise Invalid("empty ids")
      }
      out["ids"] = Json::array(ids)
    }
    _ => raise Invalid("invalid ids")
  }
  match field(v, "filter") {
    None => ()
    Some(filter) => {
      validate_cql2_filter(filter, 0)
      out["filter"] = filter
      out["filter-lang"] = Json::string("cql2-json")
    }
  }
  let limit = match field(v, "limit") {
    Some(Number(n, ..)) => {
      let parsed = n.to_int()
      if parsed.to_double() != n {
        raise Invalid("invalid limit")
      }
      parsed
    }
    _ => 10
  }
  if limit < 1 || limit > 1000 {
    raise Invalid("invalid limit")
  }
  out["limit"] = Json::number(limit.to_double())
  match optional_string(v, "datetime") {
    None => ()
    Some(value) if value != "" => out["datetime"] = Json::string(value)
    _ => raise Invalid("invalid datetime")
  }
  let body = Json::object(out).stringify()
  let output : Json = { "status": "ok", "body": body }
  output.stringify()
}

///|
fn validate_search_bbox(bbox : Array[Double]) -> Unit raise StacError {
  if bbox.length() != 4 {
    raise Invalid("invalid bbox length")
  }
  if bbox[0] > bbox[2] ||
    bbox[1] > bbox[3] ||
    bbox[0] < -180.0 ||
    bbox[2] > 180.0 ||
    bbox[1] < -90.0 ||
    bbox[3] > 90.0 {
    raise Invalid("invalid bbox")
  }
}

///|
fn error_json(message : String) -> String {
  let output : Json = { "status": "invalid", "message": message }
  output.stringify()
}

///|
pub fn page_summary(input : String) -> String {
  page_summary_checked(input) catch {
    Invalid(message) => error_json(message)
  }
}

///|
fn page_summary_checked(input : String) -> String raise StacError {
  let page = parse_page(input)
  let items : Array[Json] = []
  for item in page.items {
    let assets : Array[Json] = []
    for asset in item.assets {
      let entry : Json = { "key": asset.key, "href": asset.href }
      assets.push(entry)
    }
    let entry : Json = {
      "id": item.id,
      "collection": match item.collection {
        Some(s) => Json::string(s)
        None => Json::null()
      },
      "datetime": match item.datetime {
        Some(s) => Json::string(s)
        None => Json::null()
      },
      "geometry": item.geometry,
      "bbox": match item.bbox {
        Some(v) => v
        None => Json::null()
      },
      "properties": item.properties,
      "assets": Json::array(assets),
    }
    items.push(entry)
  }
  let next : Json = match page.next {
    None => Json::null()
    Some(link) =>
      {
        "href": link.href,
        "method": link.http_method,
        "body": match link.body {
          Some(v) => v
          None => Json::null()
        },
        "headers": match link.headers {
          Some(v) => v
          None => Json::null()
        },
        "merge": link.merge,
      }
  }
  let output : Json = {
    "status": "ok",
    "items": Json::array(items),
    "next": next,
  }
  output.stringify()
}