package com.iqser.red.service.persistence.management.v1.processor.utils;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class FileSystemBackedArchiver implements AutoCloseable {

    private final boolean rethrowExceptions;
    private final Set<String> createdFolders = new HashSet<>();
    private final File tempFile;
    private final ZipOutputStream zipOutputStream;

    private long tempFileLength;


    @SneakyThrows
    public FileSystemBackedArchiver() {

        this(false);

    }


    /**
     * Controls whether exceptions are re-thrown.  Mostly meant for testing.
     *
     * @param rethrowExceptions If true exceptions caught when handling streams and files will be re-thrown.
     */
    @SneakyThrows
    FileSystemBackedArchiver(boolean rethrowExceptions) {

        this.rethrowExceptions = rethrowExceptions;
        tempFile = FileUtils.createTempFile("archive", ".zip");
        zipOutputStream = new ZipOutputStream(new FileOutputStream(tempFile));
    }


    public void addEntries(ArchiveModel... archives) {

        addEntries(Arrays.asList(archives));

    }


    public void addEntries(List<ArchiveModel> archives) {

        archives.forEach(this::addEntry);
    }


    @SneakyThrows
    public InputStream toInputStream() {

        closeStreamAndStoreTempFileLength();
        return new BufferedInputStream(new FileInputStream(tempFile));
    }


    @SneakyThrows
    public void addEntry(ArchiveModel archiveModel) {
        // begin writing a new ZIP entry, positions the stream to the start of the entry entity
        if (archiveModel.getFolderName() != null && !createdFolders.contains(archiveModel.getFolderName())) {
            ZipEntry folder = new ZipEntry(archiveModel.getFolderName() + File.separator);
            zipOutputStream.putNextEntry(folder);
            zipOutputStream.closeEntry();
            createdFolders.add(archiveModel.getFolderName());
        }

        String zipEntryName = archiveModel.getFolderName() != null ? archiveModel.getFolderName() + File.separator + archiveModel.getName() : archiveModel.getName();
        zipOutputStream.putNextEntry(new ZipEntry(zipEntryName));
        zipOutputStream.write(archiveModel.getBytes(), 0, archiveModel.getBytes().length);
        zipOutputStream.closeEntry();
        zipOutputStream.flush();
    }


    @Override
    public void close() {

        closeStreamAndStoreTempFileLength();

        try {
            Files.delete(tempFile.toPath());
        } catch (IOException e) {
            log.warn("Failed to delete temp-file", e);
            if (rethrowExceptions) {
                throw new RuntimeException(e);
            }
        }

    }


    public long getContentLength() {

        closeStreamAndStoreTempFileLength();
        return tempFileLength;
    }


    private void closeStreamAndStoreTempFileLength() {

        try {
            zipOutputStream.close();
        } catch (IOException e) {
            log.warn("Failed to close temp-file stream", e);
            if (rethrowExceptions) {
                throw new RuntimeException(e);
            }
        }

        if (tempFile.exists()) {
            try {
                var basicFileAttributes = Files.readAttributes(tempFile.toPath(), BasicFileAttributes.class);
                tempFileLength = basicFileAttributes.size();
            } catch (IOException e) {
                if (rethrowExceptions) {
                    throw new RuntimeException(e);
                }
            }
        } else {
            log.warn("The temp file {} was deleted before it was completely processed", tempFile);
        }
    }


    @Data
    @AllArgsConstructor
    public static class ArchiveModel {

        private String folderName;
        private String name;
        private byte[] bytes;

    }

}
