/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.cassandra.sidecar.common.request.data;

import java.util.Date;
import java.util.Objects;
import java.util.UUID;
import java.util.function.Consumer;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.cassandra.sidecar.common.DataObjectBuilder;
import org.apache.cassandra.sidecar.common.data.ConsistencyConfig;
import org.apache.cassandra.sidecar.common.data.ConsistencyLevel;
import org.apache.cassandra.sidecar.common.data.RestoreJobSecrets;
import org.apache.cassandra.sidecar.common.data.RestoreJobStatus;
import org.apache.cassandra.sidecar.common.data.SSTableImportOptions;
import org.apache.cassandra.sidecar.common.utils.Preconditions;
import org.apache.cassandra.sidecar.common.utils.StringUtils;
import org.jetbrains.annotations.Nullable;

import static org.apache.cassandra.sidecar.common.data.RestoreJobConstants.JOB_AGENT;
import static org.apache.cassandra.sidecar.common.data.RestoreJobConstants.JOB_CONSISTENCY_LEVEL;
import static org.apache.cassandra.sidecar.common.data.RestoreJobConstants.JOB_EXPIRE_AT;
import static org.apache.cassandra.sidecar.common.data.RestoreJobConstants.JOB_ID;
import static org.apache.cassandra.sidecar.common.data.RestoreJobConstants.JOB_IMPORT_OPTIONS;
import static org.apache.cassandra.sidecar.common.data.RestoreJobConstants.JOB_LOCAL_DATA_CENTER;
import static org.apache.cassandra.sidecar.common.data.RestoreJobConstants.JOB_RESTORE_TO_LOCAL_DATA_CENTER_ONLY;
import static org.apache.cassandra.sidecar.common.data.RestoreJobConstants.JOB_SECRETS;

/**
 * Request payload for creating restore jobs.
 */
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
public class CreateRestoreJobRequestPayload
{
    private final UUID jobId;
    private final String jobAgent;
    private final RestoreJobSecrets secrets;
    private final SSTableImportOptions importOptions;
    private final long expireAtInMillis;
    private final ConsistencyConfig consistencyConfig;
    private final boolean localDatacenterOnly;

    /**
     * Builder to build a CreateRestoreJobRequest
     *
     * @param secrets          secrets for access objects on storage cloud
     * @param expireAtInMillis time in future that the job expires,
     *                         i.e. fail the restore job if it is not in a final {@link RestoreJobStatus} yet
     * @return builder
     */
    public static Builder builder(RestoreJobSecrets secrets, long expireAtInMillis)
    {
        return new Builder(secrets, expireAtInMillis);
    }

    /**
     * CreateRestoreJobRequest deserializer
     *
     * @param jobId            job id of restore job
     * @param jobAgent         arbitrary text a job can put, which can be used to identify itself during Http request
     * @param secrets          secrets to be used by restore job to download data
     * @param importOptions    the configured options for SSTable import
     * @param expireAtInMillis a timestamp in the future when the job is considered expired
     * @param consistencyLevel consistency level a job should satisfy
     * @param localDatacenter  the local datacenter name; required if using local consistency level and localDatacenterOnly is specified
     * @param localDatacenterOnly whether the job should restore to the specified local datacenter only
     */
    @JsonCreator
    public CreateRestoreJobRequestPayload(@JsonProperty(JOB_ID) UUID jobId,
                                          @JsonProperty(JOB_AGENT) String jobAgent,
                                          @JsonProperty(JOB_SECRETS) RestoreJobSecrets secrets,
                                          @JsonProperty(JOB_IMPORT_OPTIONS) SSTableImportOptions importOptions,
                                          @JsonProperty(JOB_EXPIRE_AT) long expireAtInMillis,
                                          @JsonProperty(JOB_CONSISTENCY_LEVEL) String consistencyLevel,
                                          @JsonProperty(JOB_LOCAL_DATA_CENTER) String localDatacenter,
                                          @JsonProperty(JOB_RESTORE_TO_LOCAL_DATA_CENTER_ONLY) boolean localDatacenterOnly)
    {
        Preconditions.checkArgument(jobId == null || jobId.version() == 1,
                                    "Only time based UUIDs allowed for jobId");
        Preconditions.checkArgument(expireAtInMillis != 0 && expireAtInMillis > System.currentTimeMillis(),
                                    "expireAt cannot be absent or a time in past");
        Objects.requireNonNull(secrets, "secrets cannot be null");
        this.jobId = jobId;
        this.jobAgent = jobAgent;
        this.secrets = secrets;
        this.importOptions = importOptions == null
                             ? SSTableImportOptions.defaults()
                             : importOptions;
        this.expireAtInMillis = expireAtInMillis;
        this.consistencyConfig = ConsistencyConfig.parseString(consistencyLevel, localDatacenter);
        Preconditions.checkArgument(!localDatacenterOnly || StringUtils.isNotEmpty(localDatacenter),
                                    "Must specify a localDatacenter when restoreToLocalDatacenterOnly is true");
        this.localDatacenterOnly = localDatacenterOnly;
    }

    private CreateRestoreJobRequestPayload(Builder builder)
    {
        this(builder.jobId,
             builder.jobAgent,
             builder.secrets,
             builder.importOptions,
             builder.expireAtInMillis,
             nameOrNull(builder.consistencyLevel),
             builder.localDc,
             builder.localDatacenterOnly);
    }

