package p.packer.repositories;

import p.packer.models.*;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;

public final class PackerRuntimeAssetMaterializer {
    private static final Set<String> RUNTIME_RESERVED_FILE_NAMES = Set.of(
            "asset.json",
            "studio.asset.json");

    private final PackerAssetWalker assetWalker;

    public PackerRuntimeAssetMaterializer(PackerAssetWalker assetWalker) {
        this.assetWalker = Objects.requireNonNull(assetWalker, "assetWalker");
    }

    public PackerRuntimeAssetMaterialization materialize(
            Path assetRoot,
            Path manifestPath,
            Optional<PackerRegistryEntry> registryEntry,
            PackerAssetDeclarationParseResult parseResult,
            Optional<PackerAssetCacheEntry> priorAssetCache) {
        return materialize(
                assetRoot,
                manifestPath,
                registryEntry,
                parseResult,
                priorAssetCache,
                PackerRuntimeMaterializationConfig.runtimeDefault());
    }

    public PackerRuntimeAssetMaterialization materialize(
            Path assetRoot,
            Path manifestPath,
            Optional<PackerRegistryEntry> registryEntry,
            PackerAssetDeclarationParseResult parseResult,
            Optional<PackerAssetCacheEntry> priorAssetCache,
            PackerRuntimeMaterializationConfig materializationConfig) {
        final Path normalizedAssetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
        final Path normalizedManifestPath = Objects.requireNonNull(manifestPath, "manifestPath").toAbsolutePath().normalize();
        final Optional<PackerRegistryEntry> safeRegistryEntry = Objects.requireNonNull(registryEntry, "registryEntry");
        final PackerAssetDeclarationParseResult safeParseResult = Objects.requireNonNull(parseResult, "parseResult");
        final Optional<PackerAssetCacheEntry> safePriorAssetCache = Objects.requireNonNull(priorAssetCache, "priorAssetCache");
        final PackerRuntimeMaterializationConfig safeMaterializationConfig = Objects.requireNonNull(materializationConfig, "materializationConfig");
        final String currentContractFingerprint = safeParseResult.valid()
                ? PackerContractFingerprint.metadataFingerprint(safeParseResult.declaration().outputMetadata())
                : null;

        if (safeRegistryEntry.isEmpty() || !safeParseResult.valid()) {
            return new PackerRuntimeAssetMaterialization(
                    new PackerRuntimeAsset(
                            normalizedAssetRoot,
                            normalizedManifestPath,
                            safeRegistryEntry,
                            safeParseResult,
                            PackerRuntimeWalkProjection.EMPTY,
                            List.of()),
                    Optional.empty());
        }

        final List<String> availableFiles = listAvailableFiles(normalizedAssetRoot);
        final PackerWalkResult walkResult = assetWalker.walk(
                normalizedAssetRoot,
                safeParseResult.declaration(),
                reusablePriorAssetCache(safePriorAssetCache, currentContractFingerprint));
        final List<PackerRuntimeWalkFile> buildCandidateFiles = walkResult.probeResults().stream()
                .filter(probeResult -> shouldIncludeProbe(safeParseResult.declaration(), normalizedAssetRoot, probeResult, safeMaterializationConfig))
                .map(probeResult -> toRuntimeWalkFile(normalizedAssetRoot, probeResult, safeMaterializationConfig))
                .sorted(Comparator.comparing(PackerRuntimeWalkFile::relativePath, String.CASE_INSENSITIVE_ORDER))
                .toList();
        final long measuredBankSizeBytes = buildCandidateFiles.stream()
                .mapToLong(PackerRuntimeWalkFile::size)
                .sum();
        final PackerRuntimeWalkProjection walkProjection = new PackerRuntimeWalkProjection(
                availableFiles,
                buildCandidateFiles,
                measuredBankSizeBytes);
        final PackerRuntimeAsset runtimeAsset = new PackerRuntimeAsset(
                normalizedAssetRoot,
                normalizedManifestPath,
                safeRegistryEntry,
                safeParseResult,
                walkProjection,
                walkResult.diagnostics());
        final PackerAssetCacheEntry assetCacheEntry = new PackerAssetCacheEntry(
                safeRegistryEntry.get().assetId(),
                currentContractFingerprint,
                buildCandidateFiles.stream()
                        .map(file -> new PackerFileCacheEntry(
                                file.relativePath(),
                                file.mimeType(),
                                file.size(),
                                file.lastModified(),
                                file.fingerprint(),
                                file.metadata(),
                                file.diagnostics()))
                        .toList());
        return new PackerRuntimeAssetMaterialization(runtimeAsset, Optional.of(assetCacheEntry));
    }

