package com.atlassian.bitbucket.mesh.git.batch;

import com.atlassian.bitbucket.mesh.annotation.Profiled;
import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilderFactory;
import com.atlassian.bitbucket.mesh.git.builder.catfile.BatchCheckCatFileStdioHandler;
import com.atlassian.bitbucket.mesh.git.builder.diff.GitDiffRenames;
import com.atlassian.bitbucket.mesh.git.builder.difftree.FileWritingDiffTreeStdioHandler;
import com.atlassian.bitbucket.mesh.git.builder.lstree.FileWritingLsTreeStdioHandler;
import com.atlassian.bitbucket.mesh.io.MoreFiles;
import com.atlassian.bitbucket.mesh.io.NullTerminatedReader;
import com.atlassian.bitbucket.mesh.layout.StorageLayout;
import com.atlassian.bitbucket.mesh.repository.Repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.Duration;

@Profiled
public class DefaultBatchManager implements BatchManager {

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

    private final GitProcessBuilderFactory builderFactory;
    private final StorageLayout storageLayout;

    public DefaultBatchManager(GitProcessBuilderFactory builderFactory, StorageLayout storageLayout) {
        this.builderFactory = builderFactory;
        this.storageLayout = storageLayout;
    }

    @Override
    public void changes(@Nonnull BatchChangesParameters parameters, @Nonnull BatchChangeCallback callback) {
        try {
            if (parameters.getSinceCommitId().isPresent()) {
                batchChangesIncremental(parameters, callback);
            } else {
                batchChangesFull(parameters, callback);
            }
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    private void batchChangesFull(BatchChangesParameters parameters, BatchChangeCallback callback) throws IOException {
        Repository repository = parameters.getRepository();
        Path lsTreeOutput = null;
        try {
            lsTreeOutput = createTempFile(repository, "batch-changes-ls-tree");

            lsTree(parameters, lsTreeOutput);

            streamToCallback(lsTreeOutput, callback);
        } catch (IOException e) {
            log.error("Failed creating temporary files for batch changes command", e);
            throw e;
        } finally {
            deleteTempFile(lsTreeOutput);
        }
    }

    private void batchChangesIncremental(BatchChangesParameters parameters, BatchChangeCallback callback)
            throws IOException {
        Repository repository = parameters.getRepository();
        Path diffTreeOutput = null;
        Path batchCheckOutput = null;
        try {
            diffTreeOutput = createTempFile(repository, "batch-changes-diff-tree");
            batchCheckOutput = createTempFile(repository, "batch-changes-batch-check");

            diffTree(parameters, diffTreeOutput);

            batchCheck(parameters, diffTreeOutput, batchCheckOutput);

            streamToCallback(batchCheckOutput, callback);
        } catch (IOException e) {
            log.error("Failed creating temporary files for incremental batch changes", e);
            throw e;
        } finally {
            deleteTempFile(diffTreeOutput);
            deleteTempFile(batchCheckOutput);
        }
    }

    private void batchCheck(BatchChangesParameters parameters, Path diffTreeOutput, Path batchCheckOutput)
            throws IOException {
        try (BufferedReader reader = Files.newBufferedReader(diffTreeOutput);
             BufferedWriter writer = Files.newBufferedWriter(batchCheckOutput, StandardOpenOption.TRUNCATE_EXISTING)) {
            builderFactory.builder(parameters.getRepository()).catFile()
                    .batchCheck()
                    .build(new BatchCheckCatFileStdioHandler(reader, writer))
                    .run();
        } catch (IOException e) {
            log.error("Failed writing batch-check file for batch changes", e);
            throw e;
        }
    }

    private Path createTempFile(Repository repository, String prefixEnd) throws IOException {
        Path tempDir = storageLayout.getTempDir();
        String prefix = repository.getSlug() + "-" + prefixEnd;
        return Files.createTempFile(tempDir, prefix, null);
    }

    private void deleteTempFile(Path path) {
        if (path != null) {
            MoreFiles.deleteQuietly(path);
        }
    }

    private void diffTree(BatchChangesParameters parameters, Path output) throws IOException {
        try (BufferedWriter writer = Files.newBufferedWriter(output, StandardOpenOption.TRUNCATE_EXISTING)) {
            FileWritingDiffTreeStdioHandler handler = new FileWritingDiffTreeStdioHandler(writer);
            //noinspection OptionalGetWithoutIsPresent
            builderFactory.builder(parameters.getRepository()).diffTree()
                    .ancestor(parameters.getSinceCommitId().get())
                    .format(handler.getFormat())
                    .recursive(true)
                    .renames(GitDiffRenames.OFF)
                    .rev(parameters.getUntilCommitId())
                    .withIdleInterval(Duration.ofSeconds(parameters.getIdleTimeout()))
                    .withExecutionInterval(Duration.ofSeconds(parameters.getExecutionTimeout()))
                    .build(handler)
                    .run();
        } catch (IOException e) {
            log.error("Failed writing diff-tree file for incremental batch changes", e);
            throw e;
        }
    }

    private void lsTree(BatchChangesParameters parameters, Path output) throws IOException {
        try (BufferedWriter writer = Files.newBufferedWriter(output, StandardOpenOption.TRUNCATE_EXISTING)) {
            builderFactory.builder(parameters.getRepository()).lsTree()
                    .nullTerminated(true)
                    .recursive(true)
                    .sizes(true)
                    .tree(parameters.getUntilCommitId(), "")
                    .withIdleInterval(Duration.ofSeconds(parameters.getIdleTimeout()))
                    .withExecutionInterval(Duration.ofSeconds(parameters.getExecutionTimeout()))
                    .build(new FileWritingLsTreeStdioHandler(writer))
                    .run();
        } catch (IOException e) {
            log.error("Failed writing ls-tree file for full batch changes command", e);
            throw e;
        }
    }

    private void streamToCallback(Path metadata, BatchChangeCallback callback) throws IOException {
        try (BufferedReader reader = Files.newBufferedReader(metadata);
             NullTerminatedReader nullTerminatedReader = new NullTerminatedReader(reader)) {
            BatchChange change = BatchChange.read(nullTerminatedReader);
            while (change != null) {
                callback.onBatchChange(change);
                change = BatchChange.read(nullTerminatedReader);
            }
        } catch (IOException e) {
            log.error("Failed reading file contents for batch changes", e);
            throw e;
        }
    }
}
