package p.packer.services;

import p.packer.models.*;
import p.packer.repositories.PackerRuntimeAssetMaterializer;

import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;

public final class PackerRuntimePatchService {
    private final PackerAssetDeclarationParser declarationParser;
    private final PackerRuntimeAssetMaterializer assetMaterializer;

    public PackerRuntimePatchService(
            PackerAssetDeclarationParser declarationParser,
            PackerRuntimeAssetMaterializer assetMaterializer) {
        this.declarationParser = Objects.requireNonNull(declarationParser, "declarationParser");
        this.assetMaterializer = Objects.requireNonNull(assetMaterializer, "assetMaterializer");
    }

    public PackerRuntimeSnapshot afterCreateAsset(
            PackerRuntimeSnapshot snapshot,
            long generation,
            PackerRegistryState updatedRegistry,
            PackerRegistryEntry entry,
            Path assetRoot,
            Path manifestPath) {
        final PackerAssetDeclarationParseResult parsed = declarationParser.parse(manifestPath);
        final List<PackerRuntimeAsset> updatedAssets = new ArrayList<>(snapshot.assets().stream()
                .filter(candidate -> !candidate.assetRoot().equals(assetRoot.toAbsolutePath().normalize()))
                .toList());
        final var materialized = assetMaterializer.materialize(
                assetRoot,
                manifestPath,
                Optional.of(entry),
                parsed,
                snapshot.cacheState().findAsset(entry.assetId()));
        updatedAssets.add(materialized.runtimeAsset());
        updatedAssets.sort(Comparator.comparing(asset -> asset.assetRoot().toString(), String.CASE_INSENSITIVE_ORDER));
        return new PackerRuntimeSnapshot(
                generation,
                updatedRegistry,
                updatedAssets,
                mergeCacheState(snapshot.cacheState(), updatedRegistry, materialized.assetCacheEntry()));
    }

    public PackerRuntimeSnapshot afterRegisterAsset(
            PackerRuntimeSnapshot snapshot,
            long generation,
            PackerRegistryState updatedRegistry,
            PackerRegistryEntry entry,
            Path assetRoot) {
        final List<PackerRuntimeAsset> updatedAssets = new ArrayList<>();
        boolean patched = false;
        Optional<PackerAssetCacheEntry> refreshedCacheEntry = Optional.empty();
        for (PackerRuntimeAsset asset : snapshot.assets()) {
            if (asset.assetRoot().equals(assetRoot.toAbsolutePath().normalize())) {
                final var materialized = assetMaterializer.materialize(
                        asset.assetRoot(),
                        asset.manifestPath(),
                        Optional.of(entry),
                        asset.parsedDeclaration(),
                        snapshot.cacheState().findAsset(entry.assetId()));
                updatedAssets.add(materialized.runtimeAsset());
                refreshedCacheEntry = materialized.assetCacheEntry();
                patched = true;
            } else {
                updatedAssets.add(asset);
            }
        }
        if (!patched) {
            throw new IllegalStateException("Unable to patch runtime snapshot for unregistered asset: " + assetRoot);
        }
        return new PackerRuntimeSnapshot(
                generation,
                updatedRegistry,
                updatedAssets,
                mergeCacheState(snapshot.cacheState(), updatedRegistry, refreshedCacheEntry));
    }

    public PackerRuntimeSnapshot afterDeleteAsset(
            PackerRuntimeSnapshot snapshot,
            long generation,
            PackerRegistryState updatedRegistry,
            Path assetRoot) {
        final List<PackerRuntimeAsset> updatedAssets = snapshot.assets().stream()
                .filter(asset -> !asset.assetRoot().equals(assetRoot.toAbsolutePath().normalize()))
                .toList();
        return new PackerRuntimeSnapshot(
                generation,
                updatedRegistry,
                updatedAssets,
                pruneCacheState(snapshot.cacheState(), updatedRegistry));
    }

