package at.procon.dip.ingestion.service;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class TedPackageExpansionService {

    private static final int MAX_FILES = 10000;
    private static final long MAX_SINGLE_FILE_SIZE = 20L * 1024 * 1024;
    private static final long MAX_TOTAL_EXTRACTED_SIZE = 1024L * 1024 * 1024;

    public TedPackageExpansionResult expand(byte[] tarGzBytes) {
        List<TedPackageEntry> entries = new ArrayList<>();
        streamEntries(tarGzBytes, entries::add);
        String manifest = buildManifestText(entries);
        return new TedPackageExpansionResult(entries, manifest);
    }

    public void streamEntries(byte[] tarGzBytes, TedPackageEntryConsumer consumer) {
        try (TarArchiveInputStream tais = new TarArchiveInputStream(
                new GzipCompressorInputStream(new ByteArrayInputStream(tarGzBytes)))) {
            streamEntriesInternal(tais, consumer);
        } catch (IOException e) {
            throw new IllegalStateException("Failed to expand TED package", e);
        }
    }

    public void streamEntries(Path tarGzPath, TedPackageEntryConsumer consumer) {
        try (InputStream fileInputStream = Files.newInputStream(tarGzPath);
             TarArchiveInputStream tais = new TarArchiveInputStream(new GzipCompressorInputStream(fileInputStream))) {
            streamEntriesInternal(tais, consumer);
        } catch (IOException e) {
            throw new IllegalStateException("Failed to expand TED package from path " + tarGzPath, e);
        }
    }

    private void streamEntriesInternal(TarArchiveInputStream tais, TedPackageEntryConsumer consumer) throws IOException {
        long total = 0;
        int fileCount = 0;
        TarArchiveEntry entry;
        while ((entry = tais.getNextTarEntry()) != null) {
            if (entry.isDirectory()) {
                continue;
            }
            if (fileCount >= MAX_FILES) {
                break;
            }
            String entryName = entry.getName();
            if (!entryName.toLowerCase().endsWith(".xml")) {
                continue;
            }
            if (entryName.contains("..") || entryName.startsWith("/") || entryName.startsWith("\\")) {
                log.warn("Skipping suspicious TED package entry {}", entryName);
                continue;
            }
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] buffer = new byte[8192];
            long fileSize = 0;
            int read;
            while ((read = tais.read(buffer)) > 0) {
                baos.write(buffer, 0, read);
                fileSize += read;
                total += read;
                if (fileSize > MAX_SINGLE_FILE_SIZE || total > MAX_TOTAL_EXTRACTED_SIZE) {
                    throw new IllegalStateException("TED package extraction limits exceeded");
                }
            }
            byte[] xmlBytes = baos.toByteArray();
            String text = new String(xmlBytes, StandardCharsets.UTF_8);
            TedPackageEntry pkgEntry = new TedPackageEntry(entryName, fileName(entryName), xmlBytes, text);
            try {
                consumer.accept(pkgEntry);
            } catch (Exception e) {
                throw new IllegalStateException("Failed while handling TED package entry " + entryName, e);
            }
            fileCount++;
        }
    }

    private String buildManifestText(List<TedPackageEntry> entries) {
        StringBuilder manifest = new StringBuilder();
        manifest.append("TED package entries:\n");
        for (TedPackageEntry item : entries) {
            manifest.append("- ").append(item.archivePath()).append("\n");
        }
        return manifest.toString();
    }

    private String fileName(String path) {
        int idx = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
        return idx >= 0 ? path.substring(idx + 1) : path;
    }

    @FunctionalInterface
    public interface TedPackageEntryConsumer {
        void accept(TedPackageEntry entry) throws Exception;
    }

    public record TedPackageExpansionResult(List<TedPackageEntry> entries, String manifestText) {
    }

    public record TedPackageEntry(String archivePath, String fileName, byte[] data, String textUtf8) {
    }
}
