/*
 * Copyright 2010-2017 Boxfuse GmbH
 *
 * 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 org.flywaydb.core.internal.info;


import org.flywaydb.core.api.FlywayException;
import org.flywaydb.core.api.MigrationInfo;
import org.flywaydb.core.api.MigrationState;
import org.flywaydb.core.api.MigrationType;
import org.flywaydb.core.api.MigrationVersion;
import org.flywaydb.core.api.resolver.ResolvedMigration;
import org.flywaydb.core.internal.metadatatable.AppliedMigration;
import org.flywaydb.core.internal.util.ObjectUtils;

import java.util.Date;

/**
 * Default implementation of MigrationInfo.
 */
public class MigrationInfoImpl implements MigrationInfo {
    /**
     * The resolved migration to aggregate the info from.
     */
    private final ResolvedMigration resolvedMigration;

    /**
     * The applied migration to aggregate the info from.
     */
    private final AppliedMigration appliedMigration;

    /**
     * The current context.
     */
    private final MigrationInfoContext context;

    /**
     * Whether this migration was applied out of order.
     */
    private final boolean outOfOrder;

    /**
     * Creates a new MigrationInfoImpl.
     *
     * @param resolvedMigration The resolved migration to aggregate the info from.
     * @param appliedMigration  The applied migration to aggregate the info from.
     * @param context           The current context.
     * @param outOfOrder        Whether this migration was applied out of order.
     */
    public MigrationInfoImpl(ResolvedMigration resolvedMigration, AppliedMigration appliedMigration,
                             MigrationInfoContext context, boolean outOfOrder) {
        this.resolvedMigration = resolvedMigration;
        this.appliedMigration = appliedMigration;
        this.context = context;
        this.outOfOrder = outOfOrder;
    }

    /**
     * @return The resolved migration to aggregate the info from.
     */
    public ResolvedMigration getResolvedMigration() {
        return resolvedMigration;
    }

    /**
     * @return The applied migration to aggregate the info from.
     */
    public AppliedMigration getAppliedMigration() {
        return appliedMigration;
    }

    public MigrationType getType() {
        if (appliedMigration != null) {
            return appliedMigration.getType();
        }
        return resolvedMigration.getType();
    }

    public Integer getChecksum() {
        if (appliedMigration != null) {
            return appliedMigration.getChecksum();
        }
        return resolvedMigration.getChecksum();
    }

    public MigrationVersion getVersion() {
        if (appliedMigration != null) {
            return appliedMigration.getVersion();
        }
        return resolvedMigration.getVersion();
    }

    public String getDescription() {
        if (appliedMigration != null) {
            return appliedMigration.getDescription();
        }
        return resolvedMigration.getDescription();
    }

    public String getScript() {
        if (appliedMigration != null) {
            return appliedMigration.getScript();
        }
        return resolvedMigration.getScript();
    }

    public MigrationState getState() {
        if (appliedMigration == null) {
            if (resolvedMigration.getVersion() != null) {
                if (resolvedMigration.getVersion().compareTo(context.baseline) < 0) {
                    return MigrationState.BELOW_BASELINE;
                }
                if (resolvedMigration.getVersion().compareTo(context.target) > 0) {
                    return MigrationState.ABOVE_TARGET;
                }
                if ((resolvedMigration.getVersion().compareTo(context.lastApplied) < 0) && !context.outOfOrder) {
                    return MigrationState.IGNORED;
                }
            }
            return MigrationState.PENDING;
        }

        if (resolvedMigration == null) {
            if (MigrationType.SCHEMA == appliedMigration.getType()) {
                return MigrationState.SUCCESS;
            }

            if (MigrationType.BASELINE == appliedMigration.getType()) {
                return MigrationState.BASELINE;
            }

            if ((appliedMigration.getVersion() == null) || getVersion().compareTo(context.lastResolved) < 0) {
                if (appliedMigration.isSuccess()) {
                    return MigrationState.MISSING_SUCCESS;
                }
                return MigrationState.MISSING_FAILED;
            } else {
                if (appliedMigration.isSuccess()) {
                    return MigrationState.FUTURE_SUCCESS;
                }
                return MigrationState.FUTURE_FAILED;
            }
        }

        if (!appliedMigration.isSuccess()) {
            return MigrationState.FAILED;
        }

        if (appliedMigration.getVersion() == null) {
            if (appliedMigration.getInstalledRank() == context.latestRepeatableRuns.get(appliedMigration.getDescription())) {
                if (ObjectUtils.nullSafeEquals(appliedMigration.getChecksum(), resolvedMigration.getChecksum())) {
                    return MigrationState.SUCCESS;
                }
                return MigrationState.OUTDATED;
            }
            return MigrationState.SUPERSEEDED;
        }

        if (outOfOrder) {
            return MigrationState.OUT_OF_ORDER;
        }
        return MigrationState.SUCCESS;
    }

    public Date getInstalledOn() {
        if (appliedMigration != null) {
            return appliedMigration.getInstalledOn();
        }
        return null;
    }

    @Override
    public String getInstalledBy() {
        if (appliedMigration != null) {
            return appliedMigration.getInstalledBy();
        }
        return null;
    }

    @Override
    public Integer getInstalledRank() {
        if (appliedMigration != null) {
            return appliedMigration.getInstalledRank();
        }
        return null;
    }

