package p.packer.services;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import p.packer.PackerWorkspacePaths;
import p.packer.PackerWorkspaceService;
import p.packer.events.PackerEventKind;
import p.packer.events.PackerEventSink;
import p.packer.events.PackerProgress;
import p.packer.dtos.PackerDiagnosticDTO;
import p.packer.dtos.PackerEmittedArtifactDTO;
import p.packer.dtos.PackerPackExecutionSummaryDTO;
import p.packer.dtos.PackerPackSummaryAssetDTO;
import p.packer.dtos.PackerPackSummaryDTO;
import p.packer.dtos.PackerPackValidationAssetDTO;
import p.packer.messages.*;
import p.packer.messages.assets.*;
import p.packer.messages.diagnostics.PackerDiagnosticCategory;
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
import p.packer.models.*;
import p.packer.repositories.FileSystemPackerCacheRepository;
import p.packer.repositories.PackerAssetWalker;
import p.packer.repositories.PackerContractFingerprint;
import p.packer.repositories.PackerRuntimeAssetMaterializer;
import p.packer.repositories.PackerRuntimeRegistry;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.stream.Stream;

public final class FileSystemPackerWorkspaceService implements PackerWorkspaceService {
    private static final int GLYPH_BANK_COLOR_KEY_RGB565 = 0xF81F;
    private static final String SCENE_BANK_SUPPORT_FILE = "scene-bank.studio.json";
    private static final String SCENE_PACK_REQUEST_FILE = "scene-bank.pack-request.json";

    private final ObjectMapper mapper;
    private final PackerWorkspaceFoundation workspaceFoundation;
    private final PackerAssetDetailsService detailsService;
    private final PackerAssetActionReadService actionReadService;
    private final PackerRuntimePatchService runtimePatchService;
    private final PackerRuntimeRegistry runtimeRegistry;
    private final FileSystemPackerCacheRepository cacheRepository;
    private final PackerProjectWriteCoordinator writeCoordinator;
    private final PackerEventSink eventSink;
    private final PackerGlyphMaterialSupport glyphMaterialSupport;

    public FileSystemPackerWorkspaceService(
            ObjectMapper mapper,
            PackerWorkspaceFoundation workspaceFoundation,
            PackerAssetDetailsService detailsService,
            PackerAssetActionReadService actionReadService,
            PackerRuntimePatchService runtimePatchService,
            PackerRuntimeRegistry runtimeRegistry,
            FileSystemPackerCacheRepository cacheRepository,
            PackerProjectWriteCoordinator writeCoordinator,
            PackerEventSink eventSink) {
        this.mapper = Objects.requireNonNull(mapper, "mapper");
        this.workspaceFoundation = Objects.requireNonNull(workspaceFoundation, "workspaceFoundation");
        this.detailsService = Objects.requireNonNull(detailsService, "detailsService");
        this.actionReadService = Objects.requireNonNull(actionReadService, "actionReadService");
        this.runtimePatchService = Objects.requireNonNull(runtimePatchService, "runtimePatchService");
        this.runtimeRegistry = Objects.requireNonNull(runtimeRegistry, "runtimeRegistry");
        this.cacheRepository = Objects.requireNonNull(cacheRepository, "cacheRepository");
        this.writeCoordinator = Objects.requireNonNull(writeCoordinator, "writeCoordinator");
        this.eventSink = Objects.requireNonNull(eventSink, "eventSink");
        this.glyphMaterialSupport = new PackerGlyphMaterialSupport(mapper);
    }

    @Override
    public InitWorkspaceResult initWorkspace(InitWorkspaceRequest request) {
        final InitWorkspaceResult result = workspaceFoundation.initWorkspace(request);
        runtimeRegistry.refresh(Objects.requireNonNull(request, "request").project());
        return result;
    }

    @Override
    public ListAssetsResult listAssets(ListAssetsRequest request) {
        final var project = Objects.requireNonNull(request, "request").project();
        final var snapshot = request.deepSync()
                ? writeCoordinator.execute(project, () -> refreshAfterArtifactIdentityBackfill(project))
                : runtimeRegistry.getOrLoad(project).snapshot();
        final var events = new PackerOperationEventEmitter(project, eventSink);
        final var registry = snapshot.registry();
        final Map<Path, PackerRegistryEntry> registryByRoot = new HashMap<>();
        for (final var entry : registry.assets()) {
            registryByRoot.put(PackerWorkspacePaths.assetRoot(project, entry.root()), entry);
        }

        final List<PackerAssetSummary> assets = new ArrayList<>();
        final List<PackerDiagnostic> diagnostics = new ArrayList<>();
        final Set<Path> discoveredRoots = new HashSet<>();

        final List<PackerRuntimeAsset> runtimeAssets = snapshot.assets();
        final int total = runtimeAssets.size();
        for (int index = 0; index < runtimeAssets.size(); index += 1) {
            final var runtimeAsset = runtimeAssets.get(index);
            final var assetRoot = runtimeAsset.assetRoot();
            final var assetManifestPath = runtimeAsset.manifestPath();
            discoveredRoots.add(assetRoot);
            final var registryEntry = registryByRoot.get(assetRoot);
            final var parsed = runtimeAsset.parsedDeclaration();
            diagnostics.addAll(parsed.diagnostics());
            diagnostics.addAll(runtimeAsset.walkDiagnostics());
            diagnostics.addAll(identityMismatchDiagnostics(registryEntry, parsed, assetManifestPath));
            final var summary = buildSummary(project, assetRoot, registryEntry, parsed);
            assets.add(summary);
            events.emit(
                    PackerEventKind.ASSET_DISCOVERED,
                    "Discovered asset: " + summary.identity().assetName(),
                    new PackerProgress((index + 1) / (double) total, false),
                    List.of(summary.identity().assetName()));
        }

        for (final var entry : registry.assets()) {
            final var registeredRoot = PackerWorkspacePaths.assetRoot(project, entry.root());
            if (!discoveredRoots.contains(registeredRoot)) {
                diagnostics.add(new PackerDiagnostic(
                        PackerDiagnosticSeverity.ERROR,
                        PackerDiagnosticCategory.STRUCTURAL,
                        "Registered asset root is missing asset.json: " + entry.root(),
                        registeredRoot.resolve("asset.json"),
                        true));
            }
        }

        assets.sort(Comparator
                .comparing((PackerAssetSummary asset) -> asset.identity().assetRoot().toString(), String.CASE_INSENSITIVE_ORDER)
                .thenComparing(summary -> summary.identity().assetName(), String.CASE_INSENSITIVE_ORDER));
        final var status = diagnostics.stream().anyMatch(PackerDiagnostic::blocking)
                ? PackerOperationStatus.PARTIAL
                : PackerOperationStatus.SUCCESS;
        if (!diagnostics.isEmpty()) {
            events.emit(PackerEventKind.DIAGNOSTICS_UPDATED, "Asset scan diagnostics updated.", List.of());
        }
        return new ListAssetsResult(
                status,
                "Packer asset snapshot ready.",
                PackerReadMessageMapper.toAssetSummaryDTOs(assets),
                PackerReadMessageMapper.toDiagnosticDTOs(diagnostics));
    }

    @Override
    public GetAssetDetailsResult getAssetDetails(GetAssetDetailsRequest request) {
        final GetAssetDetailsRequest safeRequest = Objects.requireNonNull(request, "request");
        return writeCoordinator.execute(safeRequest.project(), () -> {
            backfillLoadedArtifactIdentity(safeRequest.project());
            return detailsService.getAssetDetails(safeRequest);
        });
    }

    @Override
    public GetAssetActionsResult getAssetActions(GetAssetActionsRequest request) {
        return actionReadService.getAssetActions(request);
    }

    @Override
    public PackWorkspaceSummaryResult getPackWorkspaceSummary(PackWorkspaceSummaryRequest request) {
        final PackerProjectContext project = Objects.requireNonNull(request, "request").project();
        final PackerRuntimeSnapshot snapshot = runtimeRegistry.getOrLoad(project).snapshot();
        final List<PackerPackSummaryAssetDTO> assets = snapshot.assets().stream()
                .filter(this::isEffectivelyIncludedInPackSet)
                .sorted(Comparator.comparingInt(runtimeAsset -> runtimeAsset.registryEntry().get().assetId()))
                .map(this::toPackSummaryAssetDTO)
                .toList();
        return new PackWorkspaceSummaryResult(
                PackerOperationStatus.SUCCESS,
                "Pack summary resolved from runtime snapshot.",
                new PackerPackSummaryDTO(assets.size(), assets));
    }

    @Override
    public ValidatePackWorkspaceResult validatePackWorkspace(ValidatePackWorkspaceRequest request) {
        final PackerProjectContext project = Objects.requireNonNull(request, "request").project();
        final PackerRuntimeSnapshot snapshot = writeCoordinator.execute(project, () -> refreshAfterArtifactIdentityBackfill(project));
        final List<PackerPackValidationAssetDTO> blockedAssets = snapshot.assets().stream()
                .filter(this::isEffectivelyIncludedInPackSet)
                .map(this::toPackValidationAssetDTO)
                .filter(PackerPackValidationAssetDTO::blocked)
                .toList();
        final PackerOperationStatus status = blockedAssets.isEmpty()
                ? PackerOperationStatus.SUCCESS
                : PackerOperationStatus.PARTIAL;
        final String summary = blockedAssets.isEmpty()
                ? "Pack validation completed without blocking diagnostics."
                : "Pack validation found blocking diagnostics in " + blockedAssets.size() + " included assets.";
        return new ValidatePackWorkspaceResult(status, summary, blockedAssets);
    }

    @Override
    public PackWorkspaceResult packWorkspace(PackWorkspaceRequest request) {
        final PackWorkspaceRequest safeRequest = Objects.requireNonNull(request, "request");
        final PackerProjectContext project = safeRequest.project();
        return writeCoordinator.execute(project, () -> packWorkspaceInWriteLane(safeRequest));
    }

    @Override
    public CreateAssetResult createAsset(CreateAssetRequest request) {
        final CreateAssetRequest safeRequest = Objects.requireNonNull(request, "request");
        final PackerProjectContext project = safeRequest.project();
        final PackerOperationEventEmitter events = new PackerOperationEventEmitter(project, eventSink);

        return writeCoordinator.execute(project, () -> createAssetInWriteLane(safeRequest, events));
    }

    @Override
    public RegisterAssetResult registerAsset(RegisterAssetRequest request) {
        final RegisterAssetRequest safeRequest = Objects.requireNonNull(request, "request");
        final PackerProjectContext project = safeRequest.project();
        final PackerOperationEventEmitter events = new PackerOperationEventEmitter(project, eventSink);
        return writeCoordinator.execute(project, () -> registerAssetInWriteLane(safeRequest, events));
    }

