///|
/// Structured SDK error type.
///
/// Every variant carries an `operation_id` so that diagnosis context is
/// never discarded.  The `Http` variant additionally carries status, headers,
/// and raw body for programmatic inspection by callers.
///
/// Variant map:
/// - Transport      — DNS, connection, timeout, TLS, or adapter failure;
/// - Http           — non-2xx response (status, headers, raw body);
/// - Decode         — response did not match the declared representation;
/// - Encode         — request serialization failed;
/// - Configuration — invalid base URL or auth configuration;
/// - Unsupported    — rejected OpenAPI semantic surfaced at runtime boundary.
pub(all) suberror SdkError {
  Transport(String, String) // operation_id, message
  Http(
    String, // operation_id
    Int, // status
    Map[String, String], // response headers
    String
  ) // raw body
  Decode(String, String) // operation_id, message
  Encode(String, String) // operation_id, message
  Configuration(String, String) // operation_id, message
  Unsupported(String, String) // operation_id, message
} derive(Debug)

///|
/// Convenience constructor for a transport error.
pub fn SdkError::transport(operation_id : String, message : String) -> SdkError {
  Transport(operation_id, message)
}

///|
/// Convenience constructor for an HTTP error.
pub fn SdkError::http(
  operation_id : String,
  status : Int,
  headers : Map[String, String],
  body : String,
) -> SdkError {
  Http(operation_id, status, headers, body)
}

///|
/// Convenience constructor for a decode error.
pub fn SdkError::decode(operation_id : String, message : String) -> SdkError {
  Decode(operation_id, message)
}

///|
/// Convenience constructor for an encode error.
pub fn SdkError::encode(operation_id : String, message : String) -> SdkError {
  Encode(operation_id, message)
}

///|
/// Convenience constructor for a configuration error.
pub fn SdkError::configuration(
  operation_id : String,
  message : String,
) -> SdkError {
  Configuration(operation_id, message)
}

///|
/// Convenience constructor for an unsupported-feature error.
pub fn SdkError::unsupported(
  operation_id : String,
  message : String,
) -> SdkError {
  Unsupported(operation_id, message)
}

///|
/// The operation id carried by this error, or `""` when unknown.
pub fn SdkError::operation_id(self : SdkError) -> String {
  match self {
    Transport(op, _) => op
    Http(op, _, _, _) => op
    Decode(op, _) => op
    Encode(op, _) => op
    Configuration(op, _) => op
    Unsupported(op, _) => op
  }
}

///|
/// HTTP status code when this is an `Http` error, otherwise `0`.
pub fn SdkError::status(self : SdkError) -> Int {
  match self {
    Http(_, status, _, _) => status
    _ => 0
  }
}

///|
/// Response headers when this is an `Http` error, otherwise an empty map.
pub fn SdkError::headers(self : SdkError) -> Map[String, String] {
  match self {
    Http(_, _, headers, _) => headers
    _ => Map([])
  }
}

///|
/// Raw response body when this is an `Http` error, otherwise `""`.
pub fn SdkError::body(self : SdkError) -> String {
  match self {
    Http(_, _, _, body) => body
    _ => ""
  }
}

///|
/// Human-readable detail message for any variant.
/// For `Http` this is a status summary; for other variants it is the
/// original message string.
pub fn SdkError::message(self : SdkError) -> String {
  match self {
    Transport(_, msg) => msg
    Http(_, status, _, body) => {
      let suffix = if body == "" { "" } else { " body: " + body }
      "HTTP " + status.to_string() + suffix
    }
    Decode(_, msg) => msg
    Encode(_, msg) => msg
    Configuration(_, msg) => msg
    Unsupported(_, msg) => msg
  }
}

///|
/// Stable, human-readable display of an SDK error.
///
/// The output is deterministic for equal inputs and never contains
/// absolute paths, timestamps, or random identifiers.
pub fn SdkError::to_string(self : SdkError) -> String {
  let op_part = match self.operation_id() {
    "" => ""
    id => " (operation: " + id + ")"
  }
  match self {
    Transport(_, _) => "transport error" + op_part + ": " + self.message()
    Http(_, _, _, _) => "http error" + op_part + ": " + self.message()
    Decode(_, _) => "decode error" + op_part + ": " + self.message()
    Encode(_, _) => "encode error" + op_part + ": " + self.message()
    Configuration(_, _) =>
      "configuration error" + op_part + ": " + self.message()
    Unsupported(_, _) => "unsupported feature" + op_part + ": " + self.message()
  }
}

