// 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.schema.tld;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.sortedCopyOf;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.util.DomainNameUtils.canonicalizeDomainName;

import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import google.registry.model.CreateAutoTimestamp;
import google.registry.model.ImmutableObject;
import google.registry.model.registry.label.ReservationType;
import java.util.Map;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import javax.persistence.CollectionTable;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Embeddable;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.JoinColumn;
import javax.persistence.MapKeyColumn;
import javax.persistence.Table;
import org.joda.time.DateTime;

/**
 * A list of reserved domain labels that are blocked from being registered for various reasons.
 *
 * <p>Note that the primary key of this entity is {@link #revisionId}, which is auto-generated by
 * the database. So, if a retry of insertion happens after the previous attempt unexpectedly
 * succeeds, we will end up with having two exact same reserved lists that differ only by
 * revisionId. This is fine though, because we only use the list with the highest revisionId.
 */
@Entity
@Table(indexes = {@Index(columnList = "name", name = "reservedlist_name_idx")})
public class ReservedList extends ImmutableObject {

  @Column(nullable = false)
  private String name;

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

  @Column(nullable = false)
  private CreateAutoTimestamp creationTimestamp = CreateAutoTimestamp.create(null);

  @Column(nullable = false)
  private Boolean shouldPublish;

  @ElementCollection
  @CollectionTable(
      name = "ReservedEntry",
      joinColumns = @JoinColumn(name = "revisionId", referencedColumnName = "revisionId"))
  @MapKeyColumn(name = "domainLabel")
  private Map<String, ReservedEntry> labelsToReservations;

  @Embeddable
  public static class ReservedEntry extends ImmutableObject {
    @Column(nullable = false)
    private ReservationType reservationType;

    @Column(nullable = true)
    private String comment;

    private ReservedEntry(ReservationType reservationType, @Nullable String comment) {
      this.reservationType = reservationType;
      this.comment = comment;
    }

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

    /** Constructs a {@link ReservedEntry} object. */
    public static ReservedEntry create(ReservationType reservationType, @Nullable String comment) {
      return new ReservedEntry(reservationType, comment);
    }

    /** Returns the reservation type for this entry. */
    public ReservationType getReservationType() {
      return reservationType;
    }

    /** Returns the comment for this entry. Retruns null if there is no comment. */
    public String getComment() {
      return comment;
    }
  }

  private ReservedList(
      String name, Boolean shouldPublish, Map<String, ReservedEntry> labelsToReservations) {
    this.name = name;
    this.shouldPublish = shouldPublish;
    this.labelsToReservations = labelsToReservations;
  }

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

  /** Constructs a {@link ReservedList} object. */
  public static ReservedList create(
      String name, Boolean shouldPublish, Map<String, ReservedEntry> labelsToReservations) {
    ImmutableList<String> invalidLabels =
        labelsToReservations.entrySet().parallelStream()
            .flatMap(
                entry -> {
                  String label = entry.getKey();
                  if (label.equals(canonicalizeDomainName(label))) {
                    return Stream.empty();
                  } else {
                    return Stream.of(label);
                  }
                })
            .collect(toImmutableList());
    checkArgument(
        invalidLabels.isEmpty(),
        "Label(s) [%s] must be in puny-coded, lower-case form",
        Joiner.on(",").join(sortedCopyOf(invalidLabels)));
    return new ReservedList(name, shouldPublish, labelsToReservations);
  }

  /** Returns the name of the reserved list. */
  public String getName() {
    return name;
  }

  /** Returns the ID of this revision, or throws if null. */
  public Long getRevisionId() {
    checkState(
        revisionId != null,
        "revisionId is null because this object has not been persisted to the database yet");
    return revisionId;
  }

  /** Returns the creation time of this revision of the reserved list. */
  public DateTime getCreationTimestamp() {
    return creationTimestamp.getTimestamp();
  }

  /** Returns a {@link Map} of domain labels to {@link ReservedEntry}. */
  public ImmutableMap<String, ReservedEntry> getLabelsToReservations() {
    return ImmutableMap.copyOf(labelsToReservations);
  }

  /** Returns true if the reserved list should be published. */
  public Boolean getShouldPublish() {
    return shouldPublish;
  }
}