    @Override
    public MoveAssetResult moveAsset(MoveAssetRequest request) {
        final MoveAssetRequest safeRequest = Objects.requireNonNull(request, "request");
        final PackerProjectContext project = safeRequest.project();
        final PackerOperationEventEmitter events = new PackerOperationEventEmitter(project, eventSink);
        return writeCoordinator.execute(project, () -> moveAssetInWriteLane(safeRequest, events));
    }

    @Override
    public DeleteAssetResult deleteAsset(DeleteAssetRequest request) {
        final DeleteAssetRequest safeRequest = Objects.requireNonNull(request, "request");
        final PackerProjectContext project = safeRequest.project();
        final PackerOperationEventEmitter events = new PackerOperationEventEmitter(project, eventSink);
        return writeCoordinator.execute(project, () -> deleteAssetInWriteLane(safeRequest, events));
    }

    @Override
    public UpdateAssetBuildParticipationResult updateAssetBuildParticipation(UpdateAssetBuildParticipationRequest request) {
        final UpdateAssetBuildParticipationRequest safeRequest = Objects.requireNonNull(request, "request");
        final PackerProjectContext project = safeRequest.project();
        final PackerOperationEventEmitter events = new PackerOperationEventEmitter(project, eventSink);
        return writeCoordinator.execute(project, () -> updateAssetBuildParticipationInWriteLane(safeRequest, events));
    }

    private CreateAssetResult createAssetInWriteLane(
            CreateAssetRequest request,
            PackerOperationEventEmitter events) {
        final PackerProjectContext project = request.project();
        workspaceFoundation.initWorkspace(new InitWorkspaceRequest(project));

        final String relativeAssetRoot = normalizeRelativeAssetRoot(request.assetRoot());
        if (relativeAssetRoot == null) {
            return failureResult(events, "Asset root must stay inside assets/ and use a non-blank relative path.", null, null, List.of());
        }

        if (request.assetFamily() == AssetFamilyCatalog.UNKNOWN) {
            return failureResult(events, "Asset type is required.", null, null, List.of());
        }
        if (request.outputFormat() == OutputFormatCatalog.UNKNOWN) {
            return failureResult(events, "Output format is required.", null, null, List.of());
        }
        if (request.outputFormat().assetFamily() != request.assetFamily()) {
            return failureResult(events, "Output format is not supported for the selected asset type.", null, null, List.of(request.assetName()));
        }
        if (!request.outputFormat().supports(request.outputCodec())) {
            return failureResult(events, "Output codec is not supported for the selected output format.", null, null, List.of(request.assetName()));
        }

        final Path assetRoot = PackerWorkspacePaths.assetRoot(project, relativeAssetRoot);
        if (!assetRoot.startsWith(PackerWorkspacePaths.assetsRoot(project))) {
            return failureResult(events, "Asset root must stay inside assets/.", null, null, List.of());
        }

        final Path manifestPath = assetRoot.resolve("asset.json");
        try {
            final PackerRegistryState registry = workspaceFoundation.loadRegistry(project);
            final boolean rootAlreadyRegistered = registry.assets().stream()
                    .map(entry -> PackerWorkspacePaths.assetRoot(project, entry.root()))
                    .anyMatch(assetRoot::equals);
            if (rootAlreadyRegistered) {
                return failureResult(events, "Asset root is already registered.", assetRoot, manifestPath, List.of(relativeAssetRoot));
            }

            if (Files.isRegularFile(manifestPath)) {
                return failureResult(events, "Asset root already contains asset.json.", assetRoot, manifestPath, List.of(relativeAssetRoot));
            }
            if (Files.isDirectory(assetRoot)) {
                try (Stream<Path> children = Files.list(assetRoot)) {
                    if (children.findAny().isPresent()) {
                        return failureResult(events, "Asset root already exists and is not empty.", assetRoot, manifestPath, List.of(relativeAssetRoot));
                    }
                }
            }

            final PackerRegistryEntry entry = workspaceFoundation.allocateIdentity(project, registry, assetRoot);
            Files.createDirectories(assetRoot);
            writeManifest(manifestPath, request, entry.assetUuid());
            writeStudioSupportFiles(assetRoot, request);
            final PackerRegistryState updated = workspaceFoundation.appendAllocatedEntry(registry, entry);
            workspaceFoundation.saveRegistry(project, updated);
            final var runtime = runtimeRegistry.update(project, (snapshot, generation) -> runtimePatchService.afterCreateAsset(
                    snapshot,
                    generation,
                    updated,
                    entry,
                    assetRoot,
                    manifestPath));
            saveRuntimeCache(project, runtime.snapshot());
            final CreateAssetResult result = new CreateAssetResult(
                    PackerOperationStatus.SUCCESS,
                    "Asset created: " + relativeAssetRoot,
                    AssetReference.forAssetId(entry.assetId()),
                    assetRoot,
                    manifestPath);
            events.emit(PackerEventKind.ACTION_APPLIED, result.summary(), List.of(relativeAssetRoot));
            return result;
        } catch (IOException exception) {
            return failureResult(
                    events,
                    "Unable to create asset: " + exception.getMessage(),
                    assetRoot,
                    manifestPath,
                    List.of(relativeAssetRoot));
        }
    }

    private PackWorkspaceResult packWorkspaceInWriteLane(PackWorkspaceRequest request) {
        final PackerProjectContext project = request.project();
        final long startedAt = System.currentTimeMillis();
        workspaceFoundation.initWorkspace(new InitWorkspaceRequest(project));

        final PackerRuntimeSnapshot refreshedSnapshot = refreshAfterArtifactIdentityBackfill(project);
        final PackerRuntimeSnapshot frozenSnapshot = createFrozenPackingSnapshot(refreshedSnapshot);
        final List<PackerPackValidationAssetDTO> blockedAssets = frozenSnapshot.assets().stream()
                .filter(this::isEffectivelyIncludedInPackSet)
                .map(this::toPackValidationAssetDTO)
                .filter(PackerPackValidationAssetDTO::blocked)
                .toList();
        if (!blockedAssets.isEmpty()) {
            return new PackWorkspaceResult(
                    PackerOperationStatus.PARTIAL,
                    "Pack validation found blocking diagnostics in " + blockedAssets.size() + " included assets.",
                    new PackerPackExecutionSummaryDTO("assets.pa", 0, System.currentTimeMillis() - startedAt, List.of()));
        }

        final Path buildRoot = project.rootPath().resolve("build").toAbsolutePath().normalize();
        final String operationId = randomOperationId();
        final Path stagingRoot = buildRoot.resolve(".staging").resolve(operationId);
        final Path stagedAssetsPath = stagingRoot.resolve("assets.pa");
        final Path stagedAssetTablePath = stagingRoot.resolve("asset_table.json");
        final Path stagedPreloadPath = stagingRoot.resolve("preload.json");
        final Path stagedAssetTableMetadataPath = stagingRoot.resolve("asset_table_metadata.json");
        try {
            Files.createDirectories(stagingRoot);
            final PackedWorkspaceArtifacts packedWorkspace = buildPackedWorkspaceArtifacts(frozenSnapshot);
            writeJson(stagedAssetTablePath, packedWorkspace.assetTable());
            writeJson(stagedPreloadPath, packedWorkspace.preload());
            writeJson(stagedAssetTableMetadataPath, packedWorkspace.assetTableMetadata());
            Files.write(stagedAssetsPath, packedWorkspace.assetsPaBytes());

            final Path finalAssetsPath = buildRoot.resolve("assets.pa");
            final Path finalAssetTablePath = buildRoot.resolve("asset_table.json");
            final Path finalPreloadPath = buildRoot.resolve("preload.json");
            final Path finalAssetTableMetadataPath = buildRoot.resolve("asset_table_metadata.json");
            Files.createDirectories(buildRoot);
            Files.move(stagedAssetsPath, finalAssetsPath, StandardCopyOption.REPLACE_EXISTING);
            Files.move(stagedAssetTablePath, finalAssetTablePath, StandardCopyOption.REPLACE_EXISTING);
            Files.move(stagedPreloadPath, finalPreloadPath, StandardCopyOption.REPLACE_EXISTING);
            Files.move(stagedAssetTableMetadataPath, finalAssetTableMetadataPath, StandardCopyOption.REPLACE_EXISTING);

            final List<PackerEmittedArtifactDTO> emittedArtifacts = List.of(
                    emittedArtifact("assets.pa", finalAssetsPath, true),
                    emittedArtifact("asset_table.json", finalAssetTablePath, false),
                    emittedArtifact("preload.json", finalPreloadPath, false),
                    emittedArtifact("asset_table_metadata.json", finalAssetTableMetadataPath, false));
            return new PackWorkspaceResult(
                    PackerOperationStatus.SUCCESS,
                    "Pack workspace completed successfully.",
                    new PackerPackExecutionSummaryDTO(
                            "assets.pa",
                            packedWorkspace.packedAssetCount(),
                            System.currentTimeMillis() - startedAt,
                            emittedArtifacts));
        } catch (Exception exception) {
            return new PackWorkspaceResult(
                    PackerOperationStatus.FAILED,
                    "Pack workspace failed: " + exception.getMessage(),
                    new PackerPackExecutionSummaryDTO("assets.pa", 0, System.currentTimeMillis() - startedAt, List.of()));
        }
    }

