///|
struct Client {
  base_url : @uri.Uri
  api_key : Bytes?
  uv : @uv.Loop
  ssl_ctx : @openssl.SslCtx
}

///|
pub fn Client::new(base_url? : String, api_key? : String) -> Client raise {
  let base_url = match base_url {
    Some(base_url) => @encoding.encode(base_url, encoding=UTF8)
    None =>
      match @uv.os_getenv("OPENAI_API_BASE_URL") {
        Some(base_url) => base_url
        None => "https://api.openai.com/v1"
      }
  }
  let base_url = @uri.Uri::parse(base_url)
  let api_key = match api_key {
    Some(api_key) => Some(@encoding.encode(api_key, encoding=UTF8))
    None =>
      match @uv.os_getenv("OPENAI_API_KEY") {
        Some(api_key) => Some(api_key)
        None => None
      }
  }
  if @openssl.init_ssl(
      @openssl.INIT_LOAD_SSL_STRINGS | @openssl.INIT_LOAD_CRYPTO_STRINGS,
    ) !=
    1 {
    fail("Failed to initialize OpenSSL")
  }
  let ssl_ctx = @openssl.SslCtx::new(@openssl.Tls::client_method())
  ssl_ctx.set_default_verify_paths()
  Client::{ base_url, api_key, uv: @uv.Loop::new(), ssl_ctx }
}

///|
fn Client::dns_resolve(
  self : Client,
  host : Bytes,
  port : Bytes,
) -> @uv.Sockaddr raise {
  let errors = []
  let hints = @uv.AddrInfoHints::new()
  let mut addr = None
  let _ = self.uv.getaddrinfo(
    infos => for info in infos {
      addr = Some(info.addr())
    },
    error => errors.push(error),
    host,
    port,
    hints~,
  )
  self.uv.run(Default)
  for error in errors {
    raise error
  }
  guard addr is Some(addr) else { fail("Failed to resolve address") }
  addr
}

///|
fn Client::tcp_connect(
  self : Client,
  addr : @uv.Sockaddr,
) -> @uv.Tcp raise @uv.Errno {
  let client = @uv.Tcp::new(self.uv)
  let errors : Array[@uv.Errno] = []
  client.connect(addr, () => (), error => errors.push(error)) |> ignore()
  self.uv.run(Default)
  for error in errors {
    raise error
  }
  client
}

///|
fn Client::ssl_connect(
  self : Client,
  tcp : @uv.Tcp,
  poll : @uv.Poll,
  host : Bytes,
) -> @openssl.Ssl raise {
  fn on_poll(
    poll : @uv.Poll,
    ssl : @openssl.Ssl,
    errors : Array[Error],
  ) -> Unit {
    match ssl.get_error(ssl.connect()) {
      None => poll.stop() catch { error => errors.push(error) }
      WantRead =>
        poll.start(
          @uv.PollEvent::readable(),
          (_, _) => on_poll(poll, ssl, errors),
          (_, error) => errors.push(error),
        ) catch {
          error => errors.push(error)
        }
      WantWrite =>
        poll.start(
          @uv.PollEvent::writable(),
          (_, _) => on_poll(poll, ssl, errors),
          (_, error) => errors.push(error),
        ) catch {
          error => errors.push(error)
        }
      error => errors.push(Failure("SSL connect error: \{error}"))
    }
  }

  let tcp_os_sock = tcp.os_sock()
  let ssl = @openssl.Ssl::new(self.ssl_ctx)
  let _ = ssl.set_fd(tcp_os_sock.to_int())
  let _ = ssl.set_tlsext_host_name(host)
  let _ = ssl.set1_host(host)
  match ssl.get_error(ssl.connect()) {
    WantRead => {
      let errors = []
      poll.start(
        @uv.PollEvent::readable(),
        (_, _) => on_poll(poll, ssl, errors),
        (_, error) => errors.push(error),
      )
      self.uv.run(Default)
      for error in errors {
        raise error
      }
    }
    WantWrite => {
      let errors = []
      poll.start(
        @uv.PollEvent::writable(),
        (_, _) => on_poll(poll, ssl, errors),
        (_, error) => errors.push(error),
      )
      self.uv.run(Default)
      for error in errors {
        raise error
      }
    }
    error => fail("SSL connect error: \{error}")
  }
  ssl
}

