// Copyright 2017 The Nomulus 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
//
//     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.

package google.registry.flows;

import static com.google.common.collect.Sets.intersection;
import static google.registry.model.EppResourceUtils.isLinked;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import google.registry.flows.EppException.AuthorizationErrorException;
import google.registry.flows.EppException.InvalidAuthorizationInformationErrorException;
import google.registry.flows.EppException.ObjectDoesNotExistException;
import google.registry.flows.EppException.ParameterValuePolicyErrorException;
import google.registry.flows.EppException.ParameterValueRangeErrorException;
import google.registry.flows.exceptions.MissingTransferRequestAuthInfoException;
import google.registry.flows.exceptions.NotPendingTransferException;
import google.registry.flows.exceptions.NotTransferInitiatorException;
import google.registry.flows.exceptions.ResourceAlreadyExistsForThisClientException;
import google.registry.flows.exceptions.ResourceCreateContentionException;
import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException;
import google.registry.flows.exceptions.ResourceToDeleteIsReferencedException;
import google.registry.flows.exceptions.TooManyResourceChecksException;
import google.registry.model.EppResource;
import google.registry.model.EppResource.ForeignKeyedEppResource;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Period;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.host.Host;
import google.registry.model.transfer.TransferStatus;
import google.registry.persistence.VKey;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

/** Static utility functions for resource flows. */
public final class ResourceFlowUtils {

  private ResourceFlowUtils() {}

  /** Check that the given registrarId corresponds to the owner of given resource. */
  public static void verifyResourceOwnership(String myRegistrarId, EppResource resource)
      throws EppException {
    if (!myRegistrarId.equals(resource.getPersistedCurrentSponsorRegistrarId())) {
      throw new ResourceNotOwnedException();
    }
  }

  /** Check if there are domains linked to the host to be deleted. Throws an exception if so. */
  public static void checkLinkedDomains(final String targetId, Instant now) throws EppException {
    VKey<Host> key =
        ForeignKeyUtils.loadKey(Host.class, targetId, now)
            .orElseThrow(() -> new ResourceDoesNotExistException(Host.class, targetId));
    if (isLinked(key, now)) {
      throw new ResourceToDeleteIsReferencedException();
    }
  }

  public static void verifyHasPendingTransfer(Domain domain) throws NotPendingTransferException {
    if (domain.getTransferData().getTransferStatus() != TransferStatus.PENDING) {
      throw new NotPendingTransferException(domain.getForeignKey());
    }
  }

  public static void verifyTransferInitiator(String registrarId, Domain domain)
      throws NotTransferInitiatorException {
    if (!domain.getTransferData().getGainingRegistrarId().equals(registrarId)) {
      throw new NotTransferInitiatorException();
    }
  }

  public static <R extends EppResource & ForeignKeyedEppResource> R loadAndVerifyExistence(
      Class<R> clazz, String targetId, Instant now) throws ResourceDoesNotExistException {
    return verifyExistence(clazz, targetId, ForeignKeyUtils.loadResource(clazz, targetId, now));
  }

  public static <R extends EppResource> R verifyExistence(
      Class<R> clazz, String targetId, Optional<R> resource) throws ResourceDoesNotExistException {
    return resource.orElseThrow(() -> new ResourceDoesNotExistException(clazz, targetId));
  }

  public static <R extends EppResource> void verifyResourceDoesNotExist(
      Class<R> clazz, String targetId, Instant now, String registrarId) throws EppException {
    verifyResourceDoesNotExist(clazz, targetId, now, Duration.ZERO, registrarId);
  }

  /**
   * Verifies that a resource with the specified type and id did not exist at the specified time.
   *
   * <p>This will throw an exception if the resource exists with a deletionTime greater than the
   * specified time (i.e. a registrar is attempting to create a duplicate domain, host, or contact).
   * If the resource had existed within the specified lookback window, but is not active now, i.e.
   * it is recently deleted, then don't throw an exception, but do return the recently deleted
   * resource for further processing (this is used by the Expiry Access Period).
   *
   * <p>If there is no need to specially handle the situation of recently deleted resources, then
   * simply pass a lookback duration of {@link Duration#ZERO}.
   */
  public static <R extends EppResource> Optional<R> verifyResourceDoesNotExist(
      Class<R> clazz, String targetId, Instant now, Duration lookback, String registrarId)
      throws EppException {
    Instant startOfWindow = now.minus(lookback);
    Optional<R> resource = ForeignKeyUtils.loadResource(clazz, targetId, startOfWindow);
    if (resource.isPresent()) {
      if (resource.get().getDeletionTime().isBefore(now)) {
        return resource;
      }
      // These are similar exceptions, but we can track them internally as log-based metrics.
      if (Objects.equals(registrarId, resource.get().getPersistedCurrentSponsorRegistrarId())) {
        throw new ResourceAlreadyExistsForThisClientException(targetId);
      } else {
        throw new ResourceCreateContentionException(targetId);
      }
    }
    return Optional.empty();
  }

  /** Check that the given AuthInfo is present for a resource being transferred. */
  public static void verifyAuthInfoPresentForResourceTransfer(Optional<AuthInfo> authInfo)
      throws EppException {
    if (authInfo.isEmpty()) {
      throw new MissingTransferRequestAuthInfoException();
    }
  }

  /** Check that the given AuthInfo is either missing or else is valid for the given resource. */
  public static void verifyOptionalAuthInfo(Optional<AuthInfo> authInfo, Domain domain)
      throws EppException {
    if (authInfo.isPresent()) {
      verifyAuthInfo(authInfo.get(), domain);
    }
  }