    private RegisterAssetResult registerAssetInWriteLane(
            RegisterAssetRequest request,
            PackerOperationEventEmitter events) {
        final PackerProjectContext project = request.project();
        workspaceFoundation.initWorkspace(new InitWorkspaceRequest(project));
        final PackerRuntimeSnapshot snapshot = runtimeRegistry.getOrLoad(project).snapshot();
        final PackerRegisterAssetEvaluation evaluation = actionReadService.evaluateRegister(snapshot, project, request.assetReference());
        final Path assetRoot = evaluation.resolved().assetRoot();
        final Path manifestPath = assetRoot.resolve("asset.json");
        final String relativeAssetRoot = relativeAssetRoot(project, assetRoot);
        if (!evaluation.canRegister()) {
            final String summary = Objects.requireNonNullElse(evaluation.reason(), "Asset cannot be registered.");
            return registerFailureResult(events, summary, assetRoot, manifestPath, List.of(relativeAssetRoot));
        }

        final var declaration = evaluation.resolved().runtimeAsset().orElseThrow().parsedDeclaration().declaration();
        try {
            final PackerRegistryState registry = workspaceFoundation.loadRegistry(project);
            final PackerRegistryEntry entry = workspaceFoundation.registerExistingAsset(
                    project,
                    registry,
                    assetRoot,
                    declaration.assetUuid());
            final PackerRegistryState updated = workspaceFoundation.appendAllocatedEntry(registry, entry);
            workspaceFoundation.saveRegistry(project, updated);
            final var runtime = runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterRegisterAsset(
                    currentSnapshot,
                    generation,
                    updated,
                    entry,
                    assetRoot));
            saveRuntimeCache(project, runtime.snapshot());
            final RegisterAssetResult result = new RegisterAssetResult(
                    PackerOperationStatus.SUCCESS,
                    "Asset registered: " + relativeAssetRoot,
                    AssetReference.forAssetId(entry.assetId()),
                    assetRoot,
                    manifestPath);
            events.emit(PackerEventKind.ACTION_APPLIED, result.summary(), List.of(relativeAssetRoot));
            return result;
        } catch (RuntimeException exception) {
            return registerFailureResult(
                    events,
                    "Unable to register asset: " + exception.getMessage(),
                    assetRoot,
                    manifestPath,
                    List.of(relativeAssetRoot));
        }
    }

    private DeleteAssetResult deleteAssetInWriteLane(
            DeleteAssetRequest request,
            PackerOperationEventEmitter events) {
        final PackerProjectContext project = request.project();
        workspaceFoundation.initWorkspace(new InitWorkspaceRequest(project));
        final PackerRuntimeSnapshot snapshot = runtimeRegistry.getOrLoad(project).snapshot();
        final PackerDeleteAssetEvaluation evaluation = actionReadService.evaluateDelete(snapshot, project, request.assetReference());
        final Path assetRoot = evaluation.resolved().assetRoot();
        final Path manifestPath = assetRoot.resolve("asset.json");
        final String relativeAssetRoot = relativeAssetRoot(project, assetRoot);
        if (!evaluation.canDelete()) {
            final String summary = Objects.requireNonNullElse(evaluation.reason(), "Asset cannot be deleted.");
            return deleteFailureResult(events, summary, assetRoot, manifestPath, List.of(relativeAssetRoot));
        }

        try {
            final PackerRegistryState registry = workspaceFoundation.loadRegistry(project);
            final PackerRegistryState updatedRegistry = removeRegistryEntry(registry, evaluation.resolved().registryEntry().orElse(null));
            Files.deleteIfExists(manifestPath);
            if (!updatedRegistry.equals(registry)) {
                workspaceFoundation.saveRegistry(project, updatedRegistry);
            }
            final var runtime = runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterDeleteAsset(
                    currentSnapshot,
                    generation,
                    updatedRegistry,
                    assetRoot));
            saveRuntimeCache(project, runtime.snapshot());
            final DeleteAssetResult result = new DeleteAssetResult(
                    PackerOperationStatus.SUCCESS,
                    "Asset deleted: " + relativeAssetRoot,
                    assetRoot,
                    manifestPath);
            events.emit(PackerEventKind.ACTION_APPLIED, result.summary(), List.of(relativeAssetRoot));
            return result;
        } catch (IOException exception) {
            return deleteFailureResult(
                    events,
                    "Unable to delete asset: " + exception.getMessage(),
                    assetRoot,
                    manifestPath,
                    List.of(relativeAssetRoot));
        }
    }

    private UpdateAssetBuildParticipationResult updateAssetBuildParticipationInWriteLane(
            UpdateAssetBuildParticipationRequest request,
            PackerOperationEventEmitter events) {
        final PackerProjectContext project = request.project();
        workspaceFoundation.initWorkspace(new InitWorkspaceRequest(project));
        final PackerRuntimeSnapshot snapshot = runtimeRegistry.getOrLoad(project).snapshot();
        final PackerDeleteAssetEvaluation evaluation = actionReadService.evaluateDelete(snapshot, project, request.assetReference());
        final Path assetRoot = evaluation.resolved().assetRoot();
        final String relativeAssetRoot = relativeAssetRoot(project, assetRoot);
        if (evaluation.resolved().registryEntry().isEmpty()) {
            return new UpdateAssetBuildParticipationResult(
                    PackerOperationStatus.FAILED,
                    "Asset must be registered before changing build participation.",
                    null);
        }
        if (!evaluation.canDelete()) {
            return new UpdateAssetBuildParticipationResult(
                    PackerOperationStatus.FAILED,
                    Objects.requireNonNullElse(evaluation.reason(), "Asset build participation cannot be updated."),
                    null);
        }

        try {
            if (request.includedInBuild() && !sceneBankCanEnterBuild(assetRoot)) {
                return new UpdateAssetBuildParticipationResult(
                        PackerOperationStatus.FAILED,
                        "Scene Bank must enable at least one layer before entering the build.",
                        null);
            }
            final PackerRegistryEntry currentEntry = evaluation.resolved().registryEntry().orElseThrow();
            final PackerRegistryEntry updatedEntry = new PackerRegistryEntry(
                    currentEntry.assetId(),
                    currentEntry.assetUuid(),
                    currentEntry.root(),
                    request.includedInBuild());
            final PackerRegistryState registry = workspaceFoundation.loadRegistry(project);
            final PackerRegistryState updatedRegistry = replaceRegistryEntryRoot(registry, updatedEntry);
            workspaceFoundation.saveRegistry(project, updatedRegistry);
            runtimeRegistry.update(project, (currentSnapshot, generation) -> new PackerRuntimeSnapshot(
                    generation,
                    updatedRegistry,
                    currentSnapshot.assets(),
                    currentSnapshot.cacheState()));
            final UpdateAssetBuildParticipationResult result = new UpdateAssetBuildParticipationResult(
                    PackerOperationStatus.SUCCESS,
                    (request.includedInBuild() ? "Included" : "Excluded") + " asset in build: " + relativeAssetRoot,
                    AssetReference.forAssetId(updatedEntry.assetId()));
            events.emit(PackerEventKind.ACTION_APPLIED, result.summary(), List.of(relativeAssetRoot));
            return result;
        } catch (RuntimeException exception) {
            return new UpdateAssetBuildParticipationResult(
                    PackerOperationStatus.FAILED,
                    "Unable to update asset build participation: " + exception.getMessage(),
                    null);
        }
    }

    private MoveAssetResult moveAssetInWriteLane(
            MoveAssetRequest request,
            PackerOperationEventEmitter events) {
        final PackerProjectContext project = request.project();
        workspaceFoundation.initWorkspace(new InitWorkspaceRequest(project));
        final PackerRuntimeSnapshot snapshot = runtimeRegistry.getOrLoad(project).snapshot();
        final PackerMoveAssetEvaluation evaluation = actionReadService.evaluateMove(snapshot, project, request.assetReference(), request.targetRoot());
        if (!evaluation.canMove()) {
            return moveFailureResult(
                    events,
                    Objects.requireNonNullElse(evaluation.reason(), "Asset cannot be moved."),
                    evaluation.resolved().assetRoot(),
                    evaluation.resolved().assetRoot().resolve("asset.json"),
                    List.of(relativeAssetRoot(project, evaluation.resolved().assetRoot())));
        }

        final Path sourceRoot = evaluation.resolved().assetRoot();
        final Path targetRoot = evaluation.targetRoot().orElseThrow();
        final Path targetManifestPath = evaluation.targetManifestPath().orElseThrow();
        final String sourceRelativeRoot = relativeAssetRoot(project, sourceRoot);
        final String targetRelativeRoot = evaluation.targetRelativeRoot().orElseThrow();
        try {
            Files.createDirectories(targetRoot.getParent());
            Files.move(sourceRoot, targetRoot);

            final PackerRegistryState registry = workspaceFoundation.loadRegistry(project);
            final Optional<PackerRegistryEntry> updatedEntry = evaluation.resolved().registryEntry()
                    .map(entry -> new PackerRegistryEntry(entry.assetId(), entry.assetUuid(), targetRelativeRoot, entry.includedInBuild()));
            final PackerRegistryState updatedRegistry = updatedEntry
                    .map(entry -> replaceRegistryEntryRoot(registry, entry))
                    .orElse(registry);
            if (!updatedRegistry.equals(registry)) {
                workspaceFoundation.saveRegistry(project, updatedRegistry);
            }
            final var runtime = runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterMoveAsset(
                    currentSnapshot,
                    generation,
                    updatedRegistry,
                    updatedEntry,
                    sourceRoot,
                    targetRoot,
                    targetManifestPath));
            saveRuntimeCache(project, runtime.snapshot());
            final AssetReference canonicalReference = updatedEntry
                    .map(packerRegistryEntry -> AssetReference.forAssetId(packerRegistryEntry.assetId()))
                    .orElseGet(() -> AssetReference.forRelativeAssetRoot(targetRelativeRoot));
            final MoveAssetResult result = new MoveAssetResult(
                    PackerOperationStatus.SUCCESS,
                    "Asset moved: " + sourceRelativeRoot + " -> " + targetRelativeRoot,
                    canonicalReference,
                    targetRoot,
                    targetManifestPath);
            events.emit(PackerEventKind.ACTION_APPLIED, result.summary(), List.of(sourceRelativeRoot, targetRelativeRoot));
            return result;
        } catch (IOException exception) {
            return moveFailureResult(
                    events,
                    "Unable to move asset: " + exception.getMessage(),
                    sourceRoot,
                    sourceRoot.resolve("asset.json"),
                    List.of(sourceRelativeRoot, targetRelativeRoot));
        }
    }

    private CreateAssetResult failureResult(
            PackerOperationEventEmitter events,
            String summary,
            Path assetRoot,
            Path manifestPath,
            List<String> affectedAssets) {
        final CreateAssetResult result = new CreateAssetResult(
                PackerOperationStatus.FAILED,
                summary,
                null,
                assetRoot,
                manifestPath);
        events.emit(PackerEventKind.ACTION_FAILED, result.summary(), affectedAssets);
        return result;
    }

    private RegisterAssetResult registerFailureResult(
            PackerOperationEventEmitter events,
            String summary,
            Path assetRoot,
            Path manifestPath,
            List<String> affectedAssets) {
        final RegisterAssetResult result = new RegisterAssetResult(
                PackerOperationStatus.FAILED,
                summary,
                null,
                assetRoot,
                manifestPath);
        events.emit(PackerEventKind.ACTION_FAILED, result.summary(), affectedAssets);
        return result;
    }

    private DeleteAssetResult deleteFailureResult(
            PackerOperationEventEmitter events,
            String summary,
            Path assetRoot,
            Path manifestPath,
            List<String> affectedAssets) {
        final DeleteAssetResult result = new DeleteAssetResult(
                PackerOperationStatus.FAILED,
                summary,
                assetRoot,
                manifestPath);
        events.emit(PackerEventKind.ACTION_FAILED, result.summary(), affectedAssets);
        return result;
    }

    private MoveAssetResult moveFailureResult(
            PackerOperationEventEmitter events,
            String summary,
            Path assetRoot,
            Path manifestPath,
            List<String> affectedAssets) {
        final MoveAssetResult result = new MoveAssetResult(
                PackerOperationStatus.FAILED,
                summary,
                null,
                assetRoot,
                manifestPath);
        events.emit(PackerEventKind.ACTION_FAILED, result.summary(), affectedAssets);
        return result;
    }

    private void writeManifest(Path manifestPath, CreateAssetRequest request, String assetUuid) throws IOException {
        final Map<String, Object> manifest = new LinkedHashMap<>();
        manifest.put("schema_version", 1);
        manifest.put("asset_uuid", assetUuid);
        manifest.put("name", request.assetName());
        manifest.put("type", request.assetFamily().manifestType());
        manifest.put("output", Map.of(
                "format", request.outputFormat().manifestValue(),
                "codec", request.outputCodec().manifestValue(),
                "pipeline", Map.of()));
        manifest.put("preload", Map.of("enabled", request.preloadEnabled()));
        mapper.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
    }

    private void writeStudioSupportFiles(Path assetRoot, CreateAssetRequest request) throws IOException {
        if (request.assetFamily() != AssetFamilyCatalog.SCENE_BANK) {
            return;
        }
        final Map<String, Object> supportFile = new LinkedHashMap<>();
        supportFile.put("schema_version", 1);
        supportFile.put("map_width", 16);
        supportFile.put("map_height", 16);
        supportFile.put("layer_count", 4);
        final List<Map<String, Object>> layers = new ArrayList<>();
        for (int index = 1; index <= 4; index += 1) {
            layers.add(Map.of(
                    "index", index,
                    "enabled", false,
                    "name", "Layer " + index,
                    "width", 16,
                    "height", 16,
                    "tilemap", "scene.tmx",
                    "tileset_asset_root", "",
                    "parallax_factor_x", 1.0,
                    "parallax_factor_y", 1.0,
                    "default_palette_id", 0));
        }
        supportFile.put("layers", layers);
        mapper.writerWithDefaultPrettyPrinter().writeValue(
                assetRoot.resolve(SCENE_BANK_SUPPORT_FILE).toFile(),
                supportFile);
    }

    private boolean sceneBankCanEnterBuild(Path assetRoot) {
        final Path supportFilePath = assetRoot.resolve(SCENE_BANK_SUPPORT_FILE);
        if (!Files.isRegularFile(supportFilePath)) {
            return true;
        }
        try {
            final JsonNode root = mapper.readTree(supportFilePath.toFile());
            if (!(root instanceof ObjectNode objectNode)) {
                return false;
            }
            final JsonNode layersNode = objectNode.get("layers");
            if (!(layersNode instanceof ArrayNode layers)) {
                return false;
            }
            for (JsonNode layer : layers) {
                if (layer.path("enabled").asBoolean(false)) {
                    return true;
                }
            }
            return false;
        } catch (IOException exception) {
            return false;
        }
    }

    private String normalizeRelativeAssetRoot(String candidate) {
        final String raw = Objects.requireNonNullElse(candidate, "").trim().replace('\\', '/');
        if (raw.isBlank()) {
            return null;
        }
        final Path normalized = Path.of(raw).normalize();
        if (normalized.isAbsolute() || normalized.startsWith("..")) {
            return null;
        }
        final String value = normalized.toString().replace('\\', '/');
        return value.isBlank() ? null : value;
    }

    private PackerAssetSummary buildSummary(
            PackerProjectContext project,
            Path assetRoot,
            PackerRegistryEntry registryEntry,
            PackerAssetDeclarationParseResult parsed) {
        final String assetName = parsed.declaration() != null
                ? parsed.declaration().name()
                : assetRoot.getFileName().toString();
        final AssetFamilyCatalog assetFamily = parsed.declaration() != null
                ? parsed.declaration().assetFamily()
                : AssetFamilyCatalog.UNKNOWN;
        final boolean preload = parsed.declaration() != null && parsed.declaration().preloadEnabled();
        final boolean hasDiagnostics = !parsed.diagnostics().isEmpty();
        final PackerAssetState state = registryEntry == null
                ? PackerAssetState.UNREGISTERED
                : PackerAssetState.REGISTERED;
        final PackerBuildParticipation buildParticipation = state == PackerAssetState.REGISTERED
                ? (registryEntry.includedInBuild() ? PackerBuildParticipation.INCLUDED : PackerBuildParticipation.EXCLUDED)
                : PackerBuildParticipation.EXCLUDED;
        return new PackerAssetSummary(
                assetReferenceFor(project, assetRoot, registryEntry),
                new PackerAssetIdentity(
                        registryEntry == null ? null : registryEntry.assetId(),
                        registryEntry == null
                                ? parsed.declaration() == null ? null : parsed.declaration().assetUuid()
                                : registryEntry.assetUuid(),
                        assetName,
                        assetRoot),
                state,
                buildParticipation,
                assetFamily,
                preload,
                hasDiagnostics);
    }

    private List<PackerDiagnostic> identityMismatchDiagnostics(
            PackerRegistryEntry registryEntry,
            PackerAssetDeclarationParseResult parsed,
            Path manifestPath) {
        if (registryEntry == null || parsed.declaration() == null || registryEntry.assetUuid().equals(parsed.declaration().assetUuid())) {
            return List.of();
        }
        return List.of(new PackerDiagnostic(
                PackerDiagnosticSeverity.ERROR,
                PackerDiagnosticCategory.STRUCTURAL,
                "Field 'asset_uuid' does not match the registered asset identity.",
                manifestPath,
                true));
    }

    private AssetReference assetReferenceFor(
            PackerProjectContext project,
            Path assetRoot,
            PackerRegistryEntry registryEntry) {
        if (registryEntry != null) {
            return AssetReference.forAssetId(registryEntry.assetId());
        }
        return AssetReference.forRelativeAssetRoot(PackerWorkspacePaths.assetsRoot(project)
                .relativize(assetRoot.toAbsolutePath().normalize())
                .toString()
                .replace('\\', '/'));
    }

    private String relativeAssetRoot(PackerProjectContext project, Path assetRoot) {
        return PackerWorkspacePaths.relativeAssetRoot(project, assetRoot).replace('\\', '/');
    }

    private PackerPackSummaryAssetDTO toPackSummaryAssetDTO(PackerRuntimeAsset runtimeAsset) {
        final PackerRegistryEntry registryEntry = runtimeAsset.registryEntry()
                .orElseThrow(() -> new IllegalArgumentException("pack summary asset must be registered"));
        final PackerAssetDeclarationParseResult parsed = runtimeAsset.parsedDeclaration();
        final PackerAssetDeclaration declaration = parsed.declaration();
        final String assetName = declaration != null
                ? declaration.name()
                : runtimeAsset.assetRoot().getFileName().toString();
        final AssetFamilyCatalog assetFamily = declaration != null
                ? declaration.assetFamily()
                : AssetFamilyCatalog.UNKNOWN;
        return new PackerPackSummaryAssetDTO(
                registryEntry.assetId(),
                assetName,
                assetFamily,
                declaration != null ? declaration.artifacts().size() : 0,
                declaration != null ? declaration.outputFormat() : OutputFormatCatalog.UNKNOWN,
                declaration != null ? declaration.outputMetadata() : Map.of(),
                declaration != null ? PackerReadMessageMapper.normalizeMetadata(declaration.outputPipelineMetadata()) : Map.of(),
                0L);
    }

    private PackerPackValidationAssetDTO toPackValidationAssetDTO(PackerRuntimeAsset runtimeAsset) {
        final PackerRegistryEntry registryEntry = runtimeAsset.registryEntry()
                .orElseThrow(() -> new IllegalArgumentException("pack validation asset must be registered"));
        final PackerAssetDeclarationParseResult parsed = runtimeAsset.parsedDeclaration();
        final PackerAssetDeclaration declaration = parsed.declaration();
        final String assetName = declaration != null
                ? declaration.name()
                : runtimeAsset.assetRoot().getFileName().toString();
        final List<PackerDiagnostic> blockingDiagnostics = Stream.of(
                        parsed.diagnostics(),
                        runtimeAsset.walkDiagnostics(),
                        collectPackRelevantWalkFileDiagnostics(runtimeAsset),
                        glyphMaterialSupport.validate(runtimeAsset),
                        packExecutionSupportDiagnostics(parsed.declaration(), runtimeAsset.manifestPath()),
                        identityMismatchDiagnostics(registryEntry, parsed, runtimeAsset.manifestPath()))
                .flatMap(Collection::stream)
                .filter(PackerDiagnostic::blocking)
                .toList();
        return new PackerPackValidationAssetDTO(
                registryEntry.assetId(),
                assetName,
                runtimeAsset.assetRoot(),
                0L,
                PackerReadMessageMapper.toDiagnosticDTOs(blockingDiagnostics));
    }

    private List<PackerDiagnostic> collectPackRelevantWalkFileDiagnostics(PackerRuntimeAsset runtimeAsset) {
        final List<PackerDiagnostic> diagnostics = new ArrayList<>();
        final Set<String> selectedPaths = runtimeAsset.parsedDeclaration().declaration() == null
                ? Set.of()
                : runtimeAsset.parsedDeclaration().declaration().artifacts().stream()
                        .map(PackerAssetArtifactSelection::file)
                        .collect(java.util.stream.Collectors.toSet());
        for (PackerRuntimeWalkFile file : runtimeAsset.walkProjection().buildCandidateFiles()) {
            if (!selectedPaths.isEmpty() && !selectedPaths.contains(file.relativePath())) {
                continue;
            }
            diagnostics.addAll(file.diagnostics());
        }
        return diagnostics;
    }

    private List<PackerDiagnostic> packExecutionSupportDiagnostics(
            PackerAssetDeclaration declaration,
            Path manifestPath) {
        if (declaration == null) {
            return List.of();
        }
        if (declaration.assetFamily() == AssetFamilyCatalog.GLYPH_BANK
                || declaration.assetFamily() == AssetFamilyCatalog.SCENE_BANK) {
            return List.of();
        }
        return List.of(new PackerDiagnostic(
                PackerDiagnosticSeverity.ERROR,
                PackerDiagnosticCategory.STRUCTURAL,
                "Pack execution is not implemented yet for asset family " + declaration.assetFamily().manifestType() + ".",
                manifestPath,
                true));
    }

    private void saveRuntimeCache(PackerProjectContext project, PackerRuntimeSnapshot snapshot) {
        cacheRepository.save(project, snapshot.cacheState());
    }

    private PackerRuntimeSnapshot refreshAfterArtifactIdentityBackfill(PackerProjectContext project) {
        final PackerRuntimeSnapshot snapshot = runtimeRegistry.refresh(project).snapshot();
        return backfillArtifactIdentity(snapshot)
                ? runtimeRegistry.refresh(project).snapshot()
                : snapshot;
    }

    private PackerRuntimeSnapshot backfillLoadedArtifactIdentity(PackerProjectContext project) {
        final PackerRuntimeSnapshot snapshot = runtimeRegistry.getOrLoad(project).snapshot();
        return backfillArtifactIdentity(snapshot)
                ? runtimeRegistry.refresh(project).snapshot()
                : snapshot;
    }

    private boolean backfillArtifactIdentity(PackerRuntimeSnapshot snapshot) {
        boolean changed = false;
        for (PackerRuntimeAsset runtimeAsset : snapshot.assets()) {
            try {
                changed = backfillArtifactIdentity(runtimeAsset) || changed;
            } catch (IOException exception) {
                throw new IllegalStateException("Unable to update artifact fingerprints for " + runtimeAsset.manifestPath(), exception);
            }
        }
        return changed;
    }

    private boolean backfillArtifactIdentity(PackerRuntimeAsset runtimeAsset) throws IOException {
        if (runtimeAsset.parsedDeclaration().declaration() == null || runtimeAsset.parsedDeclaration().declaration().artifacts().isEmpty()) {
            return false;
        }
        final Path manifestPath = runtimeAsset.manifestPath();
        if (!Files.isRegularFile(manifestPath)) {
            return false;
        }
        final JsonNode rawManifest = mapper.readTree(manifestPath.toFile());
        if (!(rawManifest instanceof ObjectNode manifest) || !(manifest.path("artifacts") instanceof ArrayNode artifactsNode)) {
            return false;
        }

        final Map<String, PackerRuntimeWalkFile> walkFilesByPath = new LinkedHashMap<>();
        runtimeAsset.walkProjection().buildCandidateFiles().forEach(file -> walkFilesByPath.put(file.relativePath(), file));
        final Set<String> artifactIds = new HashSet<>();
        for (JsonNode artifactNode : artifactsNode) {
            if (artifactNode.isObject()) {
                final String artifactId = artifactNode.path("artifact_id").asText("").trim();
                if (!artifactId.isBlank()) {
                    artifactIds.add(artifactId);
                }
            }
        }

        boolean changed = false;
        for (JsonNode artifactNode : artifactsNode) {
            if (!(artifactNode instanceof ObjectNode artifactObject)) {
                continue;
            }
            final String file = artifactObject.path("file").asText("").trim();
            if (file.isBlank()) {
                continue;
            }
            if (artifactObject.path("artifact_id").asText("").isBlank()) {
                artifactObject.put("artifact_id", nextArtifactId(artifactIds));
                changed = true;
            }
            final String fingerprint = Optional.ofNullable(walkFilesByPath.get(file))
                    .map(PackerRuntimeWalkFile::fingerprint)
                    .orElse(null);
            if (fingerprint != null && !fingerprint.isBlank() && !fingerprint.equals(artifactObject.path("fingerprint").asText(null))) {
                artifactObject.put("fingerprint", fingerprint);
                changed = true;
            }
        }
        if (changed) {
            mapper.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
        }
        return changed;
    }

    private PackerRuntimeSnapshot createFrozenPackingSnapshot(PackerRuntimeSnapshot snapshot) {
        final PackerRuntimeAssetMaterializer materializer = new PackerRuntimeAssetMaterializer(new PackerAssetWalker(mapper));
        final List<PackerRuntimeAsset> assets = snapshot.assets().stream()
                .map(asset -> rematerializeForPacking(snapshot, asset, materializer))
                .toList();
        return new PackerRuntimeSnapshot(snapshot.generation(), snapshot.registry(), assets, snapshot.cacheState());
    }

    private PackerRuntimeAsset rematerializeForPacking(
            PackerRuntimeSnapshot snapshot,
            PackerRuntimeAsset asset,
            PackerRuntimeAssetMaterializer materializer) {
        if (!isEffectivelyIncludedInPackSet(asset) || !asset.parsedDeclaration().valid()) {
            return asset;
        }
        return materializer.materialize(
                asset.assetRoot(),
                asset.manifestPath(),
                asset.registryEntry(),
                asset.parsedDeclaration(),
                asset.registryEntry().flatMap(entry -> snapshot.cacheState().findAsset(entry.assetId())),
                PackerRuntimeMaterializationConfig.packingBuild()).runtimeAsset();
    }

    private PackedWorkspaceArtifacts buildPackedWorkspaceArtifacts(PackerRuntimeSnapshot snapshot) throws IOException {
        final List<PackedAsset> packedAssets = snapshot.assets().stream()
                .filter(this::isEffectivelyIncludedInPackSet)
                .sorted(Comparator.comparingInt(asset -> asset.registryEntry().get().assetId()))
                .map(this::packRuntimeAsset)
                .toList();

        final List<Map<String, Object>> assetTable = new ArrayList<>();
        final List<Map<String, Object>> preload = new ArrayList<>();
        final List<Map<String, Object>> assetTableMetadata = new ArrayList<>();
        final Map<String, Integer> nextPreloadSlotByBankType = new HashMap<>();
        final ByteArrayOutputStream payload = new ByteArrayOutputStream();
        for (PackedAsset packedAsset : packedAssets) {
            final int offset = payload.size();
            payload.write(packedAsset.payload());
            assetTable.add(new LinkedHashMap<>(Map.of(
                    "asset_id", packedAsset.assetId(),
                    "asset_name", packedAsset.assetName(),
                    "bank_type", packedAsset.bankType(),
                    "offset", offset,
                    "size", packedAsset.payload().length,
                    "decoded_size", packedAsset.decodedSize(),
                    "codec", packedAsset.codec(),
                    "metadata", packedAsset.runtimeMetadata())));
            if (packedAsset.preloadEnabled()) {
                final int slot = nextPreloadSlotByBankType.getOrDefault(packedAsset.bankType(), 0);
                nextPreloadSlotByBankType.put(packedAsset.bankType(), slot + 1);
                preload.add(new LinkedHashMap<>(Map.of(
                        "asset_id", packedAsset.assetId(),
                        "slot", slot)));
            }
            assetTableMetadata.add(new LinkedHashMap<>(Map.of(
                    "asset_id", packedAsset.assetId(),
                    "metadata", packedAsset.toolingMetadata())));
        }

        final byte[] headerBytes = canonicalJsonBytes(Map.of(
                "asset_table", assetTable,
                "preload", preload));
        final byte[] preludeBytes = buildPrelude(headerBytes.length);
        final ByteArrayOutputStream assetsPa = new ByteArrayOutputStream();
        assetsPa.write(preludeBytes);
        assetsPa.write(headerBytes);
        assetsPa.write(payload.toByteArray());
        return new PackedWorkspaceArtifacts(
                assetsPa.toByteArray(),
                assetTable,
                preload,
                assetTableMetadata,
                packedAssets.size());
    }

    private PackedAsset packRuntimeAsset(PackerRuntimeAsset runtimeAsset) {
        final PackerAssetDeclaration declaration = runtimeAsset.parsedDeclaration().declaration();
        if (declaration == null) {
            throw new IllegalStateException("Unsupported pack output family for current implementation.");
        }
        return switch (declaration.assetFamily()) {
            case GLYPH_BANK -> packGlyphBank(runtimeAsset, declaration);
            case SCENE_BANK -> packSceneBank(runtimeAsset, declaration);
            default -> throw new IllegalStateException("Unsupported pack output family for current implementation.");
        };
    }

    private boolean isEffectivelyIncludedInPackSet(PackerRuntimeAsset runtimeAsset) {
        if (runtimeAsset.registryEntry().isEmpty() || !runtimeAsset.registryEntry().get().includedInBuild()) {
            return false;
        }
        final PackerAssetDeclaration declaration = runtimeAsset.parsedDeclaration().declaration();
        if (declaration == null) {
            return true;
        }
        if (declaration.assetFamily() == AssetFamilyCatalog.SCENE_BANK) {
            return true;
        }
        return !declaration.artifacts().isEmpty();
    }

    private PackedAsset packGlyphBank(PackerRuntimeAsset runtimeAsset, PackerAssetDeclaration declaration) {
        final int tileSize = parseTileSize(declaration.outputMetadata().get("tile_size"));
        final int width = 256;
        final int height = 256;
        final int tilesPerRow = width / tileSize;
        final byte[] sheetPixels = new byte[width * height];
        final Map<String, PackerRuntimeWalkFile> selectedByPath = new LinkedHashMap<>();
        runtimeAsset.walkProjection().buildCandidateFiles().forEach(file -> selectedByPath.put(file.relativePath(), file));
        final Map<Integer, Integer> declaredPaletteIndexByRgb = declaredPaletteIndexByRgb(declaration);

        declaration.artifacts().stream()
                .sorted(Comparator.comparingInt(PackerAssetArtifactSelection::index))
                .forEach(artifact -> {
                    final PackerRuntimeWalkFile walkFile = selectedByPath.get(artifact.file());
                    if (walkFile == null) {
                        throw new IllegalStateException("Selected tile artifact is missing from packing snapshot: " + artifact.file());
                    }
                    final Object tileValue = walkFile.metadata().get("tile");
                    if (!(tileValue instanceof PackerTileIndexedV1 tile)) {
                        throw new IllegalStateException("Selected tile artifact is missing normalized tile metadata: " + artifact.file());
                    }
                    final byte[] paletteIndices = remapTilePaletteIndices(
                            tile,
                            walkFile.metadata().get("palette"),
                            artifact.file(),
                            declaredPaletteIndexByRgb);
                    final int tileX = (artifact.index() % tilesPerRow) * tileSize;
                    final int tileY = (artifact.index() / tilesPerRow) * tileSize;
                    for (int localY = 0; localY < tile.height(); localY += 1) {
                        for (int localX = 0; localX < tile.width(); localX += 1) {
                            final int sourceOffset = localY * tile.width() + localX;
                            final int targetOffset = (tileY + localY) * width + (tileX + localX);
                            sheetPixels[targetOffset] = paletteIndices[sourceOffset];
                        }
                    }
                });

        final byte[] packedPixels = packNibbles(sheetPixels);
        final byte[] paletteBytes = emitGlyphBankPalettes(declaration);
        final byte[] payload = new byte[packedPixels.length + paletteBytes.length];
        System.arraycopy(packedPixels, 0, payload, 0, packedPixels.length);
        System.arraycopy(paletteBytes, 0, payload, packedPixels.length, paletteBytes.length);

        final LinkedHashMap<String, Object> runtimeMetadata = new LinkedHashMap<>();
        runtimeMetadata.put("tile_size", tileSize);
        runtimeMetadata.put("width", width);
        runtimeMetadata.put("height", height);
        runtimeMetadata.put("palette_count", 64);
        runtimeMetadata.put("palette_authored", countAuthoredGlyphBankPalettes(declaration));
        runtimeMetadata.put("codec", Map.of());
        declaration.outputMetadata().forEach((key, value) -> {
            if (!"tile_size".equals(key)) {
                runtimeMetadata.putIfAbsent(key, value);
            }
        });

        final LinkedHashMap<String, Object> toolingMetadata = new LinkedHashMap<>(runtimeMetadata);
        toolingMetadata.put("pipeline", normalizeToolingPipelineMetadata(declaration));

        final PackerRegistryEntry registryEntry = runtimeAsset.registryEntry()
                .orElseThrow(() -> new IllegalStateException("Packed runtime asset must be registered"));
        return new PackedAsset(
                registryEntry.assetId(),
                declaration.name(),
                "GLYPH",
                "NONE",
                payload,
                width * height + 2048,
                runtimeMetadata,
                toolingMetadata,
                declaration.preloadEnabled());
    }

    private byte[] remapTilePaletteIndices(
            PackerTileIndexedV1 tile,
            Object paletteMetadata,
            String artifactFile,
            Map<Integer, Integer> declaredPaletteIndexByRgb) {
        final int[] originalArgb8888 = originalPaletteColors(paletteMetadata);
        if (originalArgb8888.length == 0) {
            throw new IllegalStateException("Selected tile artifact is missing palette metadata required for palette remapping: " + artifactFile);
        }
        final byte[] remapped = Arrays.copyOf(tile.paletteIndices(), tile.paletteIndices().length);
        for (int index = 0; index < remapped.length; index += 1) {
            final int paletteIndex = Byte.toUnsignedInt(remapped[index]);
            if (paletteIndex == 0) {
                continue;
            }
            if (paletteIndex > originalArgb8888.length) {
                throw new IllegalStateException("Tile palette index exceeds discovered palette size for artifact: " + artifactFile);
            }
            final int originalArgb = originalArgb8888[paletteIndex - 1];
            final Integer materialIndex = declaredPaletteIndexByRgb.get(rgb24(originalArgb));
            if (materialIndex == null || materialIndex < 1 || materialIndex > 15) {
                throw new IllegalStateException("Unable to resolve canonical palette index for artifact color in " + artifactFile);
            }
            remapped[index] = (byte) (materialIndex & 0x0F);
        }
        return remapped;
    }

    private Map<Integer, Integer> declaredPaletteIndexByRgb(PackerAssetDeclaration declaration) {
        final JsonNode palettesNode = declaration.outputPipelineMetadata().get("palettes");
        if (!(palettesNode instanceof ArrayNode palettesArray) || palettesArray.isEmpty()) {
            throw new IllegalStateException("Glyph packing requires at least one declared pipeline palette.");
        }
        final JsonNode paletteNode = palettesArray.get(0).path("palette").path("originalArgb8888");
        if (!paletteNode.isArray() || paletteNode.isEmpty()) {
            throw new IllegalStateException("Glyph packing requires originalArgb8888 entries in the declared pipeline palette.");
        }
        final LinkedHashMap<Integer, Integer> mapping = new LinkedHashMap<>();
        for (int index = 0; index < paletteNode.size(); index += 1) {
            final JsonNode colorNode = paletteNode.get(index);
            if (!colorNode.canConvertToInt()) {
                throw new IllegalStateException("Declared pipeline palette contains a non-integer originalArgb8888 entry.");
            }
            mapping.put(rgb24(colorNode.intValue()), index + 1);
        }
        return Map.copyOf(mapping);
    }

    private int[] originalPaletteColors(Object paletteMetadata) {
        if (paletteMetadata instanceof PackerPaletteV1 palette) {
            return palette.originalArgb8888();
        }
        if (!(paletteMetadata instanceof Map<?, ?> map)) {
            return new int[0];
        }
        final Object colors = map.get("originalArgb8888");
        if (!(colors instanceof List<?> list)) {
            return new int[0];
        }
        final int[] values = new int[list.size()];
        for (int index = 0; index < list.size(); index += 1) {
            final Object color = list.get(index);
            if (!(color instanceof Number number)) {
                return new int[0];
            }
            values[index] = number.intValue();
        }
        return values;
    }

    private int rgb24(int argb) {
        return argb & 0x00FFFFFF;
    }

    private PackedAsset packSceneBank(PackerRuntimeAsset runtimeAsset, PackerAssetDeclaration declaration) {
        final ScenePackRequest request = runtimeAsset.walkProjection().buildCandidateFiles().stream()
                .map(file -> file.metadata().get("scenePackRequest"))
                .filter(ScenePackRequest.class::isInstance)
                .map(ScenePackRequest.class::cast)
                .findFirst()
                .orElseThrow(() -> new IllegalStateException("Accepted Scene Bank request artifact is missing from the packing snapshot: " + SCENE_PACK_REQUEST_FILE));

        final byte[] payload = encodeScenePackRequest(request);
        final LinkedHashMap<String, Object> runtimeMetadata = new LinkedHashMap<>();
        runtimeMetadata.put("version", request.version());
        runtimeMetadata.put("layer_count", request.layers().size());
        runtimeMetadata.put("codec", Map.of());

        final LinkedHashMap<String, Object> toolingMetadata = new LinkedHashMap<>(runtimeMetadata);
        final PackerRegistryEntry registryEntry = runtimeAsset.registryEntry()
                .orElseThrow(() -> new IllegalStateException("Packed runtime asset must be registered"));
        return new PackedAsset(
                registryEntry.assetId(),
                declaration.name(),
                "SCENE",
                "NONE",
                payload,
                sceneDecodedSize(request),
                runtimeMetadata,
                toolingMetadata,
                declaration.preloadEnabled());
    }

    private int parseTileSize(String value) {
        return switch (Objects.requireNonNullElse(value, "")) {
            case "8x8" -> 8;
            case "16x16" -> 16;
            case "32x32" -> 32;
            default -> throw new IllegalStateException("Unsupported tile_size for tile-bank packing: " + value);
        };
    }

    private byte[] packNibbles(byte[] logicalPixels) {
        final byte[] packed = new byte[(logicalPixels.length + 1) / 2];
        for (int offset = 0; offset < logicalPixels.length; offset += 2) {
            final int high = logicalPixels[offset] & 0x0F;
            final int low = offset + 1 < logicalPixels.length ? logicalPixels[offset + 1] & 0x0F : 0;
            packed[offset / 2] = (byte) ((high << 4) | low);
        }
        return packed;
    }

    private byte[] emitGlyphBankPalettes(PackerAssetDeclaration declaration) {
        final byte[] bytes = new byte[64 * 16 * 2];
        final JsonNode palettesNode = declaration.outputPipelineMetadata().get("palettes");
        if (!(palettesNode instanceof com.fasterxml.jackson.databind.node.ArrayNode palettesArray)) {
            return bytes;
        }
        for (JsonNode declarationNode : palettesArray) {
            final JsonNode indexNode = declarationNode.path("index");
            final JsonNode paletteNode = declarationNode.path("palette");
            if (!indexNode.isInt() || !paletteNode.isObject()) {
                continue;
            }
            final int paletteIndex = indexNode.intValue();
            if (paletteIndex < 0 || paletteIndex >= 64) {
                continue;
            }
            final int paletteBaseOffset = (paletteIndex * 16) * 2;
            bytes[paletteBaseOffset] = (byte) (GLYPH_BANK_COLOR_KEY_RGB565 & 0xFF);
            bytes[paletteBaseOffset + 1] = (byte) ((GLYPH_BANK_COLOR_KEY_RGB565 >>> 8) & 0xFF);
            final JsonNode convertedNode = paletteNode.path("convertedRgb565");
            if (!convertedNode.isArray()) {
                continue;
            }
            for (int colorIndex = 0; colorIndex < Math.min(15, convertedNode.size()); colorIndex += 1) {
                final int rgb565 = convertedNode.get(colorIndex).asInt(0);
                final int baseOffset = ((paletteIndex * 16) + colorIndex + 1) * 2;
                bytes[baseOffset] = (byte) (rgb565 & 0xFF);
                bytes[baseOffset + 1] = (byte) ((rgb565 >>> 8) & 0xFF);
            }
        }
        return bytes;
    }

    private int countAuthoredGlyphBankPalettes(PackerAssetDeclaration declaration) {
        final JsonNode palettesNode = declaration.outputPipelineMetadata().get("palettes");
        return palettesNode instanceof ArrayNode palettesArray ? palettesArray.size() : 0;
    }

    private byte[] encodeScenePackRequest(ScenePackRequest request) {
        final ByteArrayOutputStream output = new ByteArrayOutputStream();
        writeBytes(output, new byte[]{'S', 'C', 'N', 'E'});
        writeShortLE(output, request.version());
        writeShortLE(output, request.layers().size());
        writeIntLE(output, 0);
        for (ScenePackLayerRequest layer : request.layers()) {
            writeByte(output, layer.active() ? 0b0000_0001 : 0);
            writeIntLE(output, layer.glyphAssetId());
            writeByte(output, layer.tileSize());
            writeShortLE(output, 0);
            writeFloatLE(output, layer.parallaxFactorX());
            writeFloatLE(output, layer.parallaxFactorY());
            writeIntLE(output, layer.width());
            writeIntLE(output, layer.height());
            writeIntLE(output, layer.width() * layer.height());
            writeIntLE(output, 0);
            for (ScenePackTileRequest tile : layer.tiles()) {
                int flags = tile.active() ? 0b0000_0001 : 0;
                if (tile.flipX()) {
                    flags |= 0b0000_0010;
                }
                if (tile.flipY()) {
                    flags |= 0b0000_0100;
                }
                writeByte(output, flags);
                writeByte(output, tile.paletteId());
                writeShortLE(output, tile.glyphId());
            }
        }
        return output.toByteArray();
    }

    private int sceneDecodedSize(ScenePackRequest request) {
        int decodedSize = 0;
        for (ScenePackLayerRequest layer : request.layers()) {
            decodedSize += 16 + (layer.tiles().size() * 8);
        }
        return decodedSize;
    }

    private void writeIntLE(ByteArrayOutputStream output, int value) {
        writeBytes(output, ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(value).array());
    }

    private void writeShortLE(ByteArrayOutputStream output, int value) {
        writeBytes(output, ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort((short) value).array());
    }

    private void writeFloatLE(ByteArrayOutputStream output, float value) {
        writeBytes(output, ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putFloat(value).array());
    }

    private void writeByte(ByteArrayOutputStream output, int value) {
        output.write(value & 0xFF);
    }

    private void writeBytes(ByteArrayOutputStream output, byte[] bytes) {
        output.writeBytes(bytes);
    }

    private Map<String, Object> normalizeToolingPipelineMetadata(PackerAssetDeclaration declaration) {
        return new LinkedHashMap<>(PackerReadMessageMapper.normalizeMetadata(declaration.outputPipelineMetadata()));
    }

    private byte[] buildPrelude(int headerLength) {
        final int preludeLength = 32;
        final ByteBuffer buffer = ByteBuffer.allocate(preludeLength).order(ByteOrder.LITTLE_ENDIAN);
        buffer.put((byte) 'A').put((byte) 'S').put((byte) 'P').put((byte) 'A');
        buffer.putInt(1);
        buffer.putInt(headerLength);
        buffer.putLong((long) preludeLength + headerLength);
        buffer.putInt(0);
        buffer.putInt(0);
        buffer.putInt(0);
        return buffer.array();
    }

    private byte[] canonicalJsonBytes(Object value) throws IOException {
        final Object canonical = canonicalizeJsonValue(value);
        return mapper.copy()
                .configure(com.fasterxml.jackson.databind.SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true)
                .writeValueAsBytes(canonical);
    }

    private Object canonicalizeJsonValue(Object value) {
        if (value instanceof Map<?, ?> map) {
            final LinkedHashMap<String, Object> sorted = new LinkedHashMap<>();
            map.entrySet().stream()
                    .sorted(Comparator.comparing(entry -> String.valueOf(entry.getKey())))
                    .forEach(entry -> sorted.put(String.valueOf(entry.getKey()), canonicalizeJsonValue(entry.getValue())));
            return sorted;
        }
        if (value instanceof List<?> list) {
            return list.stream().map(this::canonicalizeJsonValue).toList();
        }
        return value;
    }

    private void writeJson(Path path, Object value) throws IOException {
        Files.createDirectories(path.getParent());
        mapper.writerWithDefaultPrettyPrinter().writeValue(path.toFile(), value);
    }

    private PackerEmittedArtifactDTO emittedArtifact(String label, Path path, boolean canonical) throws IOException {
        return new PackerEmittedArtifactDTO(label, path, canonical, Files.size(path));
    }

    private String randomOperationId() {
        final String alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        final StringBuilder builder = new StringBuilder(20);
        final Random random = new Random();
        for (int index = 0; index < 20; index += 1) {
            builder.append(alphabet.charAt(random.nextInt(alphabet.length())));
        }
        return builder.toString();
    }

    private record PackedAsset(
            int assetId,
            String assetName,
            String bankType,
            String codec,
            byte[] payload,
            int decodedSize,
            Map<String, Object> runtimeMetadata,
            Map<String, Object> toolingMetadata,
            boolean preloadEnabled) {
    }

    private record PackedWorkspaceArtifacts(
            byte[] assetsPaBytes,
            List<Map<String, Object>> assetTable,
            List<Map<String, Object>> preload,
            List<Map<String, Object>> assetTableMetadata,
            int packedAssetCount) {
    }

    private PackerRegistryState removeRegistryEntry(
            PackerRegistryState registry,
            PackerRegistryEntry entry) {
        if (entry == null) {
            return registry;
        }
        final List<PackerRegistryEntry> updatedEntries = registry.assets().stream()
                .filter(candidate -> candidate.assetId() != entry.assetId())
                .toList();
        return registry.withAssets(updatedEntries, registry.nextAssetId());
    }

    private PackerRegistryState replaceRegistryEntryRoot(
            PackerRegistryState registry,
            PackerRegistryEntry updatedEntry) {
        final List<PackerRegistryEntry> updatedEntries = registry.assets().stream()
                .map(candidate -> candidate.assetId() == updatedEntry.assetId() ? updatedEntry : candidate)
                .toList();
        return registry.withAssets(updatedEntries, registry.nextAssetId());
    }

    @Override
    public UpdateAssetContractResponse updateAssetContract(final UpdateAssetContractRequest request) {
        final UpdateAssetContractRequest safeRequest = Objects.requireNonNull(request, "request");
        final PackerProjectContext project = safeRequest.project();
        return writeCoordinator.execute(project, () -> updateAssetContractInWriteLane(safeRequest));
    }

    @Override
    public ApplyBankCompositionResponse applyBankComposition(final ApplyBankCompositionRequest request) {
        final ApplyBankCompositionRequest safeRequest = Objects.requireNonNull(request, "request");
        final PackerProjectContext project = safeRequest.project();
        return writeCoordinator.execute(project, () -> applyBankCompositionInWriteLane(safeRequest));
    }

    @Override
    public ApplyPaletteOverhaulingResponse applyPaletteOverhauling(final ApplyPaletteOverhaulingRequest request) {
        final ApplyPaletteOverhaulingRequest safeRequest = Objects.requireNonNull(request, "request");
        final PackerProjectContext project = safeRequest.project();
        return writeCoordinator.execute(project, () -> applyPaletteOverhaulingInWriteLane(safeRequest));
    }

    private UpdateAssetContractResponse updateAssetContractInWriteLane(UpdateAssetContractRequest request) {
        final PackerProjectContext project = request.project();
        workspaceFoundation.initWorkspace(new InitWorkspaceRequest(project));
        final PackerRuntimeSnapshot snapshot = runtimeRegistry.getOrLoad(project).snapshot();
        final PackerDeleteAssetEvaluation evaluation = actionReadService.evaluateDelete(snapshot, project, request.assetReference());
        if (!evaluation.canDelete()) {
            return new UpdateAssetContractResponse(
                    false,
                    Objects.requireNonNullElse(evaluation.reason(), "Asset contract cannot be updated."));
        }
        if (request.outputCodec() == OutputCodecCatalog.UNKNOWN) {
            return new UpdateAssetContractResponse(false, "Output codec is required.");
        }

        final Path assetRoot = evaluation.resolved().assetRoot();
        final Path manifestPath = assetRoot.resolve("asset.json");
        if (!Files.isRegularFile(manifestPath)) {
            return new UpdateAssetContractResponse(false, "asset.json was not found for the requested asset root.");
        }

        final ObjectNode manifest;
        try {
            final JsonNode rawManifest = mapper.readTree(manifestPath.toFile());
            if (!(rawManifest instanceof ObjectNode objectNode)) {
                return new UpdateAssetContractResponse(false, "asset.json must contain a JSON object at the root.");
            }
            manifest = objectNode;
        } catch (IOException exception) {
            return new UpdateAssetContractResponse(false, "Unable to read asset manifest: " + exception.getMessage());
        }

        final OutputFormatCatalog outputFormat = resolveManifestOutputFormat(manifest);
        if (outputFormat == OutputFormatCatalog.UNKNOWN) {
            return new UpdateAssetContractResponse(false, "Output format is missing or invalid in asset.json.");
        }
        if (!outputFormat.supports(request.outputCodec())) {
            return new UpdateAssetContractResponse(false, "Output codec is not supported for the selected output format.");
        }

        try {
            final String previousContractFingerprint = contractFingerprint(manifest);
            patchManifestContract(manifest, request);
            if (!Objects.equals(previousContractFingerprint, contractFingerprint(manifest))) {
                clearManifestBankComposition(manifest);
            }
            mapper.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
            final var runtime = runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterUpdateAssetContract(
                    currentSnapshot,
                    generation,
                    assetRoot,
                    manifestPath,
                    evaluation.resolved().registryEntry()));
            saveRuntimeCache(project, runtime.snapshot());
            return new UpdateAssetContractResponse(true, null);
        } catch (IOException exception) {
            return new UpdateAssetContractResponse(false, "Unable to update asset contract: " + exception.getMessage());
        } catch (RuntimeException exception) {
            return new UpdateAssetContractResponse(false, "Unable to update runtime snapshot: " + exception.getMessage());
        }
    }

    private ApplyBankCompositionResponse applyBankCompositionInWriteLane(ApplyBankCompositionRequest request) {
        final PackerProjectContext project = request.project();
        workspaceFoundation.initWorkspace(new InitWorkspaceRequest(project));
        final PackerRuntimeSnapshot snapshot = runtimeRegistry.getOrLoad(project).snapshot();
        final PackerDeleteAssetEvaluation evaluation = actionReadService.evaluateDelete(snapshot, project, request.assetReference());
        if (!evaluation.canDelete()) {
            return new ApplyBankCompositionResponse(
                    false,
                    Objects.requireNonNullElse(evaluation.reason(), "Asset bank composition cannot be updated."));
        }

        final Path assetRoot = evaluation.resolved().assetRoot();
        final Path manifestPath = assetRoot.resolve("asset.json");
        if (!Files.isRegularFile(manifestPath)) {
            return new ApplyBankCompositionResponse(false, "asset.json was not found for the requested asset root.");
        }

        final ObjectNode manifest;
        try {
            final JsonNode rawManifest = mapper.readTree(manifestPath.toFile());
            if (!(rawManifest instanceof ObjectNode objectNode)) {
                return new ApplyBankCompositionResponse(false, "asset.json must contain a JSON object at the root.");
            }
            manifest = objectNode;
        } catch (IOException exception) {
            return new ApplyBankCompositionResponse(false, "Unable to read asset manifest: " + exception.getMessage());
        }

        try {
            patchManifestArtifacts(manifest, request, evaluation.resolved().runtimeAsset());
            mapper.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
            final var runtime = runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterUpdateAssetContract(
                    currentSnapshot,
                    generation,
                    assetRoot,
                    manifestPath,
                    evaluation.resolved().registryEntry()));
            saveRuntimeCache(project, runtime.snapshot());
            return new ApplyBankCompositionResponse(true, null);
        } catch (IOException exception) {
            return new ApplyBankCompositionResponse(false, "Unable to update asset bank composition: " + exception.getMessage());
        } catch (RuntimeException exception) {
            return new ApplyBankCompositionResponse(false, "Unable to update runtime snapshot: " + exception.getMessage());
        }
    }

    private ApplyPaletteOverhaulingResponse applyPaletteOverhaulingInWriteLane(ApplyPaletteOverhaulingRequest request) {
        final PackerProjectContext project = request.project();
        workspaceFoundation.initWorkspace(new InitWorkspaceRequest(project));
        final PackerRuntimeSnapshot snapshot = runtimeRegistry.getOrLoad(project).snapshot();
        final PackerDeleteAssetEvaluation evaluation = actionReadService.evaluateDelete(snapshot, project, request.assetReference());
        if (!evaluation.canDelete()) {
            return new ApplyPaletteOverhaulingResponse(
                    false,
                    Objects.requireNonNullElse(evaluation.reason(), "Asset palette overhauling cannot be updated."));
        }
        if (request.selectedPalettes().isEmpty()) {
            return new ApplyPaletteOverhaulingResponse(false, "At least one palette must be selected.");
        }

        final Path assetRoot = evaluation.resolved().assetRoot();
        final Path manifestPath = assetRoot.resolve("asset.json");
        if (!Files.isRegularFile(manifestPath)) {
            return new ApplyPaletteOverhaulingResponse(false, "asset.json was not found for the requested asset root.");
        }

        final ObjectNode manifest;
        try {
            final JsonNode rawManifest = mapper.readTree(manifestPath.toFile());
            if (!(rawManifest instanceof ObjectNode objectNode)) {
                return new ApplyPaletteOverhaulingResponse(false, "asset.json must contain a JSON object at the root.");
            }
            manifest = objectNode;
        } catch (IOException exception) {
            return new ApplyPaletteOverhaulingResponse(false, "Unable to read asset manifest: " + exception.getMessage());
        }

        try {
            patchManifestPipelinePalettes(manifest, request);
            mapper.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
            final var runtime = runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterUpdateAssetContract(
                    currentSnapshot,
                    generation,
                    assetRoot,
                    manifestPath,
                    evaluation.resolved().registryEntry()));
            saveRuntimeCache(project, runtime.snapshot());
            return new ApplyPaletteOverhaulingResponse(true, null);
        } catch (IOException exception) {
            return new ApplyPaletteOverhaulingResponse(false, "Unable to update asset palette overhauling: " + exception.getMessage());
        } catch (RuntimeException exception) {
            return new ApplyPaletteOverhaulingResponse(false, "Unable to update runtime snapshot: " + exception.getMessage());
        }
    }

    private OutputFormatCatalog resolveManifestOutputFormat(ObjectNode manifest) {
        final JsonNode outputNode = manifest.get("output");
        if (!(outputNode instanceof ObjectNode outputObject)) {
            return OutputFormatCatalog.UNKNOWN;
        }
        final JsonNode formatNode = outputObject.get("format");
        if (formatNode == null || !formatNode.isTextual()) {
            return OutputFormatCatalog.UNKNOWN;
        }
        return OutputFormatCatalog.fromManifestValue(formatNode.asText());
    }

    private void patchManifestContract(ObjectNode manifest, UpdateAssetContractRequest request) {
        final ObjectNode preloadNode = mutableObject(manifest, "preload");
        preloadNode.put("enabled", request.preloadEnabled());

        final ObjectNode outputNode = mutableObject(manifest, "output");
        outputNode.put("codec", request.outputCodec().manifestValue());

        final ObjectNode codecConfigurationNode = mutableObject(outputNode, "codec_configuration");
        codecConfigurationNode.removeAll();
        final String selectedCodecPrefix = request.outputCodec().name() + ":";
        request.codecFieldValues().forEach((fieldKey, fieldValue) -> {
            if (fieldKey == null || fieldKey.isBlank() || !fieldKey.startsWith(selectedCodecPrefix)) {
                return;
            }
            final String normalizedFieldKey = fieldKey.substring(selectedCodecPrefix.length()).trim();
            if (normalizedFieldKey.isBlank()) {
                return;
            }
            codecConfigurationNode.put(normalizedFieldKey, Objects.requireNonNullElse(fieldValue, ""));
        });

        final ObjectNode metadataNode = mutableObject(outputNode, "metadata");
        metadataNode.removeAll();
        request.metadataFieldValues().entrySet().stream()
                .sorted(Map.Entry.comparingByKey())
                .forEach(entry -> {
                    if (entry.getKey() == null || entry.getKey().isBlank()) {
                        return;
                    }
                    metadataNode.put(entry.getKey().trim(), Objects.requireNonNullElse(entry.getValue(), ""));
                });
    }

    private void patchManifestArtifacts(
            ObjectNode manifest,
            ApplyBankCompositionRequest request,
            Optional<PackerRuntimeAsset> runtimeAsset) {
        manifest.remove("inputs");

        final Map<String, ExistingArtifactEntry> existingArtifactsByFile = existingArtifactsByFile(manifest.path("artifacts"));
        final Map<String, PackerRuntimeWalkFile> walkFilesByPath = new LinkedHashMap<>();
        runtimeAsset.ifPresent(asset -> asset.walkProjection().buildCandidateFiles().forEach(file -> walkFilesByPath.put(file.relativePath(), file)));
        final var artifactsNode = manifest.putArray("artifacts");
        for (int index = 0; index < request.selectedFiles().size(); index += 1) {
            final String file = Objects.requireNonNullElse(request.selectedFiles().get(index), "").trim();
            if (file.isBlank() || !isTrustedRelativePath(file)) {
                throw new IllegalArgumentException("Selected artifact file paths must stay inside the asset root.");
            }
            final ExistingArtifactEntry existing = existingArtifactsByFile.get(file);
            final var artifactNode = artifactsNode.addObject();
            artifactNode.put("artifact_id", existing != null && existing.artifactId() != null ? existing.artifactId() : nextArtifactId());
            artifactNode.put("file", file);
            artifactNode.put("index", index);
            final String fingerprint = Optional.ofNullable(walkFilesByPath.get(file))
                    .map(PackerRuntimeWalkFile::fingerprint)
                    .orElse(existing == null ? null : existing.fingerprint());
            if (fingerprint != null && !fingerprint.isBlank()) {
                artifactNode.put("fingerprint", fingerprint);
            }
        }
    }

    private Map<String, ExistingArtifactEntry> existingArtifactsByFile(JsonNode artifactsNode) {
        if (!(artifactsNode instanceof ArrayNode artifactsArray)) {
            return Map.of();
        }
        final Map<String, ExistingArtifactEntry> existing = new LinkedHashMap<>();
        for (JsonNode entry : artifactsArray) {
            if (!entry.isObject()) {
                continue;
            }
            final String file = entry.path("file").asText("").trim();
            if (file.isBlank()) {
                continue;
            }
            existing.put(file, new ExistingArtifactEntry(
                    blankToNull(entry.path("artifact_id").asText(null)),
                    blankToNull(entry.path("fingerprint").asText(null))));
        }
        return Map.copyOf(existing);
    }

    private String nextArtifactId() {
        return "art-" + UUID.randomUUID().toString().replace("-", "").substring(0, 12);
    }

    private String nextArtifactId(Set<String> existingArtifactIds) {
        String artifactId = nextArtifactId();
        while (existingArtifactIds.contains(artifactId)) {
            artifactId = nextArtifactId();
        }
        existingArtifactIds.add(artifactId);
        return artifactId;
    }

    private String blankToNull(String value) {
        if (value == null) {
            return null;
        }
        final String trimmed = value.trim();
        return trimmed.isBlank() ? null : trimmed;
    }

    private record ExistingArtifactEntry(
            String artifactId,
            String fingerprint) {
    }

    private void patchManifestPipelinePalettes(ObjectNode manifest, ApplyPaletteOverhaulingRequest request) {
        final ObjectNode outputNode = mutableObject(manifest, "output");
        final ObjectNode pipelineNode = mutableObject(outputNode, "pipeline");
        final ArrayNode palettesNode = pipelineNode.putArray("palettes");
        for (int paletteOrder = 0; paletteOrder < request.selectedPalettes().size(); paletteOrder += 1) {
            final Map<String, Object> palette = request.selectedPalettes().get(paletteOrder);
            final List<Integer> originalArgb8888 = integerList(palette.get("originalArgb8888"));
            final List<Integer> convertedRgb565 = integerList(palette.get("convertedRgb565"));
            if (originalArgb8888.isEmpty() || convertedRgb565.isEmpty()) {
                throw new IllegalArgumentException("Each selected palette must contain originalArgb8888 and convertedRgb565 entries.");
            }
            final ObjectNode paletteNode = palettesNode.addObject();
            paletteNode.put("index", paletteIndex(palette, paletteOrder));
            final ObjectNode payloadNode = paletteNode.putObject("palette");
            final ArrayNode originalNode = payloadNode.putArray("originalArgb8888");
            originalArgb8888.forEach(originalNode::add);
            final ArrayNode convertedNode = payloadNode.putArray("convertedRgb565");
            convertedRgb565.forEach(convertedNode::add);
        }
        if (palettesNode.isEmpty()) {
            throw new IllegalArgumentException("At least one palette must be selected.");
        }
    }

    private int paletteIndex(Map<String, Object> palette, int fallbackIndex) {
        final Object rawIndex = palette.get("index");
        if (rawIndex instanceof Number number && number.intValue() >= 0) {
            return number.intValue();
        }
        return fallbackIndex;
    }

    private List<Integer> integerList(Object value) {
        if (!(value instanceof Iterable<?> iterable)) {
            return List.of();
        }
        final List<Integer> numbers = new ArrayList<>();
        for (Object item : iterable) {
            if (!(item instanceof Number number)) {
                return List.of();
            }
            numbers.add(number.intValue());
        }
        return List.copyOf(numbers);
    }

    private String contractFingerprint(ObjectNode manifest) {
        final JsonNode outputNode = manifest.path("output");
        if (!(outputNode instanceof ObjectNode outputObject)) {
            return PackerContractFingerprint.metadataFingerprint(Map.of());
        }
        final JsonNode metadataNode = outputObject.path("metadata");
        if (!(metadataNode instanceof ObjectNode metadataObject)) {
            return PackerContractFingerprint.metadataFingerprint(Map.of());
        }
        final Map<String, String> metadata = new LinkedHashMap<>();
        metadataObject.fields().forEachRemaining(entry -> {
            if (entry.getKey() == null || entry.getKey().isBlank()) {
                return;
            }
            final JsonNode valueNode = entry.getValue();
            if (valueNode == null || valueNode.isContainerNode()) {
                return;
            }
            metadata.put(entry.getKey().trim(), valueNode.isNull() ? "" : valueNode.asText(""));
        });
        return PackerContractFingerprint.metadataFingerprint(metadata);
    }

    private void clearManifestBankComposition(ObjectNode manifest) {
        manifest.remove("inputs");
        manifest.putArray("artifacts");
    }

    private boolean isTrustedRelativePath(String value) {
        final Path path = Path.of(value).normalize();
        return !path.isAbsolute() && !path.startsWith("..");
    }

    private ObjectNode mutableObject(ObjectNode parent, String fieldName) {
        final JsonNode current = parent.get(fieldName);
        if (current instanceof ObjectNode objectNode) {
            return objectNode;
        }
        final ObjectNode created = mapper.createObjectNode();
        parent.set(fieldName, created);
        return created;
    }
}