///|
/// A transport-neutral request.
pub struct Request {
  http_method : String
  path : String
  query : Array[(String, String)]
  headers : Map[String, String]
  body : String?
} derive(Debug)

///|
pub fn Request::new(
  http_method : String,
  path : String,
  query? : Array[(String, String)] = [],
  headers? : Map[String, String] = Map([]),
  body? : String,
) -> Request {
  { http_method, path, query, headers, body }
}

///|
pub fn Request::http_method(self : Request) -> String {
  self.http_method
}

///|
pub fn Request::path(self : Request) -> String {
  self.path
}

///|
pub fn Request::query(self : Request) -> Array[(String, String)] {
  self.query
}

///|
pub fn Request::headers(self : Request) -> Map[String, String] {
  self.headers
}

///|
pub fn Request::body(self : Request) -> String? {
  self.body
}

///|
/// A transport-neutral response.
pub struct Response {
  status : Int
  headers : Map[String, String]
  body : String
} derive(Debug)

///|
pub fn Response::new(
  status : Int,
  headers? : Map[String, String] = Map([]),
  body? : String = "",
) -> Response {
  { status, headers, body }
}

///|
pub fn Response::status(self : Response) -> Int {
  self.status
}

///|
pub fn Response::headers(self : Response) -> Map[String, String] {
  self.headers
}

///|
pub fn Response::body(self : Response) -> String {
  self.body
}

///|
/// The only boundary generated operations use to perform I/O.
pub trait Transport {
  fn send(Self, Request) -> Response raise SdkError
}

///|
pub fn[T : Transport] send(
  transport : T,
  request : Request,
) -> Response raise SdkError {
  transport.send(request)
}

///|
/// In-memory transport useful for deterministic unit tests.
pub struct CaptureTransport {
  response : Response
  mut last_request : Request?
} derive(Debug)

///|
pub fn CaptureTransport::new(response : Response) -> CaptureTransport {
  { response, last_request: None }
}

///|
pub fn CaptureTransport::send(
  self : CaptureTransport,
  request : Request,
) -> Response {
  self.last_request = Some(request)
  self.response
}

///|
/// The last request that was sent through this transport, if any.
pub fn CaptureTransport::last_request(self : CaptureTransport) -> Request? {
  self.last_request
}

///|
pub fn[T : ToJson] encode_json(value : T) -> String {
  value.to_json().stringify()
}

///|
/// Return the response `Content-Type`, accepting the two common header
/// capitalizations. HTTP header names are case-insensitive.
fn response_content_type(response : Response) -> String? {
  match response.headers.get("Content-Type") {
    Some(value) => Some(value)
    None => response.headers.get("content-type")
  }
}

///|
/// Whether a response media type is a JSON representation.
///
/// Parameters such as `charset=utf-8` are ignored. Both `application/json`
/// and structured suffixes such as `application/problem+json` are accepted.
fn is_json_media_type(value : String) -> Bool {
  let lower = value.to_lower()
  let media_type = match lower.find(";") {
    Some(index) => lower[:index].trim().to_owned()
    None => lower
  }
  media_type == "application/json" || media_type.has_suffix("+json")
}

///|
/// Decode a JSON response body into the target type.
///
/// A declared JSON operation that receives an unexpected response media type
/// fails explicitly instead of attempting a known-wrong decode. A missing
/// `Content-Type` is tolerated for transport-neutral capture fixtures.
///
/// `operation_id` is included in any raised error so that callers can
/// programmatically identify which operation failed.
pub fn[T : FromJson] decode_json(
  response : Response,
  operation_id? : String = "",
) -> T raise SdkError {
  match response_content_type(response) {
    Some(media_type) =>
      if !is_json_media_type(media_type) {
        raise Unsupported(
          operation_id,
          "unsupported response media type: " + media_type,
        )
      }
    None => ()
  }
  let json : Json = @json.parse(response.body) catch {
    err =>
      raise Decode(operation_id, "failed to parse JSON: " + err.to_string())
  }
  @json.from_json(json) catch {
    err => raise Decode(operation_id, err.to_string())
  }
}

///|
/// Assert that the response status is one of the expected values.
///
/// On failure, raises `Http` carrying the full status, headers, and raw
/// body so callers can programmatically inspect the error response.
pub fn expect_status(
  response : Response,
  expected : Array[Int],
  operation_id? : String = "",
) -> Unit raise SdkError {
  if !expected.contains(response.status) {
    raise Http(operation_id, response.status, response.headers, response.body)
  }
}

///|
pub impl Transport for CaptureTransport with fn send(
  self : CaptureTransport,
  request : Request,
) -> Response {
  self.last_request = Some(request)
  self.response
}

