// Copyright 2019 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.model.domain;

import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.util.DateTimeUtils.isBeforeOrAt;
import static google.registry.util.DateTimeUtils.minusHours;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;

import com.google.gson.annotations.Expose;
import google.registry.model.Buildable;
import google.registry.model.CreateAutoTimestamp;
import google.registry.model.UpdateAutoTimestampEntity;
import google.registry.persistence.converter.DurationUserType;
import jakarta.persistence.Access;
import jakarta.persistence.AccessType;
import jakarta.persistence.AttributeOverride;
import jakarta.persistence.AttributeOverrides;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import javax.annotation.Nullable;
import org.hibernate.annotations.Type;

/**
 * Represents a registry lock/unlock object, meaning that the domain is locked on the registry
 * level.
 *
 * <p>Registry locks must be requested through the registrar console by a lock-enabled contact, then
 * confirmed through email within a certain length of time. Until that confirmation is processed,
 * the completion time will remain null and the lock will have no effect. The same applies for
 * unlock actions.
 *
 * <p>Note that there will be at most one row per domain with a null completed time -- this means
 * that there is at most one pending action per domain. This is enforced at the logic level.
 *
 * <p>Note as well that in the case of a retry of a write after an unexpected success, the unique
 * constraint on {@link #verificationCode} means that the second write will fail.
 */
@Entity
@Table(
    /*
     * Unique constraint to get around Hibernate's failure to handle auto-increment field in
     * composite primary key.
     *
     * Note: indexes use the camelCase version of the field names because
     * google.registry.persistence.NomulusNamingStrategy does not translate the field name into the
     * snake_case column name until the write itself.
     */
    indexes = {
      @Index(
          name = "idx_registry_lock_repo_id_revision_id",
          columnList = "repoId, revisionId",
          unique = true),
      @Index(name = "idx_registry_lock_verification_code", columnList = "verificationCode"),
      @Index(name = "idx_registry_lock_registrar_id", columnList = "registrarId")
    })
@Access(AccessType.FIELD)
@AttributeOverride(
    name = "updateTimestamp.lastUpdateTime",
    column = @Column(nullable = false, name = "lastUpdateTime"))
public final class RegistryLock extends UpdateAutoTimestampEntity implements Buildable {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(nullable = false)
  private Long revisionId;

  /** EPP repo ID of the domain in question. */
  @Column(nullable = false)
  private String repoId;

  // TODO (b/140568328): remove this when everything is in Cloud SQL and we can join on "domain"
  @Column(nullable = false)
  @Expose
  private String domainName;

  /**
   * The ID of the registrar that performed the action -- this may be the admin ID if this action
   * was performed by a superuser.
   */
  @Column(nullable = false)
  private String registrarId;

  /**
   * The email address of the user that performed the action, or null if it was a superuser.
   *
   * <p>Note: this is misnamed in the database due to historical reasons, where we used the
   * registrar POC ID as the email address rather than a separate specialized field.
   */
  @Column(name = "registrarPocId")
  @Expose
  private String registryLockEmail;

  /** When the lock is first requested. */
  @AttributeOverrides({
    @AttributeOverride(
        name = "creationTime",
        column = @Column(name = "lockRequestTime", nullable = false))
  })
  @Expose
  private final CreateAutoTimestamp lockRequestTime = CreateAutoTimestamp.create((Instant) null);

  /** When the unlock is first requested. */
  @Expose private Instant unlockRequestTime;

  /**
   * When the user has verified the lock. If this field is null, it means the lock has not been
   * verified yet (and thus not been put into effect).
   */
  @Expose private Instant lockCompletionTime;

  /**
   * When the user has verified the unlock of this lock. If this field is null, it means the unlock
   * action has not been verified yet (and has not been put into effect).
   */
  @Expose private Instant unlockCompletionTime;

  /** The user must provide the random verification code in order to complete the action. */
  @Column(nullable = false)
  private String verificationCode;

  /**
   * True iff this action was taken by a superuser, in response to something like a URS request. In
   * this case, the action was performed by a registry admin rather than a registrar.
   */
  @Column(nullable = false)
  @Expose
  private boolean isSuperuser;

