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

///|
priv enum ClientTransport {
  Plain(@socket.Tcp)
  Proxy(Client)
}

///|
/// Simple HTTP client which connect to a remote host via TCP
struct Client {
  reader : Reader
  transport : ClientTransport
  tls : @tls.Tls?
  sender : Sender
  mut request_method : RequestMethod?
  /// whether automatic decompression should be enabled for current request
  mut auto_decompress : Bool
}

///|
/// Error raised when the proxy responded with a non-2XX response code
pub suberror ProxyError {
  ProxyError(Response)
} derive(Debug, ToJson)

///|
async fn Client::connect(
  host : StringView,
  headers? : Map[String, String] = Map([]),
  protocol? : Protocol = Https,
  proxy? : Client,
  trust? : @tls.TrustedRoot = SystemRoot,
) -> Client {
  let (hostname, port) = resolve_host(host, protocol~)
  let transport = if proxy is Some(proxy) {
    try {
      let response = proxy
        ..request(Connect, "\{hostname}:\{port}")
        .end_request()
      guard response.code is (200..<300) else { raise ProxyError(response) }
      proxy..skip_response_body().enter_passthrough_mode()
    } catch {
      err => {
        proxy.close()
        raise err
      }
    }
    Proxy(proxy)
  } else {
    Plain(@socket.Tcp::connect_to_host(hostname, port~))
  }
  headers["Host"] = host.to_owned()
  let (tls, reader, sender) = match (protocol, transport) {
    (Http, Plain(conn)) =>
      (None, Reader::new(conn), Sender::new(conn, headers~))
    (Https, Plain(conn)) => {
      let tls = @tls.Tls::client(conn, host=hostname.to_owned(), trust~) catch {
        err => {
          transport.close()
          raise err
        }
      }
      (Some(tls), Reader::new(tls), Sender::new(tls, headers~))
    }
    (Http, Proxy(proxy)) =>
      (None, Reader::new(proxy), Sender::new(proxy, headers~))
    (Https, Proxy(proxy)) => {
      let tls = @tls.Tls::client(proxy, host=hostname.to_owned(), trust~) catch {
        err => {
          transport.close()
          raise err
        }
      }
      (Some(tls), Reader::new(tls), Sender::new(tls, headers~))
    }
  }
  {
    transport,
    tls,
    reader,
    sender,
    request_method: None,
    auto_decompress: false,
  }
}

///|
/// Create a new HTTP client by connecting to a remote host.
/// Host should be specified via `protocol://host[:port]`,
/// where `protocol` is one of `http` or `https`.
/// If `protocol` is `https`, a TLS connection will be established,
/// and the certificate of the remote peer will be verified.
///
/// If the protocol is `https`, `trust` will determine the trusted root for cert validation.
/// See `@tls.TrustedRoot` for more details.
///
/// `headers` can be used to specify persistent headers for the client,
/// i.e. all requests made from this client will share these headers.
/// The ownership of `headers` will be transferred to the new client,
/// so `headers` should not be used by the caller later.
/// The following headers is automatically set,
/// and must not be specified in `headers`:
///
/// - Host
/// - Content-Length, Transfer-Encoding
///
/// If `proxy` is present, it should be another HTTP client in a clean state.
/// The new client will send a `CONNECT` request via the proxy client
/// and try to establish a tunnel via the proxy client.
/// All subsequent requests made by the new client will go through the proxy tunnel.
/// The ownership of the proxy client is transferred to the new client,
/// so it must not be used nor closed anymore by the caller.
/// Using another HTTP client as proxy allows advanced features such as
/// proxy authentication and https `CONNECT` proxy.
#alias(new, deprecated)
#label_migration(verify, fill=false, msg="use `trust` instead")
pub async fn Client::Client(
  uri : String,
  headers? : Map[String, String] = Map([]),
  proxy? : Client,
  verify? : Bool = true,
  trust? : @tls.TrustedRoot,
) -> Client {
  let (protocol, host, path) = resolve_url(uri)
  guard path is "/" else { raise InvalidFormat }
  let trust = match trust {
    Some(trust) => trust
    None => if verify { SystemRoot } else { NoVerification }
  }
  Client::connect(host, protocol~, headers~, proxy?, trust~)
}

///|
fn ClientTransport::close(self : ClientTransport) -> Unit {
  match self {
    Plain(conn) => conn.close()
    Proxy(proxy) => proxy.close()
  }
}

///|
/// Close a HTTP client and release underlying resource.
/// In particular close the underlying TCP connection.
/// This function is idempotent: it is safe to call `.close()` multiple times,
/// only the first `.close()` call takes effect.
pub fn Client::close(self : Client) -> Unit {
  if self.tls is Some(tls) {
    tls.close()
  }
  self.transport.close()
}

