/*
 * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
 *
 * 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
 *
 *     https://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.
 */
package org.pkl.core.http;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandler;
import java.net.http.HttpTimeoutException;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import javax.net.ssl.SSLContext;
import org.jspecify.annotations.Nullable;
import org.pkl.core.SecurityManagerException;

/**
 * An HTTP client.
 *
 * <p>To create a new HTTP client, use a {@linkplain #builder() builder}. To send {@linkplain
 * HttpRequest requests} and retrieve their {@linkplain HttpResponse responses}, use {@link #send}.
 * To release resources held by the client, use {@link #close}.
 *
 * <p>HTTP clients are thread-safe. Each client maintains its own connection pool and {@link
 * SSLContext}. For efficiency reasons, clients should be reused whenever possible.
 */
public interface HttpClient extends AutoCloseable {

  /** A builder of {@linkplain HttpClient HTTP clients}. */
  @SuppressWarnings("unused")
  interface Builder {
    /**
     * Sets the {@code User-Agent} header.
     *
     * <p>Defaults to {@code "Pkl/$version ($os; $flavor)"}.
     *
     * <p>An existing "User-Agent" from {@link Builder#setHeaders} and {@link Builder#addHeaders}
     * takes precedence over this field.
     */
    Builder setUserAgent(String userAgent);

    /**
     * Sets the timeout for connecting to a server.
     *
     * <p>Defaults to 60 seconds.
     */
    Builder setConnectTimeout(java.time.Duration timeout);

    /**
     * Sets the timeout for the interval between sending a request and receiving response headers.
     *
     * <p>Defaults to 60 seconds. To set a timeout for a specific request, use {@link
     * HttpRequest.Builder#timeout}.
     */
    Builder setRequestTimeout(java.time.Duration timeout);

    /**
     * Adds a CA certificate file to the client's trust store.
     *
     * <p>The given file must contain <a href="https://en.wikipedia.org/wiki/X.509">X.509</a>
     * certificates in PEM format.
     *
     * <p>If no CA certificates are added via this method nor {@link #addCertificates(byte[])}, the
     * built-in CA certificates of the Pkl native executable or JVM are used.
     */
    Builder addCertificates(Path path);

    /**
     * Adds CA certificate bytes to the client's trust store.
     *
     * <p>The given cert must be an <a href="https://en.wikipedia.org/wiki/X.509">X.509</a>
     * certificate in PEM format.
     *
     * <p>If no CA certificates are added via this method nor {@link #addCertificates(Path)}, the
     * built-in CA certificates of the Pkl native executable or JVM are used.
     */
    Builder addCertificates(byte[] certificateBytes);

    /**
     * Sets a test server's listening port.
     *
     * <p>If set, requests that specify port 0 will be modified to use the given port. This is an
     * internal test option.
     */
    Builder setTestPort(int port);

    /**
     * Sets the proxy selector to use when establishing connections.
     *
     * <p>Defaults to: {@link java.net.ProxySelector#getDefault()}.
     */
    Builder setProxySelector(java.net.ProxySelector proxySelector);

    /**
     * Configures HTTP connections to connect to the provided proxy address.
     *
     * <p>The provided {@code proxyAddress} must have scheme http, not contain userInfo, and not
     * have a path segment.
     *
     * <p>If {@code proxyAddress} is {@code null}, uses the proxy address provided by {@link
     * java.net.ProxySelector#getDefault()}.
     *
     * <p>NOTE: Due to a <a href="https://bugs.openjdk.org/browse/JDK-8256409">limitation in the
     * JDK</a>, this does not configure the proxy server used for certificate revocation checking.
     * To configure the certificate revocation checker, the result of {@link
     * java.net.ProxySelector#getDefault} needs to be changed either by setting system properties,
     * or via {@link java.net.ProxySelector#setDefault}.
     *
     * @throws IllegalArgumentException if `proxyAddress` is invalid.
     */
    Builder setProxy(@Nullable URI proxyAddress, List<String> noProxy);

    /**
     * Removes any existing rewrites, then adds the given rewrites.
     *
     * <p>A rewrite changes outbound HTTP URLs by replacing a source prefix with a targert prefix.
     *
     * <p>Each rewrite URI must start with {@code http://} or {@code https://}, and end with {@code
     * /}.
     *
     * <p>Each key describes the prefix of a request, and each value describes the replacement
     * prefix.
     *
     * <p>This can be useful for setting up mirroring of packages, which are fetched over HTTPS.
     *
     * <p>In the case of multiple matches, the longest prefix is used.
     *
     * <p>The URL hostname is case-insensitive.
     *
     * @throws IllegalArgumentException if {@code rewrites} is invalid.
     * @since 0.29.0
     */
    Builder setRewrites(Map<URI, URI> rewrites);

