package com.atlassian.bitbucket.mesh.migration;

import com.atlassian.bitbucket.mesh.io.MoreFiles;
import com.atlassian.bitbucket.mesh.io.UnderLock;
import com.atlassian.bitbucket.mesh.transaction.FileTransactionLog;
import com.atlassian.bitbucket.mesh.transaction.TransactionLog;
import com.atlassian.bitbucket.mesh.transaction.TransactionLog.Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.time.Instant;
import java.time.format.DateTimeFormatter;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

public class DefaultHierarchyMigrationState implements HierarchyMigrationState {

    private static final String PATH_COMPLETED_MARKER = "completed.log";
    private static final String PATH_MIGRATION_DETAILS = "details.dat";
    private static final String PATH_RUNNING_MARKER = "running.log";
    private static final String PATH_TMP = "tmp";
    private static final String PATH_TRANSACTION_LOG = "transactions.dat";

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

    private final String hierarchyId;
    private final Path migrationDir;
    private final Path tmpDir;
    private final int targetPartition;

    private TransactionLog transactionLog;

    public DefaultHierarchyMigrationState(String hierarchyId, Path migrationDir) throws IOException {
        this.hierarchyId = hierarchyId;
        this.migrationDir = migrationDir;

        targetPartition = getTargetPartition(getDetailsPath());
        tmpDir = migrationDir.resolve(PATH_TMP);
    }

    public DefaultHierarchyMigrationState(String hierarchyId, Path migrationDir,
                                          int targetPartition) throws IOException {
        this.hierarchyId = hierarchyId;
        this.migrationDir = migrationDir;
        this.targetPartition = targetPartition;

        MoreFiles.mkdir(migrationDir);
        MoreFiles.write(getDetailsPath(), Integer.toString(targetPartition), UTF_8);
        tmpDir = MoreFiles.mkdir(migrationDir, PATH_TMP);
    }

    @Override
    public void clear(boolean everything) {
        if (everything) {
            deleteSafely(migrationDir, "migration dir");
        } else {
            //Drop all recorded transactions
            getTransactionLog().process(transactions -> Result.proceed(), 500);
            deleteSafely(tmpDir, "tmp directory");
        }
    }

    @Override
    public int getTargetPartition() {
        return targetPartition;
    }

    @Nonnull
    @Override
    public Path getTmpDir() {
        return tmpDir;
    }

    @Nonnull
    @Override
    public TransactionLog getTransactionLog() {
        if (transactionLog == null) {
            transactionLog = new FileTransactionLog(getTransactionLogPath());
        }
        return transactionLog;
    }

    @Override
    public boolean isCompleted() {
        return MoreFiles.canOpen(getCompletedMarker());
    }

    @Override
    public boolean isRunning() {
        return MoreFiles.canOpen(getRunningMarker());
    }

    @Override
    public void start() throws IOException {
        UnderLock.forFile(getRunningMarker()).edit((reader, writer) -> {
            Instant now = Instant.now();
            if (reader != null) {
                String currentContent = reader.readLine();
                if (isNotBlank(currentContent)) {
                    log.warn("Overwriting migration running marker for {} (content = {})", hierarchyId, currentContent);
                }
            }

            writer.write(Long.toString(now.toEpochMilli()));
            writer.write(";");
            writer.write(DateTimeFormatter.ISO_INSTANT.format(now));
            writer.write(";");
            writer.write("Started migration to partition ");
            writer.write(Integer.toString(targetPartition));
            writer.write(";");
            writer.write(getProcessName());
        }, true);
    }

    @Override
    public void stop(boolean completed) {
        if (completed) {
            String isoInstant = DateTimeFormatter.ISO_INSTANT.format(Instant.now());
            try {
                MoreFiles.write(getCompletedMarker(),
                        "Migration to partition " + targetPartition + " completed on " + isoInstant, UTF_8);
            } catch (IOException e) {
                log.error("[{}] Failed to mark hierarchy as migrated", hierarchyId, e);
            }
        } else {
            //Migration was aborted
            deleteSafely(getCompletedMarker(), "completed marker");
        }
        deleteSafely(getRunningMarker(), "running marker");
    }

    private static String getProcessName() {
        return ManagementFactory.getRuntimeMXBean().getName();
    }

    private static int getTargetPartition(Path detailsPath) throws IOException {
        return Integer.parseInt(MoreFiles.toString(detailsPath, UTF_8));
    }

    private void deleteSafely(Path path, String name) {
        try {
            MoreFiles.deleteRecursively(path);
        } catch (FileNotFoundException | NoSuchFileException e) {
            log.debug("Could not delete {} for migration of {}; the file was not found", name, hierarchyId);
        } catch (IOException e) {
            log.error("Could not delete {} for migration of {}", name, hierarchyId, e);
        }
    }

    private Path getDetailsPath() {
        return migrationDir.resolve(PATH_MIGRATION_DETAILS);
    }

    private Path getCompletedMarker() {
        return migrationDir.resolve(PATH_COMPLETED_MARKER);
    }

    private Path getRunningMarker() {
        return migrationDir.resolve(PATH_RUNNING_MARKER);
    }

    private Path getTransactionLogPath() {
        return migrationDir.resolve(PATH_TRANSACTION_LOG);
    }
}
