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

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Locale;
import java.util.UUID;

import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.apache.commons.io.IOUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import com.iqser.red.service.persistence.management.v1.processor.exception.BadRequestException;
import com.iqser.red.service.persistence.management.v1.processor.exception.NotAllowedException;
import com.iqser.red.service.persistence.management.v1.processor.utils.FileUtils;
import com.iqser.red.service.persistence.service.v1.api.shared.model.FileUploadResult;
import com.knecon.fforesight.tenantcommons.TenantContext;

import io.micrometer.core.annotation.Timed;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
@RequiredArgsConstructor
public class UploadManagementService {

    private static final int THRESHOLD_ENTRIES = 10000;      // Maximum number of files allowed
    private static final int THRESHOLD_SIZE = 1000000000;    // 1 GB total unzipped data
    private static final double THRESHOLD_RATIO = 10;        // Max allowed compression ratio

    private final UploadService uploadService;
    private final AccessControlService accessControlService;
    private final FileFormatValidationService fileFormatValidationService;


    @Timed
    public FileUploadResult upload(MultipartFile file, String dossierId, boolean keepManualRedactions, boolean disableAutomaticAnalysis) {

        String originalFilename = file.getOriginalFilename();
        if (originalFilename == null) {
            throw new BadRequestException("Could not upload file, no filename provided.");
        }

        String extension = getExtension(originalFilename);
        try {
            return switch (extension) {
                case "zip" -> handleZip(dossierId, file.getBytes(), keepManualRedactions, disableAutomaticAnalysis);
                case "csv" -> uploadService.importCsv(dossierId, file.getBytes());
                default -> {
                    validateExtensionOrThrow(extension);
                    yield uploadService.processSingleFile(dossierId, originalFilename, file.getBytes(), keepManualRedactions, disableAutomaticAnalysis);
                }
            };
        } catch (IOException e) {
            throw new BadRequestException("Failed to process file: " + e.getMessage(), e);
        }
    }


    private void validateExtensionOrThrow(String extension) {

        if (!fileFormatValidationService.getAllFileFormats().contains(extension)) {
            throw new BadRequestException("Invalid file uploaded (unrecognized extension).");
        }
        if (!fileFormatValidationService.getValidFileFormatsForTenant(TenantContext.getTenantId()).contains(extension)) {
            throw new NotAllowedException("Insufficient permissions for this file type.");
        }
    }


    /**
     * 1. Write the uploaded content to a temp ZIP file
     * 2. Check the number of entries and reject if too big or if symlinks found
     * 3. Unzip and process each file, while checking size and ratio.
     */
    private FileUploadResult handleZip(String dossierId, byte[] fileContent, boolean keepManualRedactions, boolean disableAutomaticAnalysis) throws IOException {

        File tempZip = FileUtils.createTempFile(UUID.randomUUID().toString(), ".zip");
        try (FileOutputStream fos = new FileOutputStream(tempZip)) {
            IOUtils.write(fileContent, fos);
        }

        validateZipEntries(tempZip);

        try {
            ZipData zipData = processZipContents(tempZip, dossierId, keepManualRedactions, disableAutomaticAnalysis);

            if (zipData.csvBytes != null) {
                try {
                    FileUploadResult csvResult = uploadService.importCsv(dossierId, zipData.csvBytes);
                    zipData.fileUploadResult.getProcessedAttributes().addAll(csvResult.getProcessedAttributes());
                    zipData.fileUploadResult.getProcessedFileIds().addAll(csvResult.getProcessedFileIds());
                } catch (Exception e) {
                    log.debug("CSV file inside ZIP failed to import", e);
                }
            } else if (zipData.fileUploadResult.getFileIds().isEmpty()) {
                if (zipData.containedUnpermittedFiles) {
                    throw new NotAllowedException("Zip file contains unpermitted files.");
                } else {
                    throw new BadRequestException("Only unsupported files in the ZIP.");
                }
            }

            return zipData.fileUploadResult;
        } finally {

            if (!tempZip.delete()) {
                log.warn("Could not delete temporary ZIP file: {}", tempZip);
            }
        }
    }