    public PackerRuntimeSnapshot afterMoveAsset(
            PackerRuntimeSnapshot snapshot,
            long generation,
            PackerRegistryState updatedRegistry,
            Optional<PackerRegistryEntry> updatedRegistryEntry,
            Path sourceRoot,
            Path targetRoot,
            Path targetManifestPath) {
        final PackerAssetDeclarationParseResult parsed = declarationParser.parse(targetManifestPath);
        final List<PackerRuntimeAsset> updatedAssets = new ArrayList<>();
        boolean patched = false;
        Optional<PackerAssetCacheEntry> refreshedCacheEntry = Optional.empty();
        for (PackerRuntimeAsset asset : snapshot.assets()) {
            if (asset.assetRoot().equals(sourceRoot.toAbsolutePath().normalize())) {
                final var materialized = assetMaterializer.materialize(
                        targetRoot,
                        targetManifestPath,
                        updatedRegistryEntry,
                        parsed,
                        updatedRegistryEntry.flatMap(entry -> snapshot.cacheState().findAsset(entry.assetId())));
                updatedAssets.add(materialized.runtimeAsset());
                refreshedCacheEntry = materialized.assetCacheEntry();
                patched = true;
            } else {
                updatedAssets.add(asset);
            }
        }
        if (!patched) {
            throw new IllegalStateException("Unable to patch runtime snapshot for moved asset: " + sourceRoot);
        }
        updatedAssets.sort(Comparator.comparing(asset -> asset.assetRoot().toString(), String.CASE_INSENSITIVE_ORDER));
        return new PackerRuntimeSnapshot(
                generation,
                updatedRegistry,
                updatedAssets,
                mergeCacheState(snapshot.cacheState(), updatedRegistry, refreshedCacheEntry));
    }

    public PackerRuntimeSnapshot afterUpdateAssetContract(
            PackerRuntimeSnapshot snapshot,
            long generation,
            Path assetRoot,
            Path manifestPath,
            Optional<PackerRegistryEntry> registryEntry) {
        final PackerAssetDeclarationParseResult parsed = declarationParser.parse(manifestPath);
        final List<PackerRuntimeAsset> updatedAssets = new ArrayList<>();
        boolean patched = false;
        Optional<PackerAssetCacheEntry> refreshedCacheEntry = Optional.empty();
        for (PackerRuntimeAsset asset : snapshot.assets()) {
            if (asset.assetRoot().equals(assetRoot.toAbsolutePath().normalize())) {
                final var materialized = assetMaterializer.materialize(
                        asset.assetRoot(),
                        asset.manifestPath(),
                        asset.registryEntry(),
                        parsed,
                        asset.registryEntry().flatMap(entry -> snapshot.cacheState().findAsset(entry.assetId())));
                updatedAssets.add(materialized.runtimeAsset());
                refreshedCacheEntry = materialized.assetCacheEntry();
                patched = true;
            } else {
                updatedAssets.add(asset);
            }
        }
        if (!patched) {
            final var materialized = assetMaterializer.materialize(
                    assetRoot,
                    manifestPath,
                    registryEntry,
                    parsed,
                    registryEntry.flatMap(entry -> snapshot.cacheState().findAsset(entry.assetId())));
            updatedAssets.add(materialized.runtimeAsset());
            refreshedCacheEntry = materialized.assetCacheEntry();
            updatedAssets.sort(Comparator.comparing(asset -> asset.assetRoot().toString(), String.CASE_INSENSITIVE_ORDER));
        }
        return new PackerRuntimeSnapshot(
                generation,
                snapshot.registry(),
                updatedAssets,
                mergeCacheState(snapshot.cacheState(), snapshot.registry(), refreshedCacheEntry));
    }

    private PackerWorkspaceCacheState mergeCacheState(
            PackerWorkspaceCacheState currentCacheState,
            PackerRegistryState registry,
            Optional<PackerAssetCacheEntry> refreshedCacheEntry) {
        final Map<Integer, PackerAssetCacheEntry> byAssetId = new LinkedHashMap<>();
        currentCacheState.assets().forEach(entry -> byAssetId.put(entry.assetId(), entry));
        refreshedCacheEntry.ifPresent(entry -> byAssetId.put(entry.assetId(), entry));
        return pruneCacheEntries(byAssetId, registry);
    }

    private PackerWorkspaceCacheState pruneCacheState(
            PackerWorkspaceCacheState currentCacheState,
            PackerRegistryState registry) {
        final Map<Integer, PackerAssetCacheEntry> byAssetId = new LinkedHashMap<>();
        currentCacheState.assets().forEach(entry -> byAssetId.put(entry.assetId(), entry));
        return pruneCacheEntries(byAssetId, registry);
    }

    private PackerWorkspaceCacheState pruneCacheEntries(
            Map<Integer, PackerAssetCacheEntry> byAssetId,
            PackerRegistryState registry) {
        final Set<Integer> activeAssetIds = registry.assets().stream()
                .map(PackerRegistryEntry::assetId)
                .collect(Collectors.toSet());
        return new PackerWorkspaceCacheState(
                PackerWorkspaceCacheState.CURRENT_SCHEMA_VERSION,
                byAssetId.values().stream()
                        .filter(entry -> activeAssetIds.contains(entry.assetId()))
                        .sorted(Comparator.comparingInt(PackerAssetCacheEntry::assetId))
                        .toList());
    }
}
