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

import com.atlassian.bitbucket.mesh.repository.Repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.Comparator;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import static com.atlassian.bitbucket.mesh.util.MoreStreams.mergeSorted;
import static java.lang.Long.parseLong;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static java.nio.file.StandardOpenOption.CREATE_NEW;
import static java.util.Comparator.comparing;
import static java.util.Comparator.naturalOrder;

class ReflogMerger {

    private static final SecureRandom RANDOM = new SecureRandom();
    private static final Logger log = LoggerFactory.getLogger(ReflogMerger.class);

    private final Path destination;
    private final Repository repository;
    private final Path source;

    ReflogMerger(@Nonnull Repository repository, @Nonnull Path source, @Nonnull Path destination) {
        this.repository = repository;
        this.source = source;
        this.destination = destination;
    }

    public void merge() throws IOException {
        //In most cases entries are going to be sorted, optimistically try merging it and fall back to sorting in
        //memory if it fails
        if (!optimisticMergeSorted()) {
            //In case we encounter an unsorted result, retry by sorting the entries first
            mergeUnsorted();
        }
    }

    private Stream<ReflogLine> convertToReflogLines(Stream<String> destinationLinesStream, Path path) {
        return destinationLinesStream
                .map(line -> mapReflogLine(line, path))
                .filter(Objects::nonNull);
    }

    private ReflogLine mapReflogLine(String line, Path path) {
        try {
            return new ReflogLine(line);
        } catch (IllegalArgumentException e) {
            log.warn("[{}] Unable to parse time of reflog {}: '{}' - pruning entry",
                    repository, repository.getPath().relativize(path), line);
        }
        return null;
    }

    private void mergeUnsorted() throws IOException {
        Path tempFile = newTempDestinationPath();
        try (Stream<String> sourceLinesStream = Files.lines(source);
             Stream<String> destinationLinesStream = Files.lines(destination);
             BufferedWriter merged = Files.newBufferedWriter(tempFile, UTF_8, CREATE_NEW)) {

            Iterable<ReflogLine> lines = () -> mergeSorted(
                    //Ensure that destination is passed first, so that appending to an existing reflog with the same
                    //timestamps preserves order
                    convertToReflogLines(destinationLinesStream, source),
                    convertToReflogLines(sourceLinesStream, destination)
            )
                    //We know the inputs are not sorted, so we have to sort it explicitly
                    .sorted(naturalOrder())
                    .distinct()
                    .iterator();

            for (ReflogLine line : lines) {
                line.appendTo(merged);
            }
        }
        Files.move(tempFile, destination, REPLACE_EXISTING);
    }

    private boolean optimisticMergeSorted() throws IOException {
        Path tempFile = newTempDestinationPath();
        try (Stream<String> sourceLinesStream = Files.lines(source);
             Stream<String> destinationLinesStream = Files.lines(destination);
             BufferedWriter merged = Files.newBufferedWriter(tempFile, UTF_8, CREATE_NEW)) {

            Iterable<ReflogLine> lines = () -> mergeSorted(
                    //Ensure that destination is passed first, so that appending to an existing reflog with the same
                    //timestamps preserves order
                    convertToReflogLines(destinationLinesStream, source),
                    convertToReflogLines(sourceLinesStream, destination)
            )
                    //Since the stream has SORTED characteristics, this will not require consuming the whole input
                    //to deduplicate
                    .distinct()
                    .iterator();

            ReflogLine prevousLine = null;
            for (ReflogLine line : lines) {
                if (prevousLine != null && prevousLine.compareTo(line) > 0) {
                    //The result turned out to be unsorted, abort and fall back to sorting all lines in memory
                    Files.deleteIfExists(tempFile);
                    return false;
                }
                prevousLine = line;
                line.appendTo(merged);
            }
        }
        Files.move(tempFile, destination, REPLACE_EXISTING);
        return true;
    }

    private Path newTempDestinationPath() {
        long n = RANDOM.nextLong();
        n = (n == Long.MIN_VALUE) ? 0 : Math.abs(n);
        return destination.resolveSibling("tmp-" + Long.toString(n, 16) + "-" + destination.getFileName());
    }

    private static class ReflogLine implements Comparable<ReflogLine> {

        static final Comparator<ReflogLine> COMPARATOR =
                comparing(ReflogLine::getTimestamp);
        //Format: old SP new SP name <email> SP time TAB msg LF
        //Emulating Git behavior where the timestamp is parsed after the occurrence of a '>'
        static final Pattern REFLOG_LINE =
                Pattern.compile("^[0-9a-z]{40,} [0-9a-z]{40,} .*> (?<timestamp>\\d+)");

        final String line;
        final Instant timestamp;

        ReflogLine(String line) {
            this.line = line;
            Matcher matcher = REFLOG_LINE.matcher(line);
            if (!matcher.find()) {
                throw new IllegalArgumentException();
            }
            timestamp = Instant.ofEpochSecond(parseLong(matcher.group("timestamp")));
        }

        @Override
        public int compareTo(@Nonnull ReflogLine o) {
            return COMPARATOR.compare(this, o);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            ReflogLine that = (ReflogLine) o;
            return Objects.equals(line, that.line);
        }

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

        void appendTo(Writer writer) throws IOException {
            writer.write(line);
            writer.write("\n");
        }

        Instant getTimestamp() {
            return timestamp;
        }
    }
}
