package com.atlassian.stash.internal.scheduling;

import com.atlassian.bitbucket.validation.annotation.RequiredString;
import com.atlassian.scheduler.caesium.spi.ClusteredJob;
import com.atlassian.scheduler.config.*;
import com.atlassian.stash.internal.util.TimestampUtils;
import org.hibernate.annotations.CacheConcurrencyStrategy;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.persistence.*;
import java.util.Arrays;
import java.util.Date;
import java.util.Objects;

import static java.util.Objects.requireNonNull;

/**
 * @since 4.6
 */
@Cacheable
@DiscriminatorColumn(name = "sched_type", discriminatorType = DiscriminatorType.INTEGER)
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@Table(name = InternalClusteredJob.TABLE, indexes = {
        @Index(name = "idx_bb_clusteredjob_jrk", columnList = "job_runner_key"),
        @Index(name = "idx_bb_clusteredjob_next_run", columnList = "next_run")
})
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public abstract class InternalClusteredJob implements ClusteredJob {

    public static final String TABLE = "bb_clusteredjob";

    @Column(name = "job_id", nullable = false, unique = true, updatable = false)
    @Id
    @RequiredString(size = 255)
    private final String jobId;

    @Column(name = "job_runner_key", nullable = false, updatable = false)
    @RequiredString(size = 255)
    private final String jobRunnerKey;

    @Column(name = "next_run")
    @Temporal(TemporalType.TIMESTAMP)
    private final Date nextRun;

    @Column(name = "version", nullable = false)
    private final long version;

    @Column(name = "parameters", length = Integer.MAX_VALUE)
    private final byte[] parameters;

    @SuppressWarnings("unused") //Required for Hibernate
    protected InternalClusteredJob() {
        jobId = null;
        jobRunnerKey = null;
        nextRun = null;
        version = 0;
        parameters = null;
    }

    protected InternalClusteredJob(Builder builder) {
        jobId = builder.jobId.toString();
        jobRunnerKey = builder.jobRunnerKey.toString();
        nextRun = builder.nextRun;
        version = builder.version;
        parameters = builder.parameters;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o instanceof InternalClusteredJob) {
            InternalClusteredJob that = (InternalClusteredJob) o;
            return Objects.equals(jobId, that.jobId) &&
                    Objects.equals(jobRunnerKey, that.jobRunnerKey) &&
                    TimestampUtils.equalToSecondPrecision(nextRun, that.nextRun) &&
                    Objects.equals(version, that.version) &&
                    Arrays.equals(parameters, that.parameters);
        }
        return false;
    }

    @Override
    @Nonnull
    public JobId getJobId() {
        return JobId.of(jobId);
    }

    @Override
    @Nonnull
    public JobRunnerKey getJobRunnerKey() {
        return JobRunnerKey.of(jobRunnerKey);
    }

    @Override
    @Nullable
    public Date getNextRunTime() {
        return nextRun;
    }

    @Nonnull
    @Override
    public abstract Schedule getSchedule();

    @Override
    public long getVersion() {
        return version;
    }

    @Override
    @Nullable
    public byte[] getRawParameters() {
        return parameters;
    }

    @Override
    public int hashCode() {
        return Objects.hash(jobId);
    }

    public static class Builder {

        protected final Schedule schedule;

        private final JobId jobId;
        private final JobRunnerKey jobRunnerKey;
        private final long version;

        private Date nextRun;
        private byte[] parameters;

        public Builder(@Nonnull JobId jobId, @Nonnull JobRunnerKey jobRunnerKey, @Nonnull Schedule schedule) {
            this.jobId = requireNonNull(jobId, "jobId");
            this.jobRunnerKey = requireNonNull(jobRunnerKey, "jobRunnerKey");
            this.schedule = requireNonNull(schedule, "schedule");
            version = 0;
        }

        /**
         * Construct a builder for any implementation of the {@link ClusteredJob} interface (which may not necessarily
         * descend from InternalClusteredJob).
         * @param clusteredJob an implementation of {@link ClusteredJob} to copy
         */
        public Builder(@Nonnull ClusteredJob clusteredJob) {
            requireNonNull(clusteredJob, "clusteredJob");
            jobId = clusteredJob.getJobId();
            jobRunnerKey = clusteredJob.getJobRunnerKey();
            schedule = clusteredJob.getSchedule();
            version = clusteredJob.getVersion();
            nextRun = clusteredJob.getNextRunTime();
            parameters = clusteredJob.getRawParameters();
        }

        /**
         * Build the right concrete class for the given data.
         * @return a fully constructed subclass of InternalClusteredJob
         */
        @Nonnull
        public InternalClusteredJob build() {
            switch (schedule.getType()) {
                case CRON_EXPRESSION:
                    return new InternalClusteredCronJob(this);
                case INTERVAL:
                    return new InternalClusteredIntervalJob(this);
                default:
                    throw new IllegalStateException("Unknown Schedule.Type " + schedule.getType());
            }
        }

        @Nonnull
        public Builder nextRun(@Nullable Date value) {
            nextRun = value;
            return this;
        }

        @Nonnull
        public Builder parameters(@Nullable byte[] value) {
            parameters = value == null ? null : Arrays.copyOf(value, value.length);
            return this;
        }
    }

}