package p.packer.services;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
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.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.PackerRuntimeRegistry;

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

public final class FileSystemPackerWorkspaceService implements PackerWorkspaceService {
    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;

    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");
    }

    @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()
                ? runtimeRegistry.refresh(project).snapshot()
                : 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) {
        return detailsService.getAssetDetails(request);
    }

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

    @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));
    }

    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());
            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 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 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("inputs", Map.of());
        manifest.put("output", Map.of(
                "format", request.outputFormat().manifestValue(),
                "codec", request.outputCodec().manifestValue()));
        manifest.put("preload", Map.of("enabled", request.preloadEnabled()));
        mapper.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
    }

    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 void saveRuntimeCache(PackerProjectContext project, PackerRuntimeSnapshot snapshot) {
        cacheRepository.save(project, snapshot.cacheState());
    }

    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));
    }

    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 {
            patchManifestContract(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 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 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 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;
    }
}
