// 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.model.rde;

import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.model.rde.RdeNamingUtils.makePartialName;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.DateTimeUtils.toLocalDate;

import com.google.common.base.VerifyException;
import google.registry.model.ImmutableObject;
import google.registry.model.UpdateAutoTimestampEntity;
import google.registry.persistence.VKey;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import jakarta.persistence.IdClass;
import java.io.Serializable;
import java.time.Instant;
import java.time.LocalDate;
import java.util.Optional;

/**
 * Entity for tracking RDE revisions.
 *
 * <p>This class is used by the RDE staging, upload, and reporting systems to determine the revision
 * that should be used in the generated filename. It also determines whether or not a {@code resend}
 * flag is included in the generated XML.
 */
@Entity
@IdClass(RdeRevision.RdeRevisionId.class)
public final class RdeRevision extends UpdateAutoTimestampEntity {

  @Id String tld;

  @Id LocalDate date;

  @Id RdeMode mode;

  /**
   * Number of last revision successfully staged to GCS.
   *
   * <p>This values begins at zero upon object creation and thenceforth incremented transactionally.
   */
  @Column(nullable = false)
  int revision;

  /** Hibernate requires an empty constructor. */
  private RdeRevision() {}

  public static RdeRevision create(String tld, LocalDate date, RdeMode mode, int revision) {
    RdeRevision instance = new RdeRevision();
    instance.tld = tld;
    instance.date = date;
    instance.mode = mode;
    instance.revision = revision;
    return instance;
  }

  public int getRevision() {
    return revision;
  }

  /**
   * Returns next revision ID to use when staging a new deposit file for the given triplet.
   *
   * @return {@code 0} for first deposit generation and {@code >0} for resends
   */
  public static int getNextRevision(String tld, Instant watermark, RdeMode mode) {
    RdeRevisionId revisionId = RdeRevisionId.create(tld, toLocalDate(watermark), mode);
    Optional<RdeRevision> revisionOptional =
        tm().transact(() -> tm().loadByKeyIfPresent(VKey.create(RdeRevision.class, revisionId)));
    return revisionOptional.map(rdeRevision -> rdeRevision.revision + 1).orElse(0);
  }

  /** Returns the latest revision of the report already generated for the given triplet. */
  public static Optional<Integer> getCurrentRevision(String tld, Instant watermark, RdeMode mode) {
    int nextRevision = getNextRevision(tld, watermark, mode);
    if (nextRevision == 0) {
      return Optional.empty();
    }
    return Optional.of(nextRevision - 1);
  }

  /**
   * Sets the revision ID for a given triplet.
   *
   * <p>This method verifies that the current revision is {@code revision - 1}, or that the object
   * does not exist in the database if {@code revision == 0}.
   *
   * @throws IllegalStateException if not in a transaction
   * @throws VerifyException if the state doesn't meet the above criteria
   */
  public static void saveRevision(String tld, Instant watermark, RdeMode mode, int revision) {
    checkArgument(revision >= 0, "Negative revision: %s", revision);
    tm().assertInTransaction();
    RdeRevisionId revisionId = RdeRevisionId.create(tld, toLocalDate(watermark), mode);
    Optional<RdeRevision> revisionOptional =
        tm().loadByKeyIfPresent(VKey.create(RdeRevision.class, revisionId));
    if (revision == 0) {
      revisionOptional.ifPresent(
          rdeRevision -> {
            throw new IllegalArgumentException(
                String.format(
                    "RdeRevision object already created and revision 0 specified: %s",
                    rdeRevision));
          });
    } else {
      checkArgument(
          revisionOptional.isPresent(),
          "Couldn't find existing RDE revision %s when trying to save new revision %s",
          makePartialName(tld, watermark, mode),
          revision);
      checkArgument(
          revisionOptional.get().revision == revision - 1,
          "RDE revision object should be at revision %s but was: %s",
          revision - 1,
          revisionOptional.get());
    }
    tm().put(RdeRevision.create(tld, toLocalDate(watermark), mode, revision));
  }

  /** Class to represent the composite primary key of {@link RdeRevision} entity. */
  public static class RdeRevisionId extends ImmutableObject implements Serializable {

    String tld;

    // Auto-conversion doesn't work for ID classes, we must specify @Column
    @Column(columnDefinition = "date")
    LocalDate date;

    @Enumerated(EnumType.STRING)
    RdeMode mode;

    /** Hibernate requires this default constructor. */
    private RdeRevisionId() {}

    public static RdeRevisionId create(String tld, LocalDate date, RdeMode mode) {
      RdeRevisionId instance = new RdeRevisionId();
      instance.tld = tld;
      instance.date = date;
      instance.mode = mode;
      return instance;
    }
  }
}