    public Integer getExecutionTime() {
        if (appliedMigration != null) {
            return appliedMigration.getExecutionTime();
        }
        return null;
    }

    /**
     * Validates this migrationInfo for consistency.
     *
     * @return The error message, or {@code null} if everything is fine.
     */
    public String validate() {
        if (getState().isFailed()
                && (!context.future || MigrationState.FUTURE_FAILED != getState())) {
            if (getVersion() == null) {
                return "Detected failed repeatable migration: " + getDescription();
            }
            return "Detected failed migration to version " + getVersion() + " (" + getDescription() + ")";
        }

        if ((resolvedMigration == null)
                && (appliedMigration.getType() != MigrationType.SCHEMA)
                && (appliedMigration.getType() != MigrationType.BASELINE)
                && (appliedMigration.getVersion() != null)
                && (!context.missing || (MigrationState.MISSING_SUCCESS != getState() && MigrationState.MISSING_FAILED != getState()))
                && (!context.future || (MigrationState.FUTURE_SUCCESS != getState() && MigrationState.FUTURE_FAILED != getState()))) {
            return "Detected applied migration not resolved locally: " + getVersion();
        }

        if (!context.pending && MigrationState.PENDING == getState() || MigrationState.IGNORED == getState()) {
            if (getVersion() != null) {
                return "Detected resolved migration not applied to database: " + getVersion();
            }
            return "Detected resolved repeatable migration not applied to database: " + getDescription();
        }

        if (!context.pending && MigrationState.OUTDATED == getState()) {
            return "Detected outdated resolved repeatable migration that should be re-applied to database: " + getDescription();
        }

        if (resolvedMigration != null && appliedMigration != null) {
            Object migrationIdentifier = appliedMigration.getVersion();
            if (migrationIdentifier == null) {
                // Repeatable migrations
                migrationIdentifier = appliedMigration.getScript();
            }
            if (getVersion() == null || getVersion().compareTo(context.baseline) > 0) {
                if (resolvedMigration.getType() != appliedMigration.getType()) {
                    return createMismatchMessage("type", migrationIdentifier,
                            appliedMigration.getType(), resolvedMigration.getType());
                }
                if (resolvedMigration.getVersion() != null
                        || (context.pending &&
                        ((MigrationState.OUTDATED != getState()) && (MigrationState.SUPERSEEDED != getState())))) {
                    if (!ObjectUtils.nullSafeEquals(resolvedMigration.getChecksum(), appliedMigration.getChecksum())) {
                        return createMismatchMessage("checksum", migrationIdentifier,
                                appliedMigration.getChecksum(), resolvedMigration.getChecksum());
                    }
                }
                if (!resolvedMigration.getDescription().equals(appliedMigration.getDescription())) {
                    return createMismatchMessage("description", migrationIdentifier,
                            appliedMigration.getDescription(), resolvedMigration.getDescription());
                }
            }
        }
        return null;
    }

    /**
     * Creates a message for a mismatch.
     *
     * @param mismatch            The type of mismatch.
     * @param migrationIdentifier The offending version.
     * @param applied             The applied value.
     * @param resolved            The resolved value.
     * @return The message.
     */
    private String createMismatchMessage(String mismatch, Object migrationIdentifier, Object applied, Object resolved) {
        return String.format("Migration " + mismatch + " mismatch for migration %s\n" +
                        "-> Applied to database : %s\n" +
                        "-> Resolved locally    : %s",
                migrationIdentifier, applied, resolved);
    }

    @SuppressWarnings("NullableProblems")
    public int compareTo(MigrationInfo o) {
        if ((getInstalledRank() != null) && (o.getInstalledRank() != null)) {
            return getInstalledRank() - o.getInstalledRank();
        }

        MigrationState state = getState();
        MigrationState oState = o.getState();

        if (((getInstalledRank() != null) || (o.getInstalledRank() != null))
                && (!(state == MigrationState.BELOW_BASELINE || oState == MigrationState.BELOW_BASELINE
                || state == MigrationState.IGNORED || oState == MigrationState.IGNORED))) {
            if (getInstalledRank() != null) {
                return Integer.MIN_VALUE;
            }
            if (o.getInstalledRank() != null) {
                return Integer.MAX_VALUE;
            }
        }

        if (getVersion() != null && o.getVersion() != null) {
            return getVersion().compareTo(o.getVersion());
        }

        // Versioned pending migrations go before repeatable ones
        if (getVersion() != null) {
            return Integer.MIN_VALUE;
        }
        if (o.getVersion() != null) {
            return Integer.MAX_VALUE;
        }

        return getDescription().compareTo(o.getDescription());
    }

    @SuppressWarnings("SimplifiableIfStatement")
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        MigrationInfoImpl that = (MigrationInfoImpl) o;

        if (appliedMigration != null ? !appliedMigration.equals(that.appliedMigration) : that.appliedMigration != null)
            return false;
        if (!context.equals(that.context)) return false;
        return !(resolvedMigration != null ? !resolvedMigration.equals(that.resolvedMigration) : that.resolvedMigration != null);
    }

    @Override
    public int hashCode() {
        int result = resolvedMigration != null ? resolvedMigration.hashCode() : 0;
        result = 31 * result + (appliedMigration != null ? appliedMigration.hashCode() : 0);
        result = 31 * result + context.hashCode();
        return result;
    }
}