///|
/// The standard Base64 alphabet (RFC 4648).
let base64_chars : Array[Char] = [
  'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
  'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
  'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
  'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/',
]

///|
/// Encode a UTF-8 string as Base64 (RFC 4648, padded).
///
/// Basic authentication needs this, and the runtime must not depend on a
/// platform codec for something this small and this well specified.
fn base64_encode(input : String) -> String {
  let bytes = @utf8.encode(input)
  let out = StringBuilder()
  let mut i = 0
  while i < bytes.length() {
    let b0 = bytes[i].to_int() & 0xFF
    let b1 = if i + 1 < bytes.length() {
      bytes[i + 1].to_int() & 0xFF
    } else {
      0
    }
    let b2 = if i + 2 < bytes.length() {
      bytes[i + 2].to_int() & 0xFF
    } else {
      0
    }
    let triple = (b0 << 16) | (b1 << 8) | b2
    out.write_char(base64_chars[(triple >> 18) & 0x3F])
    out.write_char(base64_chars[(triple >> 12) & 0x3F])
    if i + 1 < bytes.length() {
      out.write_char(base64_chars[(triple >> 6) & 0x3F])
    } else {
      out.write_char('=')
    }
    if i + 2 < bytes.length() {
      out.write_char(base64_chars[triple & 0x3F])
    } else {
      out.write_char('=')
    }
    i = i + 3
  }
  out.to_string()
}

///|
/// Build auth headers for the given security requirements.
/// Does not perform I/O; pure map construction.
///
/// Supports the V1 schemes `bearer`, `basic`, and `apiKey`. An `apiKey`
/// carried in the query string contributes no header; `auth_query` supplies it
/// instead.
///
/// `operation_id` is included in any raised `Configuration` error: a missing
/// or unusable credential is a configuration fault, not a transport fault.
pub fn auth_headers(
  cfg : Config,
  requirements : Array[String],
  operation_id? : String = "",
) -> Map[String, String] raise SdkError {
  let headers : Map[String, String] = Map([])
  for requirement in requirements {
    match requirement {
      "bearer" => {
        let token = match cfg.bearer_token() {
          Some(t) => t
          None =>
            raise Configuration(
              operation_id,
              "missing bearer token for " + requirement,
            )
        }
        headers["Authorization"] = "Bearer " + token
      }
      "basic" => {
        let username = match cfg.basic_username() {
          Some(u) => u
          None =>
            raise Configuration(
              operation_id,
              "missing basic auth username for " + requirement,
            )
        }
        let password = match cfg.basic_password() {
          Some(p) => p
          None =>
            raise Configuration(
              operation_id,
              "missing basic auth password for " + requirement,
            )
        }
        headers["Authorization"] = "Basic " +
          base64_encode(username + ":" + password)
      }
      "apiKey" => {
        let location = match cfg.api_key_location() {
          Some(l) => l
          None =>
            raise Configuration(
              operation_id,
              "missing apiKey location for " + requirement,
            )
        }
        if location == "header" {
          let key_name = match cfg.api_key_name() {
            Some(n) => n
            None =>
              raise Configuration(
                operation_id,
                "missing apiKey name for " + requirement,
              )
          }
          let key_value = match cfg.api_key_value() {
            Some(v) => v
            None =>
              raise Configuration(
                operation_id,
                "missing apiKey value for " + requirement,
              )
          }
          headers[key_name] = key_value
        }
      }
      _ =>
        raise Configuration(
          operation_id,
          "unsupported auth scheme: " + requirement,
        )
    }
  }
  headers
}

///|
/// Build the query parameters an in-query API key contributes.
///
/// Returns an empty array for every other scheme so a caller can append the
/// result unconditionally.
pub fn auth_query(
  cfg : Config,
  requirements : Array[String],
  operation_id? : String = "",
) -> Array[(String, String)] raise SdkError {
  let params : Array[(String, String)] = []
  for requirement in requirements {
    if requirement == "apiKey" {
      let location = match cfg.api_key_location() {
        Some(l) => l
        None =>
          raise Configuration(
            operation_id,
            "missing apiKey location for " + requirement,
          )
      }
      if location == "query" {
        let key_name = match cfg.api_key_name() {
          Some(n) => n
          None =>
            raise Configuration(
              operation_id,
              "missing apiKey name for " + requirement,
            )
        }
        let key_value = match cfg.api_key_value() {
          Some(v) => v
          None =>
            raise Configuration(
              operation_id,
              "missing apiKey value for " + requirement,
            )
        }
        params.push((key_name, key_value))
      }
    }
  }
  params
}