    /**
     * @return job id of restore job
     */
    @JsonProperty(JOB_ID)
    public UUID jobId()
    {
        return jobId;
    }

    /**
     * @return arbitrary text a job can put, which can be used to identity itself during Http request
     */
    @JsonProperty(JOB_AGENT)
    public String jobAgent()
    {
        return jobAgent;
    }

    /**
     * @return secrets to be used by restore job to download data
     */
    @JsonProperty(JOB_SECRETS)
    public RestoreJobSecrets secrets()
    {
        return secrets;
    }

    /**
     * @return the options used for importing SSTables
     */
    @JsonProperty(JOB_IMPORT_OPTIONS)
    public SSTableImportOptions importOptions()
    {
        return importOptions;
    }

    /**
     * @return timestamp the job expires, i.e. fail the restore job if it is not in a final {@link RestoreJobStatus} yet
     */
    @JsonProperty(JOB_EXPIRE_AT)
    public long expireAtInMillis()
    {
        return expireAtInMillis;
    }

    /**
     * Convert the expireAtInMillis timestamp as {@link Date}
     *
     * @return date
     */
    public Date expireAtAsDate()
    {
        return new Date(expireAtInMillis);
    }

    /**
     * @return the consistency level a job should satisfy
     */
    @JsonProperty(JOB_CONSISTENCY_LEVEL)
    @Nullable
    public String consistencyLevel()
    {
        return nameOrNull(consistencyConfig.consistencyLevel);
    }

    /**
     * @return the local data center the job restore data too. The field is only required when consistency level is for local DC, e.g. LOCAL_QUORUM
     */
    @JsonProperty(JOB_LOCAL_DATA_CENTER)
    @Nullable
    public String localDatacenter()
    {
        return consistencyConfig.localDatacenter;
    }

    /**
     * @return whether the job should restore only to its specified localDatacenter
     */
    @JsonProperty(JOB_RESTORE_TO_LOCAL_DATA_CENTER_ONLY)
    public boolean shouldRestoreToLocalDatacenterOnly()
    {
        return localDatacenterOnly;
    }

    public ConsistencyConfig consistencyConfig()
    {
        return consistencyConfig;
    }

    @Override
    public String toString()
    {
        return "CreateRestoreJobRequest{" +
               JOB_ID + "='" + jobId + "', " +
               JOB_AGENT + "='" + jobAgent + "', " +
               JOB_SECRETS + "='" + secrets + "', " +
               JOB_EXPIRE_AT + "='" + expireAtInMillis + "', " +
               JOB_CONSISTENCY_LEVEL + "='" + consistencyLevel() + "', " +
               JOB_LOCAL_DATA_CENTER + "='" + localDatacenter() + "', " +
               JOB_RESTORE_TO_LOCAL_DATA_CENTER_ONLY + "='" + shouldRestoreToLocalDatacenterOnly() + "', " +
               JOB_IMPORT_OPTIONS + "='" + importOptions + "'}";
    }

    /**
     * Builds the CreateRestoreJobRequest
     */
    public static class Builder implements DataObjectBuilder<Builder, CreateRestoreJobRequestPayload>
    {
        private final RestoreJobSecrets secrets;
        private final SSTableImportOptions importOptions = SSTableImportOptions.defaults();
        private final long expireAtInMillis;

        private UUID jobId = null;
        private String jobAgent = null;
        private ConsistencyLevel consistencyLevel = null;
        private String localDc = null;
        private boolean localDatacenterOnly = false;

        Builder(RestoreJobSecrets secrets, long expireAtInMillis)
        {
            this.secrets = secrets;
            this.expireAtInMillis = expireAtInMillis;
        }

        public Builder jobId(UUID jobId)
        {
            return update(b -> b.jobId = jobId);
        }

        public Builder jobAgent(String jobAgent)
        {
            return update(b -> b.jobAgent = jobAgent);
        }

        public Builder updateImportOptions(Consumer<SSTableImportOptions> updater)
        {
            return update(b -> updater.accept(b.importOptions));
        }

        public Builder consistencyLevel(ConsistencyLevel consistencyLevel)
        {
            return consistencyLevel(consistencyLevel, null);
        }

        public Builder consistencyLevel(ConsistencyLevel consistencyLevel, String localDc)
        {
            return update(b -> {
                b.consistencyLevel = consistencyLevel;
                b.localDc = localDc;
            });
        }

        public Builder restoreToLocalDatacenterOnly(boolean localDatacenterOnly)
        {
            return update(b -> b.localDatacenterOnly = localDatacenterOnly);
        }

        @Override
        public Builder self()
        {
            return this;
        }

        public CreateRestoreJobRequestPayload build()
        {
            Preconditions.checkArgument(consistencyLevel == null
                                        || !consistencyLevel.isLocalDcOnly
                                        || (localDc != null && !localDc.isEmpty()),
                                        "Must specify a non-empty " + JOB_LOCAL_DATA_CENTER + " for consistency level: " + consistencyLevel);
            return new CreateRestoreJobRequestPayload(this);
        }
    }

    private static String nameOrNull(ConsistencyLevel cl)
    {
        return cl == null ? null : cl.name();
    }
}
