// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
/// Supported HTTP protocols
#derive(@debug.Debug, Eq, Compare, Hash, @json.ToJson)
pub(all) enum Protocol {
  Http
  Https
}

///|
/// The default port of a HTTP protocol
pub fn Protocol::default_port(p : Protocol) -> Int32 {
  match p {
    Http => 80
    Https => 443
  }
}

///|
/// Error raised when an invalid URI is specified
#derive(@debug.Debug, @json.ToJson)
pub suberror UriParseError {
  InvalidFormat
  UnsupportedProtocol(String)
}

///|
fn resolve_url(uri : StringView) -> (Protocol, String, StringView) raise {
  guard uri.find("://") is Some(protocol_len) else { raise InvalidFormat }
  let protocol = match uri[:protocol_len] {
    "http" => Http
    "https" => Https
    protocol => raise UnsupportedProtocol(protocol.to_owned())
  }
  let uri = uri[protocol_len + 3:]
  let (host, path) = if uri.find("/") is Some(i) {
    uri.split_at(i)
  } else {
    (uri, "/")
  }
  (protocol, host.to_owned(), path)
}

///|
#cfg(any(target="native", target="wasm"))
fn resolve_host(
  host : StringView,
  protocol~ : Protocol,
) -> (StringView, Int32) raise {
  if host =~ (
      re"^" +
      (re".*" as host) +
      re":" +
      (re"[0-9]+" as port_str) +
      re"$"
    ) {
    let port = @string.parse_int(port_str) catch { _ => raise InvalidFormat }
    guard port is (1..<65536) else { raise InvalidFormat }
    (host, port)
  } else {
    (host, protocol.default_port())
  }
}

///|
/// Perform a generic HTTP request to `uri`. Return the connection assigned for this request.
/// The caller should first write request body to the connection through the `@io.BinaryWriter` interface
/// (or through the `@io.TextWriter` interface using the builtin `.text` view),
/// then call `.end_request()` to finish the request and obtain server response,
/// and finally read response message body through the `@io.BinaryReader` interface
/// (or through the `@io.TextReader` interface using the builtin `.text` view) on the connection.
/// 
/// The returned connection must be closed after finishing the request.
///
/// For requests with short message body,
/// `@http.request_binary`/`@http.request_text`/`@http.request_json` can be used to
/// automatically collect request/response body.
///
/// For common request methods, `@http.get`, `@http.post` etc. can be used instead.
pub async fn request(
  meth : RequestMethod,
  uri : StringView,
  headers~ : Headers = {},
  proxy? : ClientConnection,
) -> ClientConnection {
  let (protocol, host, path) = resolve_url(uri)
  let client = ClientConnection::connect(host, headers~, protocol~, proxy?)
  errdefer client.close()
  client.request(meth, path)
  client
}

///|
/// Perform a single HTTP request to `uri` with binary payload/response.
/// Supported protocols are `http://` and `https://`.
/// The HTTP response message and the whole response body will be returned.
///
/// `proxy`, if present, specifies the proxy to use for this request.
/// See `@http.ClientConnection(..)` for more details.
/// `proxy` is not supported on JavaScript backend.
///
/// See `ClientConnection::request` for more details.
pub async fn request_binary(
  meth : RequestMethod,
  uri : StringView,
  body : BytesView,
  headers~ : Headers = {},
  proxy? : ClientConnection,
) -> (ResponseHeader, Bytes) {
  let conn = request(meth, uri, headers~, proxy?)
  defer conn.close()
  let response = conn..write(body).end_request()
  (response, conn.read_all())
}

///|
/// Perform a single HTTP request to `uri` with UTF-8 encoded payload/response.
/// Supported protocols are `http://` and `https://`.
/// The HTTP response message and the whole response body will be returned.
///
/// `proxy`, if present, specifies the proxy to use for this request.
/// See `@http.ClientConnection(..)` for more details.
/// `proxy` is not supported on JavaScript backend.
///
/// See `ClientConnection::request` for more details.
pub async fn request_text(
  meth : RequestMethod,
  uri : StringView,
  body : StringView,
  headers~ : Headers,
  proxy? : ClientConnection,
) -> (ResponseHeader, String) {
  let conn = request(meth, uri, headers~, proxy?)
  defer conn.close()
  conn.text.write(body)
  let response = conn.end_request()
  (response, conn.text.read_all())
}