  /** Check that the given {@link AuthInfo} is valid for the given domain. */
  public static void verifyAuthInfo(AuthInfo authInfo, Domain domain) throws EppException {
    String authRepoId = authInfo.getPw().getRepoId();
    // Previously one could auth against a contact, but we no longer hold any contact info
    if (authRepoId != null) {
      throw new BadAuthInfoForResourceException();
    }
    String authPassword = authInfo.getPw().getValue();
    String domainPassword = domain.getAuthInfo().getPw().getValue();
    if (!MessageDigest.isEqual(
        authPassword.getBytes(StandardCharsets.UTF_8),
        domainPassword.getBytes(StandardCharsets.UTF_8))) {
      throw new BadAuthInfoForResourceException();
    }
  }

  /** Check that the resource does not have any disallowed status values. */
  public static void verifyNoDisallowedStatuses(
      EppResource resource, ImmutableSet<StatusValue> disallowedStatuses) throws EppException {
    Set<StatusValue> problems = Sets.intersection(resource.getStatusValues(), disallowedStatuses);
    if (!problems.isEmpty()) {
      throw new ResourceStatusProhibitsOperationException(problems);
    }
  }

  /** Get the list of target ids from a check command. */
  public static void verifyTargetIdCount(List<String> targetIds, int maxChecks)
      throws TooManyResourceChecksException {
    if (targetIds.size() > maxChecks) {
      throw new TooManyResourceChecksException(maxChecks);
    }
  }

  /**
   * Verifies the adds and removes on a resource.
   *
   * <p>This throws an exception in three different situations: if the same value is being both
   * added and removed, if a value is being added that is already present, or if a value is being
   * removed that isn't present.
   */
  public static <T> void verifyAddsAndRemoves(
      ImmutableSet<T> existingFields, ImmutableSet<T> fieldsToAdd, ImmutableSet<T> fieldsToRemove)
      throws AddRemoveSameValueException,
          AddExistingValueException,
          RemoveNonexistentValueException {
    if (!intersection(fieldsToAdd, fieldsToRemove).isEmpty()) {
      throw new AddRemoveSameValueException();
    }
    if (!intersection(fieldsToAdd, existingFields).isEmpty()) {
      throw new AddExistingValueException();
    }
    if (intersection(fieldsToRemove, existingFields).size() != fieldsToRemove.size()) {
      throw new RemoveNonexistentValueException();
    }
  }

  /** Check that all {@link StatusValue} objects in a set are client-settable. */
  public static void verifyAllStatusesAreClientSettable(Set<StatusValue> statusValues)
      throws StatusNotClientSettableException {
    for (StatusValue statusValue : statusValues) {
      if (!statusValue.isClientSettable()) {
        throw new StatusNotClientSettableException(statusValue.getXmlName());
      }
    }
  }

  /**
   * Computes the exDate for the domain at the given transfer approval time with an adjusted amount
   * of transfer period years if the domain is in the auto renew grace period at the time of
   * approval.
   *
   * @param domain is the domain already projected at approvalTime
   */
  public static Instant computeExDateForApprovalTime(
      DomainBase domain, Instant approvalTime, Period period) {
    boolean inAutoRenew = domain.getGracePeriodStatuses().contains(GracePeriodStatus.AUTO_RENEW);
    // inAutoRenew is set to false if the period is zero because a zero-period transfer should not
    // subsume an autorenew.
    // https://www.icann.org/resources/unthemed-pages/appendix-07-2010-01-06-en section 3.1.2
    // specifies that when a transfer occurs without a change to the expiration date, the losing
    // registrar's account should still be charged for the autorenew.
    if (period.getValue() == 0) {
      inAutoRenew = false;
    }
    return Domain.extendRegistrationWithCap(
        approvalTime,
        domain.getRegistrationExpirationTime(),
        period.getValue() - (inAutoRenew ? 1 : 0));
  }

  /** Resource with this id does not exist. */
  public static class ResourceDoesNotExistException extends ObjectDoesNotExistException {
    public ResourceDoesNotExistException(Class<?> type, String targetId) {
      super(type, targetId);
    }
  }

  /** The specified resource belongs to another client. */
  public static class ResourceNotOwnedException extends AuthorizationErrorException {
    public ResourceNotOwnedException() {
      super("The specified resource belongs to another client");
    }
  }

  /** Authorization information for accessing resource is invalid. */
  public static class BadAuthInfoForResourceException
      extends InvalidAuthorizationInformationErrorException {
    public BadAuthInfoForResourceException() {
      super("Authorization information for accessing resource is invalid");
    }
  }

  /** Cannot add and remove the same value. */
  public static class AddRemoveSameValueException extends ParameterValuePolicyErrorException {
    public AddRemoveSameValueException() {
      super("Cannot add and remove the same value");
    }
  }

  /** Cannot add a value that is already present. */
  public static class AddExistingValueException extends ParameterValuePolicyErrorException {
    public AddExistingValueException() {
      super("Cannot add a value that is already present");
    }
  }

  /** Cannot remove a value that does not exist. */
  public static class RemoveNonexistentValueException extends ParameterValuePolicyErrorException {
    public RemoveNonexistentValueException() {
      super("Cannot remove a value that does not exist");
    }
  }

  /** The specified status value cannot be set by clients. */
  public static class StatusNotClientSettableException extends ParameterValueRangeErrorException {
    public StatusNotClientSettableException(String statusValue) {
      super(String.format("Status value %s cannot be set by clients", statusValue));
    }
  }
}
