package com.atlassian.bitbucket.mesh.app;

import com.atlassian.bitbucket.mesh.Product;
import com.atlassian.bitbucket.mesh.management.SupportUtils;
import com.atlassian.bitbucket.mesh.management.SupportInfoProvider;
import com.atlassian.bitbucket.mesh.util.Version;
import com.fasterxml.jackson.core.JsonGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.nio.file.Path;

import static com.atlassian.bitbucket.mesh.layout.StorageLayout.HOME_LAYOUT_VERSION;

/**
 * A startup check which uses {@code home.properties} to validate that the home directory is compatible with the
 * application, and prevents startup if it isn't.
 */
public class HomeValidator implements SupportInfoProvider {

    static final String PATH_PROPERTIES = "home.properties";

    private static final Logger log = LoggerFactory.getLogger(HomeValidator.class);

    private final Version build;
    private final Path homeDir;

    public HomeValidator(Path homeDir, Version build) {
        this.build = build;
        this.homeDir = homeDir;
    }

    /**
     * Validates the home directory's layout is compatible with the current application.
     * <p>
     * If the application has been updated since the previous startup, a message is logged indicating an upgrade
     * has happened, and {@code home.properties} is updated with the current build and home layout versions.
     *
     * @throws HomeValidationFailedException      if validation doesn't complete successfully
     * @throws IncompatibleHomeDirectoryException if the home directory has been updated with a layout which is not
     *                                            compatible with the current application version
     */
    public void validate() {
        try {
            HomeProperties properties = HomeProperties.load(homeDir.resolve(PATH_PROPERTIES));

            boolean update;
            if (properties == null) {
                update = true;
            } else {
                update = validateLayout(properties);
            }

            if (update) {
                new HomeProperties(build, HOME_LAYOUT_VERSION)
                        .store(homeDir.resolve(PATH_PROPERTIES));
            }
        } catch (IOException e) {
            //FileNotFoundException and NoSuchFileException are handled elsewhere, so this IOException indicates
            //some other failure, like a permissions issue, has happened
            throw new HomeValidationFailedException(
                    "The home directory cannot be validated for use by " + Product.NAME + " " + build,
                    "Please check the permissions on " + homeDir,
                    e);
        }
    }

    @Override
    public void writeSupportInfo(@Nonnull JsonGenerator json) throws IOException {
        HomeProperties properties = null;
        try {
            properties = HomeProperties.load(homeDir.resolve(PATH_PROPERTIES));
        } catch (IOException e) {
            log.warn("Failed to load home properties for support info", e);
        }

        if (properties != null) {
            json.writeObjectFieldStart("home");
            try {
                json.writeStringField("path", homeDir.toString());
                json.writeStringField("build-version", properties.getBuild().toString());
                json.writeStringField("layout-version", properties.getLayout().toString());
                SupportUtils.writeFilesystemObject(json, homeDir);
            } finally {
                json.writeEndObject();
            }
        }
    }

    /**
     * Validates the home layout used by the current application is compatible with the layout which has been
     * applied on disk.
     * <p>
     * If the home layout will be updated by the current application, an informational message is logged. If
     * the new layout will not be compatible with the previous application, it is noted in the message.
     * <p>
     * If the home layout on disk is of a newer major version than the current application supports, an
     * {@link IncompatibleHomeDirectoryException} is thrown to block the downgrade. Downgrades between
     * minor versions of the same major version are <i>logged</i>, but are not <i>prevented</i>.
     * <p>
     * Lastly, if the home layout has not changed but the application build has, the upgrade or downgrade,
     * as appropriate, is logged. Note that major <i>build</i> downgrades which have compatible layouts are
     * <i>not</i> blocked. (In practice, however, it's unlikely new major releases will not include major
     * changes to the home layout.)
     *
     * @param properties {@code home.properties}
     * @return {@code true} if the layout is valid but {@code home.properties} needs to be updated; otherwise,
     *         {@code false} if {@code home.properties} is up to date
     * @throws IncompatibleHomeDirectoryException if a {@link VersionUpdate#MAJOR_DOWNGRADE major downgrade} of
     *                                            the home layout is detected
     */
    private boolean validateLayout(HomeProperties properties) {
        VersionUpdate update = VersionUpdate.of(HOME_LAYOUT_VERSION, properties.getLayout());
        if (update == VersionUpdate.MAJOR_DOWNGRADE) {
            //The home directory has been upgraded to an incompatible version to the current layout
            log.warn("Downgrading {} from {} to {} requires restoring from a backup taken prior to " +
                            "the upgrade. The home directory layout has been updated from {} to {} " +
                            "and is no longer compatible",
                    Product.NAME, properties.getBuild(), build, HOME_LAYOUT_VERSION, properties.getLayout());

            throw new IncompatibleHomeDirectoryException("The home directory has been updated by " +
                    Product.NAME + " " + properties.getBuild() + " and is no longer compatible with " + build,
                    "Restore " + Product.NAME + " from a backup to downgrade from " + properties.getBuild() + " to " +
                            build);
        }

        if (update == VersionUpdate.MAJOR_UPGRADE) {
            //A major version upgrade to the home directory layout will prevent downgrades,
            //so log a clear, descriptive message indicating downgrades won't be possible
            log.info("Upgrading {} from {} to {} is a backward-incompatible upgrade. The home directory " +
                            "will be upgraded with a new layout ({} -> {}). It will not be possible to " +
                            "downgrade to an older version without restoring from a backup",
                    Product.NAME, properties.getBuild(), build, properties.getLayout(), HOME_LAYOUT_VERSION);
        } else if (update == VersionUpdate.MINOR_UPGRADE) {
            log.info("Upgrading {} from {} to {} will upgrade the home directory from {} to {}",
                    Product.NAME, properties.getBuild(), build, properties.getLayout(), HOME_LAYOUT_VERSION);
        } else if (update == VersionUpdate.MINOR_DOWNGRADE) {
            //The new layout is different from _but compatible with_ the layout used by the
            //last build to use the home directory
            log.warn("Downgrading {} from {} to {} will not roll back the home directory layout from {} to {}",
                    Product.NAME, properties.getBuild(), build, properties.getLayout(), HOME_LAYOUT_VERSION);
        } else {
            //The home layout has not been updated, but there could still be a build update
            update = VersionUpdate.of(build, properties.getBuild());
            if (update != VersionUpdate.NONE) {
                if (update.isUpgrade()) {
                    log.info("Upgrading {} from {} to {}", Product.NAME, properties.getBuild(), build);
                } else {
                    log.warn("Downgrading {} from {} to {}", Product.NAME, properties.getBuild(), build);
                }
            }
        }

        return update.isUpgrade();
    }
}