///|
pub impl @io.Reader for Client with fn _direct_read(
  self,
  buf,
  offset~,
  max_len~,
) {
  self.reader._direct_read(buf, offset~, max_len~)
}

///|
pub impl @io.Reader for Client with fn _get_internal_buffer(self) {
  self.reader._get_internal_buffer()
}

///|
/// Write data to the body of the request currently being sent.
/// Must be called after `send_request`.
/// `end_request` must be called after all content of response body has been sent.
///
/// Writing to `@http.Client` MAY be buffered,
/// call `flush` manually to ensure data is delivered to the remote peer.
pub impl @io.Writer for Client with fn write_once(self, buf, offset~, len~) {
  guard! !(self.sender.mode is SendingHeader)
  self.sender.write_once(buf, offset~, len~)
}

///|
pub impl @io.Writer for Client with fn write_reader(self, reader) {
  guard! !(self.sender.mode is SendingHeader)
  self.sender.write_reader(reader)
}

///|
/// Flush buffered data in the request body being sent, if any.
pub async fn Client::flush(self : Client) -> Unit {
  self.sender.flush()
}

///|
/// End the body of the request currently being sent,
/// and obtain response from the server.
/// Should be called immediately after request body is fully sent.
///
/// Only the header of the response will be received and returned,
/// the body of the response can be extracted by using `Client` as a `@io.Reader`.
///
/// If the body of the last response is still not consumed,
/// it will be discarded.
pub async fn Client::end_request(self : Client) -> Response {
  self.sender.end_body()
  self.reader.skip_body()
  let response = self.reader.read_response(
    request_method=self.request_method,
    auto_decompress=self.auto_decompress,
  )
  self.request_method = None
  self.auto_decompress = false
  response
}

///|
/// Send a HTTP request to the server.
/// Only the header of the request will be sent,
/// request body can be sent by using `Client` as a `@io.Writer`.
/// Once request body has been sent,
/// `end_request` must be called to complete the request and obtain response from the server.
///
/// After performing a request,
/// the next request MUST NOT be made before the request is completed via `end_request`.
///
/// In addition to headers in `Client::connect`,
/// extra HTTP headers can be passed via `extra_headers`.
/// The following headers is automatically set by `request`,
/// and must not be specified in `extra_headers`:
///
/// - Host
/// - Transfer-Encoding
///
/// If `Content-Length` is present in `extra_headers`,
/// it should be the total length of the request body.
/// The request body can still be sent incrementally,
/// but if the actual body being sent is shorter and longer than the provided length,
/// an error will be raised.
pub async fn Client::request(
  self : Client,
  meth : RequestMethod,
  path : StringView,
  extra_headers? : Map[String, String] = Map([]),
) -> Unit {
  self.auto_decompress = self.sender.send_request(meth, path, extra_headers~)
  self.request_method = Some(meth)
}

///|
/// Skip the body of the response currently being produced,
/// so that the next request can be made.
pub async fn Client::skip_response_body(self : Client) -> Unit {
  self.reader.skip_body()
}

///|
/// Perform a `GET` request to the server, see `Client::request` for more details.
#label_migration(body, fill=false, msg="`GET` request should not contain a body")
pub async fn Client::get(
  self : Client,
  path : String,
  extra_headers? : Map[String, String] = Map([]),
  body? : &@io.Data,
) -> Response {
  self.request(Get, path, extra_headers~)
  if body is Some(body) {
    self.write(body)
  }
  self.end_request()
}

///|
/// Perform a `PUT` request to the server, see `Client::request` for more details.
pub async fn Client::put(
  self : Client,
  path : String,
  body : &@io.Data,
  extra_headers? : Map[String, String] = Map([]),
) -> Response {
  self..request(Put, path, extra_headers~)..write(body).end_request()
}

///|
/// Perform a `POST` request to the server, see `Client::request` for more details.
pub async fn Client::post(
  self : Client,
  path : String,
  body : &@io.Data,
  extra_headers? : Map[String, String] = Map([]),
) -> Response {
  self..request(Post, path, extra_headers~)..write(body).end_request()
}

///|
/// Let the client enter "pass through" mode,
/// where the client serve as a TCP tunnel (maybe TLS encrypted) directly.
/// This is useful for the special `CONNECT` HTTP request and HTTP protocol upgrade.
///
/// In passthrough mode,
/// Read/write the client becomes direct read/write on the underlying connection,
/// and all API except `@io.Reader` and `@io.Writer` must not be used anymore.
///
/// When entering pass through mode,
/// the client must be in a clean state
/// (i.e. not in the middle of sending a request).
/// Unread data from the body of the last response will be discarded.
pub async fn Client::enter_passthrough_mode(self : Client) -> Unit {
  guard! self.sender.mode is SendingHeader
  self.reader.enter_passthrough_mode()
  self.sender.enter_passthrough_mode()
}