  /** The lock that undoes this lock, if this lock has been unlocked and the domain locked again. */
  // TODO(b/176498743): Lazy loading on scalar field not supported by default. See bug for details.
  @OneToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "relockRevisionId", referencedColumnName = "revisionId")
  private RegistryLock relock;

  /** The duration after which we will re-lock this domain after it is unlocked. */
  @Column(columnDefinition = "interval")
  @Type(DurationUserType.class)
  @Expose
  private Duration relockDuration;

  public String getRepoId() {
    return repoId;
  }

  public String getDomainName() {
    return domainName;
  }

  public String getRegistrarId() {
    return registrarId;
  }

  public String getRegistryLockEmail() {
    return registryLockEmail;
  }

  public Instant getLockRequestTime() {
    return lockRequestTime.getTimestamp();
  }

  /** Returns the unlock request timestamp or null if an unlock has not been requested yet. */
  public Optional<Instant> getUnlockRequestTime() {
    return Optional.ofNullable(unlockRequestTime);
  }

  /** Returns the completion timestamp, or empty if this lock has not been completed yet. */
  public Optional<Instant> getLockCompletionTime() {
    return Optional.ofNullable(lockCompletionTime);
  }

  /**
   * Returns the unlock completion timestamp, or empty if this unlock has not been completed yet.
   */
  public Optional<Instant> getUnlockCompletionTime() {
    return Optional.ofNullable(unlockCompletionTime);
  }

  public String getVerificationCode() {
    return verificationCode;
  }

  public boolean isSuperuser() {
    return isSuperuser;
  }

  public Instant getLastUpdateTime() {
    return getUpdateTimestamp().getTimestamp();
  }

  public Long getRevisionId() {
    return revisionId;
  }

  /**
   * The lock that undoes this lock, if this lock has been unlocked and the domain locked again.
   *
   * <p>Note: this is lazily loaded, so it may not be initialized if referenced outside the
   * transaction in which this lock is loaded.
   */
  public RegistryLock getRelock() {
    return relock;
  }

  /** The duration after which we will re-lock this domain after it is unlocked. */
  public Optional<Duration> getRelockDuration() {
    return Optional.ofNullable(relockDuration);
  }

  public boolean isLocked() {
    return lockCompletionTime != null && unlockCompletionTime == null;
  }

  /** Returns true iff the lock was requested &gt;= 1 hour ago and has not been verified. */
  public boolean isLockRequestExpired(Instant now) {
    return getLockCompletionTime().isEmpty()
        && isBeforeOrAt(getLockRequestTime(), minusHours(now, 1));
  }

  /** Returns true iff the unlock was requested &gt;= 1 hour ago and has not been verified. */
  public boolean isUnlockRequestExpired(Instant now) {
    Optional<Instant> unlockRequestTimestamp = getUnlockRequestTime();
    return unlockRequestTimestamp.isPresent()
        && getUnlockCompletionTime().isEmpty()
        && isBeforeOrAt(unlockRequestTimestamp.get(), minusHours(now, 1));
  }

  @Override
  public Builder asBuilder() {
    return new Builder(clone(this));
  }

  /** Builder for {@link RegistryLock}. */
  public static class Builder extends Buildable.Builder<RegistryLock> {
    public Builder() {}

    private Builder(RegistryLock instance) {
      super(instance);
    }

    @Override
    public RegistryLock build() {
      checkArgumentNotNull(getInstance().repoId, "Repo ID cannot be null");
      checkArgumentNotNull(getInstance().domainName, "Domain name cannot be null");
      checkArgumentNotNull(getInstance().registrarId, "Registrar ID cannot be null");
      checkArgumentNotNull(getInstance().verificationCode, "Verification code cannot be null");
      checkArgument(
          getInstance().registryLockEmail != null || getInstance().isSuperuser,
          "Registrar POC ID must be provided if superuser is false");
      return super.build();
    }

    public Builder setRepoId(String repoId) {
      getInstance().repoId = repoId;
      return this;
    }

    public Builder setDomainName(String domainName) {
      getInstance().domainName = domainName;
      return this;
    }

    public Builder setRegistrarId(String registrarId) {
      getInstance().registrarId = registrarId;
      return this;
    }

    public Builder setRegistryLockEmail(String registryLockEmail) {
      getInstance().registryLockEmail = registryLockEmail;
      return this;
    }

    public Builder setUnlockRequestTime(Instant unlockRequestTime) {
      getInstance().unlockRequestTime = unlockRequestTime;
      return this;
    }

    public Builder setLockCompletionTime(Instant lockCompletionTime) {
      getInstance().lockCompletionTime = lockCompletionTime;
      return this;
    }

    public Builder setUnlockCompletionTime(Instant unlockCompletionTime) {
      getInstance().unlockCompletionTime = unlockCompletionTime;
      return this;
    }

    public Builder setVerificationCode(String verificationCode) {
      getInstance().verificationCode = verificationCode;
      return this;
    }

    public Builder isSuperuser(boolean isSuperuser) {
      getInstance().isSuperuser = isSuperuser;
      return this;
    }

    public Builder setRelock(RegistryLock relock) {
      getInstance().relock = relock;
      return this;
    }

    public Builder setRelockDuration(@Nullable Duration relockDuration) {
      getInstance().relockDuration = relockDuration;
      return this;
    }
  }
}
