// Response types shared by /search, /contents, /answer and /findSimilar.
//
// Every type here keeps the `raw` JSON it was decoded from. Exa ships new
// response fields regularly, and `raw` means reaching one does not have to wait
// for a release of this library.

///|
/// Extra links scraped out of a page, when `ExtrasOptions` asked for them.
pub(all) struct Extras {
  links : Array[String]
  image_links : Array[String]
} derive(Eq, @debug.Debug)

///|
/// Build one directly, for tests and for code that synthesises results.
pub fn Extras::new(
  links? : Array[String] = [],
  image_links? : Array[String] = [],
) -> Extras {
  { links, image_links, }
}

///|
fn Extras::decode(json : Json) -> Extras {
  {
    links: get_str_array(json, "links"),
    image_links: get_str_array(json, "imageLinks"),
  }
}

///|
/// What the request cost. `raw` holds Exa's full per-component breakdown
/// (`search.neural`, `contents.text`, and so on).
pub(all) struct CostDollars {
  total : Double
  raw : Json
} derive(Eq, @debug.Debug)

///|
/// Build one directly, for tests and for code that synthesises results.
pub fn CostDollars::new(
  total : Double,
  raw? : Json = Json::null(),
) -> CostDollars {
  { total, raw, }
}

///|
fn CostDollars::decode(json : Json) -> CostDollars {
  { total: get_double(json, "total").unwrap_or(0.0), raw: json, }
}

///|
/// One search result, and its contents when they were requested.
///
/// The same shape is returned by `/search`, `/contents`, `/findSimilar` and as
/// `/answer`'s citations, so fields not requested are simply absent.
pub(all) struct SearchResult {
  url : String
  /// The document id to pass to `/contents`. Currently equal to the URL.
  id : String
  title : String?
  published_date : String?
  author : String?
  image : String?
  favicon : String?
  text : String?
  summary : String?
  /// Raw passages from the page: they keep its newlines and can run long.
  highlights : Array[String]
  highlight_scores : Array[Double]
  subpages : Array[SearchResult]
  extras : Extras?
  raw : Json
} derive(Eq, @debug.Debug)

///|
/// Build a result directly, without going through a response body.
///
/// Everything but the URL is optional, so a test for "a result with no title"
/// is one line. `id` defaults to the URL, which is what Exa returns today, and
/// `raw` to null — a hand-built result was not decoded from anything.
pub fn SearchResult::new(
  url : String,
  id? : String,
  title? : String,
  published_date? : String,
  author? : String,
  image? : String,
  favicon? : String,
  text? : String,
  summary? : String,
  highlights? : Array[String] = [],
  highlight_scores? : Array[Double] = [],
  subpages? : Array[SearchResult] = [],
  extras? : Extras,
  raw? : Json = Json::null(),
) -> SearchResult {
  {
    url,
    id: id.unwrap_or(url),
    title,
    published_date,
    author,
    image,
    favicon,
    text,
    summary,
    highlights,
    highlight_scores,
    subpages,
    extras,
    raw,
  }
}

///|
fn SearchResult::decode(
  json : Json,
  ctx : String,
) -> SearchResult raise ExaError {
  let url = get_str_req(json, "url", ctx)
  let subpages = []
  for subpage in get_array(json, "subpages") {
    subpages.push(SearchResult::decode(subpage, ctx + " subpage"))
  }
  {
    url,
    id: get_str(json, "id").unwrap_or(url),
    title: get_str(json, "title"),
    published_date: get_str(json, "publishedDate"),
    author: get_str(json, "author"),
    image: get_str(json, "image"),
    favicon: get_str(json, "favicon"),
    text: get_str(json, "text"),
    summary: get_str(json, "summary"),
    highlights: get_str_array(json, "highlights"),
    highlight_scores: get_double_array(json, "highlightScores"),
    subpages,
    extras: field(json, "extras").map(Extras::decode),
    raw: json,
  }
}

///|
fn decode_results(
  json : Json,
  ctx : String,
) -> Array[SearchResult] raise ExaError {
  let results = []
  for item in get_array(json, "results") {
    results.push(SearchResult::decode(item, ctx))
  }
  results
}

///|
/// A source backing part of a structured output.
pub(all) struct Citation {
  url : String?
  title : String?
} derive(Eq, @debug.Debug)

///|
/// Build one directly, for tests and for code that synthesises results.
pub fn Citation::new(url? : String, title? : String) -> Citation {
  { url, title, }
}

///|
fn Citation::decode(json : Json) -> Citation {
  { url: get_str(json, "url"), title: get_str(json, "title"), }
}

///|
/// Which sources back one field of a structured output, and how confident Exa
/// is in it (`low`, `medium` or `high`).
pub(all) struct Grounding {
  field : String?
  confidence : String?
  citations : Array[Citation]
} derive(Eq, @debug.Debug)

///|
/// Build one directly, for tests and for code that synthesises results.
pub fn Grounding::new(
  field? : String,
  confidence? : String,
  citations? : Array[Citation] = [],
) -> Grounding {
  { field, confidence, citations, }
}

///|
fn Grounding::decode(json : Json) -> Grounding {
  let citations = []
  for item in get_array(json, "citations") {
    citations.push(Citation::decode(item))
  }
  {
    field: get_str(json, "field"),
    confidence: get_str(json, "confidence"),
    citations,
  }
}

///|
/// The synthesised answer returned when a request supplied an `output_schema`.
pub(all) struct SynthesisOutput {
  /// A string when no schema was given, otherwise an object matching it.
  content : Json
  /// `content` when it is a plain string, for the common case.
  content_text : String?
  grounding : Array[Grounding]
  raw : Json
} derive(Eq, @debug.Debug)

///|
/// Build one directly, for tests and for code that synthesises results.
///
/// `content_text` is filled in from `content` when it is a plain string, the
/// same way decoding does it.
pub fn SynthesisOutput::new(
  content? : Json = Json::null(),
  grounding? : Array[Grounding] = [],
  raw? : Json = Json::null(),
) -> SynthesisOutput {
  let content_text = if content is String(text) { Some(text) } else { None }
  { content, content_text, grounding, raw, }
}

///|
fn SynthesisOutput::decode(json : Json) -> SynthesisOutput {
  let grounding = []
  for item in get_array(json, "grounding") {
    grounding.push(Grounding::decode(item))
  }
  let content = field(json, "content").unwrap_or(Json::null())
  { content, content_text: get_str(json, "content"), grounding, raw: json, }
}