/*
 * RiskScape™ Copyright New Zealand Institute for Earth Science Limited
 * (Earth Sciences New Zealand) is distributed for research purposes only
 * under the terms of AGPLv3.
 *
 * RiskScape™ Copyright 2026 New Zealand Institute for Earth Science
 * Limited (Earth Sciences New Zealand). All rights reserved. Source code
 * available under the AGPLv3.
 * 
 * This program is free software: you can redistribute it and/or modify it under
 *  the terms of the GNU Affero General Public License as published by the Free
 *  Software Foundation, either version 3 of the License, or (at your option) any
 *  later version.
 * 
 * This program is distributed for RESEARCH PURPOSES ONLY, in the hope that it will
 * be useful for research and education initiatives.
 * 
 * If you are not a researcher, or you are a researcher who wishes to use this
 * program on terms other than AGPLv3 (including those who wish to restrict the
 * distribution of any source code created using this program), please contact:
 * https://riskscape.org.nz
 * 
 * This program is distributed WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Affero General Public License for more details.  You should have received a copy
 * of the GNU Affero General Public License along with this program.  If not, see
 * <http://www.gnu.org/licenses/>.
 * 
 * By way of summary only, under the AGPLv3:
 *     • Permissions of this strongest copyleft license are conditioned
 *       on making available complete source code of licensed works and
 *       modifications, which include larger works using a licensed work,
 *       under the same license.
 *     • Copyright and license notices must be preserved.
 *     • Contributors provide an express grant of patent rights.
 *     • When a modified version is used to provide a service over a
 *       network, the complete source code of the modified version must be made
 *       available.
 */
package nz.org.riskscape.engine.auth;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import nz.org.riskscape.config.ConfigString;
import nz.org.riskscape.engine.Engine;
import nz.org.riskscape.engine.RiskscapeException;
import nz.org.riskscape.engine.bind.ParameterField;
import nz.org.riskscape.engine.util.Pair;
import nz.org.riskscape.problem.Problems;

@RequiredArgsConstructor @EqualsAndHashCode
public abstract class HttpSecret implements Secret {

  /**
   * @return a {@link Request} for the given target with any matched secret applied.
   */
  public static Request getRequest(URI target, Engine engine) {
    if (engine.hasCollectionOf(Secret.class)) {
      return getRequest(target, (Secrets) engine.getCollectionByClass(Secret.class));
    } else {
      return Request.noSecret(target);
    }
  }

  /**
   * @return a {@link Request} for the given target with any matched secret applied.
   */
  public static Request getRequest(URI target, Secrets secrets) {
    HttpSecret toApply = secrets.getOfType(HttpSecret.class).stream()
        .filter(s -> s.matches(target))
        .findFirst().orElse(null);

    if (toApply != null) {
      return Request.withSecret(target, toApply);
    }

    return Request.noSecret(target);
  }

  /**
   * Minimal parts of an HTTP request that can be inspected and manipulated for authentication purposes
   */
  @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
  public static class Request {

    public static Request noSecret(URI uri) {
      Request request = new Request(uri.getScheme(), uri.getHost(), uri.getPort(), uri.getQuery());
      request.setPath(uri.getPath());
      return request;
    }

    // private - it should only be created via getRequest
    private static Request withSecret(URI uri, HttpSecret secret) {
      Request request = noSecret(uri);

      request.secret = secret;
      secret.apply(request);

      return request;
    }

    /*
     * Immutable bits - can't see why we'd want auth to change this things?
     */

    @Getter
    private final String scheme;

    @Getter
    private final String hostname;

    @Getter
    private final int port;

    @Getter
    private final String query;

    /*
     * Mutable bits
     */

    @Getter @Setter
    private String path;

    /**
     * A secret that has been applied to this request
     */
    private HttpSecret secret;

    /**
     * HTTP headers that should be added to any connection made for this request.
     */
    private final List<Pair<String, String>> headers = new ArrayList<>();

    /**
     * Add an http header that should accompany this HTTP request
     */
    public void addHeader(String name, String value) {
      headers.add(Pair.of(name, value));
    }

    /**
     * HTTP headers that should be added to any connection made for this request.
     */
    public List<Pair<String, String>> getHeaders() {
      return List.copyOf(headers);
    }

    /**
     * @return the URI for this request. May have had secrets embedded in it.
     */
    public URI getURI() {
      try {
        return new URI(
            scheme,
            null, // no user info - that's authentication stuff that should be in a header
            hostname,
            port,
            path,
            query,
            null // no anchor fragment, these aren't sent to a server
        );
      } catch (URISyntaxException e) {
        throw new RiskscapeException(Problems.caught(e));
      }
    }

    /**
     * @return a secret that was applied, or empty if none has been applied
     */
    public Optional<HttpSecret> getSecret() {
      return Optional.ofNullable(secret);
    }

    /**
     * @return true if a secret has been applied to this request
     */
    public boolean isSecretApplied() {
      return secret != null;
    }
  }

  @Getter
  private final String framework;

  @ParameterField
  private ConfigString id;

  /**
   * The hostname as the user entered it.  Note that the {@link #getHostname()} won't necessarily return the value
   * verbatim if it looks like a URI.
   */
  @Setter @ParameterField
  private String hostname;

  /**
   * By default, only https URLs should use secrets because http is insecure
   */
  @Getter @Setter @ParameterField
  private boolean allowHttp = false;

  @Getter @Setter @ParameterField
  private boolean allowSubdomains = false;

  /**
   * The location where the secret was defined set by secret builders, intended for use in error
   * messages.
   */
  @Override
  public URI getDefinedIn() {
    return id.getLocation();
  }

  @Override
  public String getId() {
    return id.toString();
  }

  public void setId(String id) {
    this.id = ConfigString.anon(id);
  }

  public boolean matches(URI uri) {
    if (!uri.getScheme().startsWith("http")) {
      return false;
    }

    if (!allowHttp && uri.getScheme().equals("http")) {
      return false;
    }

    String host = getHostname();
    if (host.equals(uri.getHost())) {
      return true;
    } else {
      return allowSubdomains && uri.getHost().endsWith("." + host);
    }
  }

  /**
   * Subclass specific behaviour to apply
   * @param request
   */
  protected abstract void apply(Request request);

  /**
   * Returns the hostname.
   *
   * If the user has provided a http/https URI then the hostname will be extracted from it.
   */
  public String getHostname() {
    if (hostname.startsWith("http") && hostname.contains("://")) {
      // it looks like it could be a URI
      try {
        URI h = new URI(hostname);
        return h.getHost();
      } catch (URISyntaxException e) {
        // ignore any URI syntax errors, it just can't be a valid URI so fall though and use the
        // hostname as is.
      }
    }
    return hostname;
  }
}