///|
/// Perform a single HTTP request to `uri` with UTF-8 encoded JSON payload/response.
/// Supported protocols are `http://` and `https://`.
/// The HTTP response message and the whole response body will be returned.
///
/// `proxy`, if present, specifies the proxy to use for this request.
/// See `@http.ClientConnection(..)` for more details.
/// `proxy` is not supported on JavaScript backend.
///
/// See `ClientConnection::request` for more details.
pub async fn request_json(
  meth : RequestMethod,
  uri : StringView,
  body : @json.Json?,
  headers~ : Headers,
  proxy? : ClientConnection,
) -> (ResponseHeader, @json.Json) {
  let conn = request(meth, uri, headers~, proxy?)
  defer conn.close()
  if body is Some(body) {
    conn.text.write(body.stringify())
  }
  let response = conn.end_request()
  (response, @json.parse(conn.text.read_all()))
}

///|
/// Perform a HTTP `GET` request to `uri` with binary response.
/// Supported protocols are `http://` and `https://`.
/// The HTTP response message and the whole response body will be returned.
///
/// `proxy`, if present, specifies the proxy to use for this request.
/// See `@http.ClientConnection::new` for more details.
/// `proxy` is not supported on JavaScript backend.
///
/// See `ClientConnection::request` for more details.
pub async fn get_binary(
  uri : StringView,
  headers~ : Headers = Map([]),
  proxy? : ClientConnection,
) -> (ResponseHeader, Bytes) {
  request_binary(Get, uri, b"", headers~, proxy?)
}

///|
/// Perform a HTTP `GET` request to `uri` with UTF-8 encoded response.
/// Supported protocols are `http://` and `https://`.
/// The HTTP response message and the whole response body will be returned.
///
/// `proxy`, if present, specifies the proxy to use for this request.
/// See `@http.ClientConnection::new` for more details.
/// `proxy` is not supported on JavaScript backend.
///
/// See `ClientConnection::request` for more details.
pub async fn get_text(
  uri : StringView,
  headers~ : Headers = Map([]),
  proxy? : ClientConnection,
) -> (ResponseHeader, String) {
  request_text(Get, uri, "", headers~, proxy?)
}

///|
/// Perform a HTTP `GET` request to `uri` with UTF-8 encoded JSON response.
/// Supported protocols are `http://` and `https://`.
/// The HTTP response message and the whole response body will be returned.
///
/// `proxy`, if present, specifies the proxy to use for this request.
/// See `@http.ClientConnection::new` for more details.
/// `proxy` is not supported on JavaScript backend.
///
/// See `ClientConnection::request` for more details.
pub async fn get_json(
  uri : StringView,
  headers~ : Headers = Map([]),
  proxy? : ClientConnection,
) -> (ResponseHeader, @json.Json) {
  request_json(Get, uri, None, headers~, proxy?)
}

///|
/// Similar to `get_binary`, but performs a `PUT` request instead.
pub async fn put_binary(
  uri : StringView,
  content : BytesView,
  headers~ : Headers = Map([]),
  proxy? : ClientConnection,
) -> (ResponseHeader, Bytes) {
  request_binary(Put, uri, content, headers~, proxy?)
}

///|
/// Similar to `get_text`, but performs a `PUT` request instead.
pub async fn put_text(
  uri : StringView,
  content : StringView,
  headers~ : Headers = Map([]),
  proxy? : ClientConnection,
) -> (ResponseHeader, String) {
  request_text(Put, uri, content, headers~, proxy?)
}

