// Copyright 2025 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.

///|
pub(all) enum Protocol {
  Http
  Https
} derive(Debug, Eq, Compare, Hash, ToJson)

///|
pub fn Protocol::default_port(p : Protocol) -> Int {
  match p {
    Http => 80
    Https => 443
  }
}

///|
pub suberror URIParseError {
  InvalidFormat
  UnsupportedProtocol(String)
} derive(Debug, ToJson)

///|
fn resolve_url(uri : String) -> (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[:i], uri[i:])
  } else {
    (uri, "/")
  }
  let path : StringView = if path == "" { "/" } else { path }
  (protocol, host.to_owned(), path)
}

///|
#cfg(any(target="native", target="wasm"))
fn resolve_host(
  host : StringView,
  protocol~ : Protocol,
) -> (StringView, Int) 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 single HTTP request to `uri`.
/// 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.Client::new` for more details.
/// `proxy` is not supported on JavaScript backend.
///
/// See `Client::request` for more details.
pub async fn request(
  uri : String,
  meth : RequestMethod,
  headers : Map[String, String],
  body : &@io.Data,
  proxy? : Client,
) -> (Response, &@io.Data) {
  let (protocol, host, path) = resolve_url(uri)
  let client = Client::connect(host, headers~, protocol~, proxy?)
  defer client.close()
  let response = client..request(meth, path)..write(body).end_request()
  (response, client.read_all())
}

///|
/// Perform a HTTP `GET` request to `uri`.
/// 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.Client::new` for more details.
/// `proxy` is not supported on JavaScript backend.
///
/// See `Client::request` for more details.
#label_migration(body, fill=false, msg="`GET` request should not contain a body")
pub async fn get(
  uri : String,
  headers? : Map[String, String] = Map([]),
  body? : &@io.Data = b"",
  proxy? : Client,
) -> (Response, &@io.Data) {
  request(uri, Get, headers, body, proxy?)
}

///|
/// Similar to `get`, but performs a `PUT` request instead.
pub async fn put(
  uri : String,
  content : &@io.Data,
  headers? : Map[String, String] = Map([]),
  proxy? : Client,
) -> (Response, &@io.Data) {
  request(uri, Put, headers, content, proxy?)
}

///|
/// Similar to `get`, but performs a `POST` request instead.
pub async fn post(
  uri : String,
  content : &@io.Data,
  headers? : Map[String, String] = Map([]),
  proxy? : Client,
) -> (Response, &@io.Data) {
  request(uri, Post, headers, content, proxy?)
}

///|
/// Similar to `@http.get`, but allow reading response body streamingly.
/// A pair `(response, client)` will be returned,
/// where `response` is the response header from the server,
/// and `client` is the HTTP client that performs the request.
/// `client` can be used to read the content of response body via `@io.Reader`,
/// see `@http.Client` for more details.
///
/// Note that the returned client must be manually closed via `.close()`
/// to close the underlying connection used for the request.
#label_migration(body, fill=false, msg="`GET` request should not contain a body")
pub async fn get_stream(
  uri : String,
  headers? : Map[String, String] = Map([]),
  body? : &@io.Data = b"",
  proxy? : Client,
) -> (Response, Client) {
  let (protocol, host, path) = resolve_url(uri)
  let client = Client::connect(host, headers~, protocol~, proxy?)
  try {
    let response = client..request(Get, path)..write(body).end_request()
    (response, client)
  } catch {
    err => {
      client.close()
      raise err
    }
  }
}

///|
/// Similar to `@http.put`, but allow writing request body streamingly.
/// The return value `client` is the HTTP client that performs the request,
/// it can be used to write the content of request body via `@io.Writer`.
/// Notice that writing to `@http.Client` is buffered,
/// so if you need to send data to the server immediately, `.flush()` must be called.
/// After writing all the content, `.end_request()` must be called
/// to complete the request and obtain response from the server.
/// After that, the response body from the server can be obtained 
/// by using `client` as a `@io.Reader`. See `@http.Client` for more details.
///
/// Note that the returned `client` must be manually closed via `.close()`
/// to close the underlying connection used for the request.
pub async fn put_stream(
  uri : String,
  headers? : Map[String, String] = Map([]),
  proxy? : Client,
) -> Client {
  let (protocol, host, path) = resolve_url(uri)
  let client = Client::connect(host, protocol~, proxy?)
  try client.request(Put, path, extra_headers=headers) catch {
    err => {
      client.close()
      raise err
    }
  } noraise {
    _ => client
  }
}

///|
/// Similar to `@http.post`, but allow writing request body streamingly.
/// The return value `client` is the HTTP client that performs the request,
/// it can be used to write the content of request body via `@io.Writer`.
/// Notice that writing to `@http.Client` is buffered,
/// so if you need to send data to the server immediately, `.flush()` must be called.
/// After writing all the content, `.end_request()` must be called
/// to complete the request and obtain response from the server.
/// After that, the response body from the server can be obtained 
/// by using `client` as a `@io.Reader`. See `@http.Client` for more details.
///
/// Note that the returned `client` must be manually closed via `.close()`
/// to close the underlying connection used for the request.
pub async fn post_stream(
  uri : String,
  headers? : Map[String, String] = Map([]),
  proxy? : Client,
) -> Client {
  let (protocol, host, path) = resolve_url(uri)
  let client = Client::connect(host, protocol~, proxy?)
  try client.request(Post, path, extra_headers=headers) catch {
    err => {
      client.close()
      raise err
    }
  } noraise {
    _ => client
  }
}