    private Optional<PackerAssetCacheEntry> reusablePriorAssetCache(
            Optional<PackerAssetCacheEntry> priorAssetCache,
            String currentContractFingerprint) {
        if (priorAssetCache.isEmpty()) {
            return Optional.empty();
        }
        final String cachedContractFingerprint = priorAssetCache.get().contractFingerprint();
        if (cachedContractFingerprint == null || !cachedContractFingerprint.equals(currentContractFingerprint)) {
            return Optional.empty();
        }
        return priorAssetCache;
    }

    private List<String> listAvailableFiles(Path assetRoot) {
        try (var paths = Files.list(assetRoot)
                .filter(Files::isRegularFile)
                .filter(path -> !isRuntimeReservedFile(path))
                .map(path -> assetRoot.relativize(path.toAbsolutePath().normalize()).toString().replace('\\', '/'))
                .sorted(String.CASE_INSENSITIVE_ORDER)) {
            return paths.toList();
        } catch (IOException exception) {
            return List.of();
        }
    }

    private boolean isRuntimeReservedFile(Path path) {
        return RUNTIME_RESERVED_FILE_NAMES.contains(path.getFileName().toString().toLowerCase(Locale.ROOT));
    }

    private PackerRuntimeWalkFile toRuntimeWalkFile(
            Path assetRoot,
            PackerProbeResult probeResult,
            PackerRuntimeMaterializationConfig materializationConfig) {
        final PackerFileProbe fileProbe = probeResult.fileProbe();
        final String relativePath = assetRoot.relativize(fileProbe.path().toAbsolutePath().normalize()).toString().replace('\\', '/');
        return new PackerRuntimeWalkFile(
                relativePath,
                fileProbe.mimeType(),
                fileProbe.size(),
                fileProbe.lastModified(),
                PackerFileFingerprint.sha256(fileProbe),
                materializationConfig.mode() == PackerWalkMode.PACKING
                        ? Optional.of(fileProbe.content())
                        : Optional.empty(),
                probeResult.metadata(),
                probeResult.diagnostics());
    }

    private boolean shouldIncludeProbe(
            PackerAssetDeclaration declaration,
            Path assetRoot,
            PackerProbeResult probeResult,
            PackerRuntimeMaterializationConfig materializationConfig) {
        if (!materializationConfig.projectionFilter().test(probeResult)) {
            return false;
        }
        if (materializationConfig.mode() != PackerWalkMode.PACKING) {
            return true;
        }
        if (declaration.assetFamily() == p.packer.messages.assets.AssetFamilyCatalog.SCENE_BANK) {
            return true;
        }
        if (declaration.artifacts().isEmpty()) {
            return false;
        }
        final String relativePath = assetRoot.relativize(probeResult.fileProbe().path().toAbsolutePath().normalize())
                .toString()
                .replace('\\', '/');
        return declaration.artifacts().stream()
                .map(PackerAssetArtifactSelection::file)
                .anyMatch(relativePath::equals);
    }

    public record PackerRuntimeAssetMaterialization(
            PackerRuntimeAsset runtimeAsset,
            Optional<PackerAssetCacheEntry> assetCacheEntry) {
        public PackerRuntimeAssetMaterialization {
            runtimeAsset = Objects.requireNonNull(runtimeAsset, "runtimeAsset");
            assetCacheEntry = Objects.requireNonNull(assetCacheEntry, "assetCacheEntry");
        }
    }
}