///|
/// Similar to `get_json`, but performs a `PUT` request instead.
pub async fn put_json(
  uri : StringView,
  content : @json.Json,
  headers~ : Headers = Map([]),
  proxy? : ClientConnection,
) -> (ResponseHeader, @json.Json) {
  request_json(Put, uri, Some(content), headers~, proxy?)
}

///|
/// Similar to `get_binary`, but performs a `POST` request instead.
pub async fn post_binary(
  uri : StringView,
  content : BytesView,
  headers~ : Headers = Map([]),
  proxy? : ClientConnection,
) -> (ResponseHeader, Bytes) {
  request_binary(Post, uri, content, headers~, proxy?)
}

///|
/// Similar to `get_text`, but performs a `POST` request instead.
pub async fn post_text(
  uri : StringView,
  content : StringView,
  headers~ : Headers = Map([]),
  proxy? : ClientConnection,
) -> (ResponseHeader, String) {
  request_text(Post, uri, content, headers~, proxy?)
}

///|
/// Similar to `get_json`, but performs a `POST` request instead.
pub async fn post_json(
  uri : StringView,
  content : @json.Json,
  headers~ : Headers = Map([]),
  proxy? : ClientConnection,
) -> (ResponseHeader, @json.Json) {
  request_json(Post, uri, Some(content), headers~, proxy?)
}

///|
/// Performs a `GET` request to the target URL.
/// Return the response header and the connection assigned for this request.
/// Response body can be obtained by reading from the connection via the `@io.BinaryReader` interface,
/// or through the `@io.TextReader` interface using the builtin `.text` view on the connection.
/// For requests with simple, short response message,
/// `@http.get_binary`/`@http.get_text`/`@http.get_json` can be used to collect the body automatically.
///
/// The returned connection must be manually closed via `.close()`
/// after the caller no longer need to use it.
pub async fn get(
  uri : StringView,
  headers~ : Headers = Map([]),
  proxy? : ClientConnection,
) -> (ResponseHeader, ClientConnection) {
  let conn = request(Get, uri, headers~, proxy?)
  errdefer conn.close()
  (conn.end_request(), conn)
}

///|
/// Performs a `PUT` request to the target URL.
/// Return the connection assigned for this request.
/// Request body can be written through the `@io.BinaryWriter` interface on the connection,
/// or through the `@io.TextWriter` interface using the builtin `.text` view on the connection.
///
/// When the request body is completely written, `end_request()` should be called on the connection
/// to terminate the request and obtain response header from the server.
/// After receiving the response header,
/// response body can be obtained by reading from the connection via the `@io.BinaryReader` interface,
/// or through the `@io.TextReader` interface using the builtin `.text` view on the connection.
///
/// For requests with simple, short message body,
/// `@http.put_binary`/`@http.put_text`/`@http.put_json` can be used to collect the body automatically.
///
/// The returned connection must be manually closed via `.close()`
/// after the caller no longer need to use it.
pub async fn put(
  uri : StringView,
  headers~ : Headers = Map([]),
  proxy? : ClientConnection,
) -> ClientConnection {
  request(Put, uri, headers~, proxy?)
}

///|
/// Performs a `POST` request to the target URL.
/// Return the connection assigned for this request.
/// Request body can be written through the `@io.BinaryWriter` interface on the connection,
/// or through the `@io.TextWriter` interface using the builtin `.text` view on the connection.
///
/// When the request body is completely written, `end_request()` should be called on the connection
/// to terminate the request and obtain response header from the server.
/// After receiving the response header,
/// response body can be obtained by reading from the connection via the `@io.BinaryReader` interface,
/// or through the `@io.TextReader` interface using the builtin `.text` view on the connection.
///
/// For requests with simple, short message body,
/// `@http.post_binary`/`@http.post_text`/`@http.post_json` can be used to collect the body automatically.
///
/// The returned connection must be manually closed via `.close()`
/// after the caller no longer need to use it.
pub async fn post(
  uri : StringView,
  headers~ : Headers = Map([]),
  proxy? : ClientConnection,
) -> ClientConnection {
  request(Post, uri, headers~, proxy?)
}