///|
/// Provider-independent HTTP request seam used by OAuth flows.
///
/// The trait is defined here (compiled on every target) together with the
/// `DefaultOAuthHttpTransport` impls that delegate to `moonbitlang/async/http`
/// (native+js only). On wasm/wasm-gc the trait is still available for a host
/// to implement; there is simply no default transport. Tests and hosts may
/// also inject a fake transport to assert exact OAuth wire requests without a
/// network connection.

///|
pub(all) struct OAuthHttpRequest {
  url : String
  body : String
  headers : Map[String, String]
}

///|
pub fn OAuthHttpRequest::OAuthHttpRequest(
  url~ : String,
  body~ : String,
  headers~ : Map[String, String],
) -> OAuthHttpRequest {
  { url, body, headers, }
}

///|
pub(all) struct OAuthHttpResponse {
  status : Int
  body : String
}

///|
pub fn OAuthHttpResponse::OAuthHttpResponse(
  status~ : Int,
  body~ : String,
) -> OAuthHttpResponse {
  { status, body, }
}

///|
/// HTTP POST seam. `DefaultOAuthHttpTransport` provides a native+js impl that
/// calls `moonbitlang/async/http`; wasm/wasm-gc has no default (hosts supply
/// their own transport or OAuth HTTP flows are unavailable on that target).
pub(open) trait OAuthHttpTransport {
  async fn post(Self, request : OAuthHttpRequest) -> OAuthHttpResponse raise OAuthError

  /// GET seam for metadata discovery (RFC 9728 / RFC 8414). The default is
  /// fail-closed so existing post-only impls keep compiling; transports that
  /// can GET must override it.
  async fn get(Self, url : String, headers : Map[String, String]) -> OAuthHttpResponse raise OAuthError = _
}

///|
impl OAuthHttpTransport with fn get(_self, _url, _headers) -> OAuthHttpResponse raise OAuthError {
  raise OAuthError::HttpError("GET not supported by this transport")
}

///|
/// Marker struct for the default `@http`-backed transport. Constructed on any
/// target; its `OAuthHttpTransport` impl is compiled only on native+js.
pub(all) struct DefaultOAuthHttpTransport {
  // empty
}

///|
pub fn DefaultOAuthHttpTransport::DefaultOAuthHttpTransport() -> DefaultOAuthHttpTransport {
  DefaultOAuthHttpTransport::{ }
}

///|
/// Percent-encode one form value using the shared OAuth URL encoder. Providers
/// use this for client ids, device codes, and refresh tokens so secrets cannot
/// change the request shape when they contain reserved characters.
pub fn oauth_form_url_encode(value : String) -> String {
  url_encode(value)
}

///|
pub impl OAuthHttpTransport for DefaultOAuthHttpTransport with fn post(
  _self : DefaultOAuthHttpTransport,
  request : OAuthHttpRequest,
) -> OAuthHttpResponse raise OAuthError {
  // moonbitlang/async's timeout cancels the in-flight HTTP task.  Keep this
  // bound in the provider-independent transport so every OAuth endpoint gets
  // the same fail-fast deadline, including refresh calls.
  @async.with_timeout(30000, () => {
    // async 0.21: @http headers are case-insensitively keyed. The
    // transport-agnostic contract stays Map[String, String] (fakes and
    // providers keep plain keys) and converts at this single @http boundary.
    let http_headers : @http.Headers = Map([])
    for k, v in request.headers {
      http_headers[k] = v
    }
    let (response, body_data) = @http.post(
      request.url,
      request.body,
      headers=http_headers,
    ) catch {
      err =>
        raise OAuthError::HttpError(
          "oauth HTTP POST failed: " + err.to_string(),
        )
    }
    let body = body_data.text() catch {
      err =>
        raise OAuthError::HttpError(
          "oauth HTTP response body read failed: " + err.to_string(),
        )
    }
    { status: response.code, body, }
  }) catch {
    @async.TimeoutError =>
      raise OAuthError::HttpError(
        "oauth HTTP POST timed out (timeout_ms=30000)",
      )
    err =>
      raise OAuthError::HttpError("oauth HTTP POST failed: " + err.to_string())
  }
}

///|
pub extend DefaultOAuthHttpTransport with OAuthHttpTransport::{post, get}

///|
pub impl OAuthHttpTransport for DefaultOAuthHttpTransport with fn get(
  _self : DefaultOAuthHttpTransport,
  url : String,
  headers : Map[String, String],
) -> OAuthHttpResponse raise OAuthError {
  @async.with_timeout(30000, () => {
    let http_headers : @http.Headers = Map([])
    for k, v in headers {
      http_headers[k] = v
    }
    let (response, body_data) = @http.get(url, headers=http_headers) catch {
      err =>
        raise OAuthError::HttpError("oauth HTTP GET failed: " + err.to_string())
    }
    let body = body_data.text() catch {
      err =>
        raise OAuthError::HttpError(
          "oauth HTTP response body read failed: " + err.to_string(),
        )
    }
    { status: response.code, body, }
  }) catch {
    @async.TimeoutError =>
      raise OAuthError::HttpError("oauth HTTP GET timed out (timeout_ms=30000)")
    err =>
      raise OAuthError::HttpError("oauth HTTP GET failed: " + err.to_string())
  }
}