    private void validateZipEntries(File tempZip) throws IOException {

        try (FileInputStream fis = new FileInputStream(tempZip); ZipFile zipFile = new ZipFile(fis.getChannel())) {

            int count = 0;
            var entries = zipFile.getEntries();
            while (entries.hasMoreElements()) {
                ZipArchiveEntry ze = entries.nextElement();

                if (ze.isUnixSymlink()) {
                    throw new BadRequestException("ZIP-files with symlinks are not allowed.");
                }

                if (!ze.isDirectory() && !ze.getName().startsWith(".")) {
                    count++;
                    if (count > THRESHOLD_ENTRIES) {
                        throw new BadRequestException("ZIP-Bomb detected: too many entries.");
                    }
                }
            }
        }
    }


    private ZipData processZipContents(File tempZip, String dossierId, boolean keepManualRedactions, boolean disableAutomaticAnalysis) throws IOException {

        ZipData zipData = new ZipData();

        try (FileInputStream fis = new FileInputStream(tempZip); ZipFile zipFile = new ZipFile(fis.getChannel())) {

            var entries = zipFile.getEntries();
            while (entries.hasMoreElements()) {
                ZipArchiveEntry entry = entries.nextElement();

                if (entry.isDirectory() || entry.getName().startsWith(".")) {
                    continue;
                }

                byte[] entryBytes = readEntryWithRatioCheck(entry, zipFile);
                zipData.totalSizeArchive += entryBytes.length;
                if (zipData.totalSizeArchive > THRESHOLD_SIZE) {
                    throw new BadRequestException("ZIP-Bomb detected (exceeds total size limit).");
                }

                String extension = getExtension(entry.getName());
                if ("csv".equalsIgnoreCase(extension)) {
                    zipData.csvBytes = entryBytes;
                } else {
                    handleRegularFile(dossierId, entryBytes, extension, extractFileName(entry.getName()), zipData, keepManualRedactions, disableAutomaticAnalysis);
                }
            }
        }
        return zipData;
    }


    private byte[] readEntryWithRatioCheck(ZipArchiveEntry entry, ZipFile zipFile) throws IOException {

        long compressedSize = entry.getCompressedSize() > 0 ? entry.getCompressedSize() : 1;
        try (var is = zipFile.getInputStream(entry); var bos = new ByteArrayOutputStream()) {

            byte[] buffer = new byte[4096];
            int bytesRead;
            int totalUncompressed = 0;

            while ((bytesRead = is.read(buffer)) != -1) {
                bos.write(buffer, 0, bytesRead);
                totalUncompressed += bytesRead;

                double ratio = (double) totalUncompressed / compressedSize;
                if (ratio > THRESHOLD_RATIO) {
                    throw new BadRequestException("ZIP-Bomb detected (compression ratio too high).");
                }
            }
            return bos.toByteArray();
        }
    }


    private void handleRegularFile(String dossierId,
                                   byte[] fileBytes,
                                   String extension,
                                   String fileName,
                                   ZipData zipData,
                                   boolean keepManualRedactions,
                                   boolean disableAutomaticAnalysis) {

        if (!fileFormatValidationService.getAllFileFormats().contains(extension)) {
            zipData.containedUnpermittedFiles = false;
            return;
        }

        if (!fileFormatValidationService.getValidFileFormatsForTenant(TenantContext.getTenantId()).contains(extension)) {
            zipData.containedUnpermittedFiles = true;
            return;
        }

        try {
            FileUploadResult result = uploadService.processSingleFile(dossierId, fileName, fileBytes, keepManualRedactions, disableAutomaticAnalysis);
            zipData.fileUploadResult.getFileIds().addAll(result.getFileIds());
        } catch (Exception e) {
            log.debug("Failed to process file '{}' in ZIP: {}", fileName, e.getMessage(), e);
        }
    }


    private String extractFileName(String path) {

        int idx = path.lastIndexOf('/');
        return (idx >= 0) ? path.substring(idx + 1) : path;
    }


    private String getExtension(String fileName) {

        int idx = fileName.lastIndexOf('.');
        if (idx < 0) {
            return "";
        }
        return fileName.substring(idx + 1).toLowerCase(Locale.ROOT);
    }


    @FieldDefaults(level = AccessLevel.PUBLIC)
    private static final class ZipData {

        byte[] csvBytes;
        int totalSizeArchive;
        FileUploadResult fileUploadResult = new FileUploadResult();
        boolean containedUnpermittedFiles;

    }

}
