// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// What moonfetch itself refuses. Errors from below pass through as they are: a socket that
/// will not connect, a TLS alert, a response that does not parse.
pub(all) suberror Refused {
  /// `checked` on a 4xx or 5xx response.
  Status(status~ : Int32, url~ : String)
  /// More redirects than allowed.
  Redirects(limit~ : Int32)
  /// The server reset the stream, or went away before answering it, with an HTTP/2 error code
  /// (RFC 9113 ยง7).
  Reset(code~ : Int32)
  /// The exchange, redirects included, took longer than `limit` milliseconds.
  Timeout(limit~ : Int32)
  /// A URL that does not parse, or names a scheme other than http and https.
  Url(String)
} derive(Eq, Debug)

///|
pub extend Refused with Eq::{equal, not_equal}

///|
pub extend Refused with Debug::{to_repr}

///|
pub impl Show for Refused with fn output(self, logger) {
  match self {
    Status(status~, url~) => logger.write_string("\{url} answered \{status}")
    Redirects(limit~) => logger.write_string("more than \{limit} redirects")
    Reset(code~) =>
      logger.write_string("stream reset with HTTP/2 error \{code}")
    Timeout(limit~) => logger.write_string("no answer within \{limit} ms")
    Url(why) => logger.write_string("bad URL: \{why}")
  }
}

///|
pub extend Refused with Show::{output, to_string}

///|
/// A response, read whole. Header names are lower case, as HTTP/2 writes them, and keep the
/// order they arrived in.
pub(all) struct Response {
  status : Int32
  reason : String
  version : String
  headers : Array[(String, String)]
  /// The URL that answered, after any redirects.
  url : String
  body : Bytes
} derive(Eq, Debug)

///|
pub extend Response with Eq::{equal, not_equal}

///|
pub extend Response with Debug::{to_repr}

///|
/// The first value of the header `name`, matched without regard to case.
pub fn Response::header(self : Response, name : StringView) -> String? {
  let want = name.to_owned().to_lower()
  for pair in self.headers {
    if pair.0 == want {
      return Some(pair.1)
    }
  }
  None
}

///|
/// Every value of the header `name`, in order: what `set-cookie` needs.
pub fn Response::values(self : Response, name : StringView) -> Array[String] {
  let want = name.to_owned().to_lower()
  self.headers.filter(pair => pair.0 == want).map(pair => pair.1)
}

///|
/// The body as UTF-8 text; a malformed sequence reads as U+FFFD.
pub fn Response::text(self : Response) -> String {
  @utf8.decode_lossy(self.body[:])
}

///|
/// The body parsed as JSON.
pub fn Response::json(self : Response) -> Json raise @json.ParseError {
  @json.parse(self.text())
}

///|
/// Whether the status is 2xx.
pub fn Response::is_success(self : Response) -> Bool {
  self.status is (200..<300)
}

///|
/// The response itself, or `Status` for a 4xx or 5xx, as reqwest's `error_for_status` does.
pub fn Response::checked(self : Response) -> Response raise Refused {
  if self.status is (400..<600) {
    raise Status(status=self.status, url=self.url)
  }
  self
}