///|
fn Client::ssl_write_all(
  self : Client,
  ssl : @openssl.Ssl,
  data : @bytes.View,
  poll : @uv.Poll,
) -> Unit raise {
  fn on_poll(
    poll : @uv.Poll,
    ssl : @openssl.Ssl,
    buf : @bytes.View,
    errors : Array[Error],
  ) -> Unit {
    let written = ssl.write(buf.data(), buf.start_offset(), buf.length())
    match ssl.get_error(written) {
      None if written == buf.length() =>
        poll.stop() catch {
          error => errors.push(error)
        }
      WantWrite | None => ()
      error => {
        errors.push(Failure("SSL write error: \{error}"))
        poll.stop() catch {
          error => errors.push(error)
        }
      }
    }
  }

  let written = ssl.write(data.data(), data.start_offset(), data.length())
  match ssl.get_error(written) {
    None if written == data.length() => return
    WantWrite => {
      let errors = []
      poll.start(
        @uv.PollEvent::writable(),
        (_, _) => on_poll(poll, ssl, data, errors),
        (_, error) => errors.push(error),
      )
      self.uv.run(Default)
      for error in errors {
        raise error
      }
    }
    WantRead => {
      let errors = []
      poll.start(
        @uv.PollEvent::readable(),
        (_, _) => on_poll(poll, ssl, data, errors),
        (_, error) => errors.push(error),
      )
      self.uv.run(Default)
      for error in errors {
        raise error
      }
    }
    error => fail("SSL write error: \{error}")
  }
}

///|
fn Client::ssl_read_all(
  self : Client,
  ssl : @openssl.Ssl,
  buffer : @buffer.T,
  poll : @uv.Poll,
) -> Unit raise {
  fn on_poll(
    poll : @uv.Poll,
    ssl : @openssl.Ssl,
    buffer : @buffer.T,
    errors : Array[Error],
  ) -> Unit {
    let bytes = Bytes::make(4096, 0)
    let read = ssl.read(bytes, 0, bytes.length())
    match ssl.get_error(read) {
      None => buffer.write_bytesview(bytes[:read])
      ZeroReturn => poll.stop() catch { e => errors.push(e) }
      WantRead =>
        poll.start(
          @uv.PollEvent::readable(),
          (_, _) => on_poll(poll, ssl, buffer, errors),
          (_, error) => errors.push(error),
        ) catch {
          error => errors.push(error)
        }
      WantWrite =>
        poll.start(
          @uv.PollEvent::writable(),
          (_, _) => on_poll(poll, ssl, buffer, errors),
          (_, error) => errors.push(error),
        ) catch {
          error => errors.push(error)
        }
      error => {
        errors.push(Failure("SSL read error: \{error}"))
        poll.stop() catch {
          e => errors.push(e)
        }
      }
    }
  }

  let errors = []
  let bytes = Bytes::make(4096, 0)
  let read = ssl.read(bytes, 0, bytes.length())
  match ssl.get_error(read) {
    None => buffer.write_bytesview(bytes[:read])
    WantRead =>
      poll.start(
        @uv.PollEvent::readable(),
        (_, _) => on_poll(poll, ssl, buffer, errors),
        (_, error) => errors.push(error),
      )
    WantWrite =>
      poll.start(
        @uv.PollEvent::writable(),
        (_, _) => on_poll(poll, ssl, buffer, errors),
        (_, error) => errors.push(error),
      )
    error => {
      errors.push(Failure("SSL read error: \{error}"))
      poll.stop() catch {
        error => errors.push(error)
      }
    }
  }
  self.uv.run(Default)
  for error in errors {
    raise error
  }
}

///|
fn send_http_request(self : Client, request : Bytes) -> Bytes raise {
  let host = match self.base_url.authority.unwrap().host {
    IPv6Address(address) => address
    RegName(name) => name
  }
  let host : Bytes = [..host]
  let addr = self.dns_resolve(host, "443")
  let tcp = self.tcp_connect(addr)
  let poll = @uv.Poll::socket(self.uv, tcp.os_sock())
  let ssl = self.ssl_connect(tcp, poll, host)
  self.ssl_write_all(ssl, request, poll)
  let buffer = @buffer.new()
  self.ssl_read_all(ssl, buffer, poll)
  buffer.contents()
}