    /**
     * Adds a rewrite rule.
     *
     * @throws IllegalArgumentException if {@code sourcePrefix} or {@code targetPrefix} is invalid.
     * @since 0.29.0
     */
    Builder addRewrite(URI sourcePrefix, URI targetPrefix);

    /**
     * Sets the HTTP headers for the request, replacing any previously configured headers.
     *
     * <p>This method clears all existing headers and replaces them with the contents of the
     * provided map.
     *
     * <p>{@code headerRules} is a map whose keys are <a
     * href="https://pkl-lang.org/main/current/language-reference/index.html#glob-patterns">glob
     * patterns</a>, and values is a map of header names and values. Multiple header values turn
     * into multiple individual headers in the HTTP request.
     *
     * <p>To add headers to all requests, use {@code **} as the glob pattern.
     *
     * <p>To describe a prefix match, add {@code **} to the glob pattern (e.g. {@code
     * https://example.com/**}).
     *
     * <p>Before an HTTP request is made, each key is matched against the request URL. If any
     * matches are found, each of their headers are added to the request.
     *
     * @throws IllegalArgumentException if any of the keys are invalid glob patterns, or if any of
     *     the header names or values are invalid.
     * @since 0.32.0
     */
    Builder setHeaders(Map<String, Map<String, List<String>>> headerRules);

    /**
     * Adds HTTP headers for URL requests that match {@code globPattern}.
     *
     * @throws IllegalArgumentException if {@code globPattern} is an invalid glob pattern, or if any
     *     of the header names or values are invalid.
     * @since 0.32.0
     * @see Builder#setHeaders
     */
    Builder addHeaders(String globPattern, Map<String, List<String>> headers);

    /**
     * Creates a new {@code HttpClient} from the current state of this builder.
     *
     * @throws HttpClientException if an error occurs while initializing the client
     */
    HttpClient build();

    /**
     * Returns an {@code HTTPClient} wrapper that defers building the actual HTTP client until the
     * wrapper's {@link HttpClient#send} method is called.
     *
     * <p>Note: When using this method, any exception thrown when building the actual HTTP client is
     * equally deferred.
     */
    HttpClient buildLazily();
  }

  /**
   * Creates a new {@code HTTPClient} builder with default settings.
   *
   * <p>The default settings are:
   *
   * <ul>
   *   <li>Connect timeout: 60 seconds
   *   <li>Request timeout: 60 seconds
   *   <li>CA certificates: none (falls back to the JVM's {@linkplain SSLContext#getDefault()
   *       default SSL context})
   * </ul>
   */
  static Builder builder() {
    return new HttpClientBuilder();
  }

  /** Returns a client that throws {@link AssertionError} on every attempt to send a request. */
  static HttpClient dummyClient() {
    return new DummyHttpClient();
  }

  /**
   * Sends an HTTP request. The response body is processed by the given body handler.
   *
   * <p>If the request does not specify a {@linkplain HttpRequest#timeout timeout}, the client's
   * {@linkplain Builder#setRequestTimeout request timeout} is used. If the request does not specify
   * a preferred {@linkplain HttpRequest#version() HTTP version}, HTTP/2 is used. The request's
   * {@code User-Agent} header is set to the client's {@link Builder#setUserAgent User-Agent}
   * header.
   *
   * <p>Depending on the given body handler, this method blocks until response headers or the entire
   * response body has been received. If response headers are not received within the request
   * timeout, {@link HttpTimeoutException} is thrown.
   *
   * <p>For additional information on how to use this method, see {@link
   * java.net.http.HttpClient#send}.
   *
   * @throws IOException if an I/O error occurs when sending or receiving
   * @throws HttpClientException if a known (user-presentable) error occurs.
   * @throws SecurityManagerException based on {@code httpRequestChecker}
   */
  <T> HttpResponse<T> send(
      HttpRequest request,
      BodyHandler<T> responseBodyHandler,
      HttpRequestChecker httpRequestChecker)
      throws IOException, SecurityManagerException;

  /**
   * Closes this client.
   *
   * <p>This method makes a best effort to release the resources held by this client in a timely
   * manner. This may involve waiting for pending requests to complete.
   *
   * <p>Subsequent calls to this method have no effect. Subsequent calls to any other method throw
   * {@link IllegalStateException}.
   */
  void close();

  @FunctionalInterface
  interface HttpRequestChecker {
    void check(URI uri) throws SecurityManagerException;
  }
}
