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

import com.atlassian.bitbucket.mesh.io.MoreFiles;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcStreamChunk;
import com.google.common.io.Closer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.WritableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicLong;

import static java.nio.file.StandardOpenOption.*;
import static java.util.Objects.requireNonNull;

/**
 * Utility class, useful for development. Add an instance to {@link com.atlassian.bitbucket.mesh.MeshCoreWiring}
 * to write recordings of incoming {@code git receive-pack} streams to disk. Annotate the instance with {@link Order}
 * to control whether the filtered or unfiltered bytes are written to disk (or create two instances to record both).
 * <p>
 * Packet lines and packs are written to different files to make it easier to inspect the non-binary packet lines.
 * <p>
 * If you need to diff the written files, use
 * {@code diff <(hexdump -C 00a0-e0243271bbe22a758d07-254.packets) <(hexdump -C 00a0-e0243271bbe22a758d07-255.packets)}
 */
public class GitReceivePackRecorder implements GitReceivePackListener {

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

    private final AtomicLong counter;
    private final Path recordingsDir;

    public GitReceivePackRecorder(Path recordingsDir) {
        this.recordingsDir = MoreFiles.mkdir(requireNonNull(recordingsDir, "recordingsDir"));
        counter = new AtomicLong();
    }

    @Override
    public GitReceivePackFilter createFilter(@Nonnull Repository repository) {
        String filename = repository.getSlug() + "-" + counter.incrementAndGet();
        try {
            WritableByteChannel packetLines = Files.newByteChannel(recordingsDir.resolve(filename + ".packets"),
                    CREATE, TRUNCATE_EXISTING, WRITE);
            WritableByteChannel pack = Files.newByteChannel(recordingsDir.resolve(filename + ".pack"),
                    CREATE, TRUNCATE_EXISTING, WRITE);
            return new RecordingFilter(packetLines, pack);
        } catch (IOException e) {
            return null;
        }
    }

    private static class RecordingFilter implements GitReceivePackFilter {

        private final WritableByteChannel pack;
        private final WritableByteChannel packets;

        private boolean failed;

        private RecordingFilter(WritableByteChannel packets, WritableByteChannel pack) {
            this.packets = packets;
            this.pack = pack;
        }

        @Nonnull
        @Override
        public RpcStreamChunk onPack(@Nonnull RpcStreamChunk chunk) {
            write(pack, chunk);
            return chunk;
        }

        @Nonnull
        @Override
        public RpcStreamChunk onPacketLine(@Nonnull RpcStreamChunk chunk) {
            write(packets, chunk);
            return chunk;
        }

        @Override
        public boolean wantsPack() {
            return !failed;
        }

        @Override
        public boolean wantsPacketLines() {
            return !failed;
        }

        private void write(WritableByteChannel channel, RpcStreamChunk chunk) {
            try {
                for (ByteBuffer buffer : chunk.getData().asReadOnlyByteBufferList()) {
                    channel.write(buffer);
                }
                if (chunk.getClosed()) {
                    packets.close();
                    pack.close();
                }
            } catch (IOException e) {
                log.warn("Failed to record receive-pack chunk", e);
                failed = true;
                try (Closer closer = Closer.create()) {
                    closer.register(pack);
                    closer.register(packets);
                } catch (IOException ex) {
                    log.warn("Failed close recordings", e);
                }
            }
        }
    }
}
