package p.studio.compiler.services;

import lombok.extern.slf4j.Slf4j;
import p.studio.compiler.messages.BuildingIssueSink;
import p.studio.compiler.messages.FESurfaceContext;
import p.studio.compiler.messages.FrontendPhaseContext;
import p.studio.compiler.models.IRBackend;
import p.studio.compiler.models.IRBackendExecutableFunction;
import p.studio.compiler.models.IRGlobal;
import p.studio.compiler.models.IRGlobalOrigin;
import p.studio.compiler.models.IRGlobalVisibility;
import p.studio.compiler.models.IRHiddenGlobalKind;
import p.studio.compiler.models.IRSyntheticCallableKind;
import p.studio.compiler.pbs.PbsFrontendCompiler;
import p.studio.compiler.pbs.PbsReservedMetadataExtractor;
import p.studio.compiler.pbs.semantics.PbsFlowSemanticsValidator;
import p.studio.compiler.pbs.stdlib.InterfaceModuleLoader;
import p.studio.compiler.pbs.stdlib.ResourceStdlibEnvironmentResolver;
import p.studio.compiler.pbs.stdlib.StdlibEnvironmentResolver;
import p.studio.compiler.source.Span;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.identifiers.CallableId;
import p.studio.compiler.source.identifiers.FileId;
import p.studio.compiler.source.identifiers.ModuleId;
import p.studio.compiler.source.tables.CallableSignatureRef;
import p.studio.utilities.logs.LogAggregator;
import p.studio.utilities.structures.ReadOnlyList;

import java.util.*;

@Slf4j
public class PBSFrontendPhaseService implements FrontendPhaseService {
    private final PbsFrontendCompiler frontendCompiler;
    private final PbsModuleAssemblyService moduleAssemblyService;
    private final PbsImportedSemanticContextService importedSemanticContextService;
    private final PbsProjectLifecycleSemanticsValidator projectLifecycleSemanticsValidator = new PbsProjectLifecycleSemanticsValidator();

    public PBSFrontendPhaseService() {
        this(new ResourceStdlibEnvironmentResolver(), new InterfaceModuleLoader());
    }

    PBSFrontendPhaseService(
            final StdlibEnvironmentResolver stdlibEnvironmentResolver,
            final InterfaceModuleLoader interfaceModuleLoader) {
        final var reservedMetadataExtractor = new PbsReservedMetadataExtractor();
        final var moduleVisibilityValidator = new p.studio.compiler.pbs.linking.PbsModuleVisibilityValidator();
        this.frontendCompiler = new PbsFrontendCompiler();
        this.moduleAssemblyService = new PbsModuleAssemblyService(
                stdlibEnvironmentResolver,
                interfaceModuleLoader,
                moduleVisibilityValidator);
        this.importedSemanticContextService = new PbsImportedSemanticContextService(
                reservedMetadataExtractor,
                frontendCompiler);
    }

    @Override
    public IRBackend compile(
            final FrontendPhaseContext ctx,
            final DiagnosticSink diagnostics,
            final LogAggregator logs,
            final BuildingIssueSink issues) {
        final var nameTable = ctx.nameTable();
        final var feSurfaceContext = ctx.feSurfaceContext();
        final var assembly = moduleAssemblyService.assemble(ctx, nameTable, diagnostics, issues);
        final var parsedSourceFiles = assembly.parsedSourceFiles();
        final var importedSemanticContexts = importedSemanticContextService.build(parsedSourceFiles, assembly.moduleTable());
        projectLifecycleSemanticsValidator.validate(parsedSourceFiles.asList(), diagnostics);
        final var failedModuleIds = assembly.mutableFailedModuleIds();
        final var moduleDependencyGraph = assembly.moduleDependencyGraph();
        final var canonicalModulePool = assembly.canonicalModulePool();

        final var compiledSourceFiles = new ArrayList<CompiledSourceFile>(parsedSourceFiles.size());
        for (final var parsedSource : parsedSourceFiles) {
            final var blockedModuleIds = blockedModulesByDependency(failedModuleIds, moduleDependencyGraph);
            if (blockedModuleIds.contains(parsedSource.moduleId())) {
                continue;
            }
            final var importedSemanticContext = importedSemanticContexts.getOrDefault(
                    parsedSource.fileId(),
                    PbsImportedSemanticContext.empty());
            final var compileErrorBaseline = diagnostics.errorCount();
            final var irBackendFile = frontendCompiler.compileParsedFile(
                    parsedSource.fileId(),
                    parsedSource.ast(),
                    diagnostics,
                    parsedSource.sourceKind(),
                    parsedSource.moduleId(),
                    canonicalModulePool,
                    ctx.hostAdmissionContext(),
                    feSurfaceContext,
                    nameTable,
                    importedSemanticContext.supplementalTopDecls(),
                    importedSemanticContext.importedCallables(),
                    importedSemanticContext.importedGlobals(),
                    importedSemanticContext.importedReservedMetadata(),
                    assembly.globalVisibilityByModule().getOrDefault(parsedSource.moduleId(), Map.of()));
            if (diagnostics.errorCount() > compileErrorBaseline) {
                failedModuleIds.add(parsedSource.moduleId());
            }
            compiledSourceFiles.add(new CompiledSourceFile(parsedSource.moduleId(), irBackendFile));
        }

        final var irBackend = mergeCompiledSources(
                compiledSourceFiles,
                failedModuleIds,
                moduleDependencyGraph);
        logs.using(log).debug("PBS frontend lowered to IR BE:\n%s".formatted(irBackend));
        return irBackend;
    }

    public static Map<FileId, ReadOnlyList<p.studio.compiler.pbs.ast.PbsAst.TopDecl>> importedSupplementalTopDeclsByFile(
            final FrontendPhaseContext ctx,
            final DiagnosticSink diagnostics,
            final BuildingIssueSink issues) {
        return semanticReadSurface(ctx, diagnostics, issues).supplementalTopDeclsByFile();
    }

    public static Map<FileId, ReadOnlyList<PbsInlineHintSurface>> inlineHintsByFile(
            final FrontendPhaseContext ctx,
            final DiagnosticSink diagnostics,
            final BuildingIssueSink issues) {
        return semanticReadSurface(ctx, diagnostics, issues).inlineHintsByFile();
    }

    public static PbsSemanticReadSurface semanticReadSurface(
            final FrontendPhaseContext ctx,
            final DiagnosticSink diagnostics,
            final BuildingIssueSink issues) {
        final var service = new PBSFrontendPhaseService();
        final var assembly = service.moduleAssemblyService.assemble(ctx, ctx.nameTable(), diagnostics, issues);
        final var importedSemanticContexts = service.importedSemanticContextService.build(
                assembly.parsedSourceFiles(),
                assembly.moduleTable());
        final var flowSemanticsValidator = new PbsFlowSemanticsValidator();
        final Map<FileId, ReadOnlyList<p.studio.compiler.pbs.ast.PbsAst.TopDecl>> supplementalTopDeclsByFile = new LinkedHashMap<>();
        final Map<FileId, ReadOnlyList<PbsInlineHintSurface>> inlineHintsByFile = new LinkedHashMap<>();
        for (final var entry : importedSemanticContexts.entrySet()) {
            supplementalTopDeclsByFile.put(entry.getKey(), entry.getValue().supplementalTopDecls());
        }
        for (final var parsedSourceFile : assembly.parsedSourceFiles()) {
            final var importedSemanticContext = importedSemanticContexts.getOrDefault(
                    parsedSourceFile.fileId(),
                    PbsImportedSemanticContext.empty());
            final var inlineHints = flowSemanticsValidator.collectInlineHints(
                    parsedSourceFile.ast(),
                    importedSemanticContext.supplementalTopDecls(),
                    ctx.feSurfaceContext(),
                    diagnostics);
            if (!inlineHints.isEmpty()) {
                inlineHintsByFile.put(parsedSourceFile.fileId(), inlineHints);
            }
        }
        return new PbsSemanticReadSurface(
                Map.copyOf(supplementalTopDeclsByFile),
                Map.copyOf(inlineHintsByFile));
    }

    public record PbsSemanticReadSurface(
            Map<FileId, ReadOnlyList<p.studio.compiler.pbs.ast.PbsAst.TopDecl>> supplementalTopDeclsByFile,
            Map<FileId, ReadOnlyList<PbsInlineHintSurface>> inlineHintsByFile) {
    }

    private IRBackend mergeCompiledSources(
            final ArrayList<CompiledSourceFile> compiledSourceFiles,
            final Set<ModuleId> failedModuleIds,
            final Map<ModuleId, Set<ModuleId>> moduleDependencyGraph) {
        final var irBackendAggregator = IRBackend.aggregator();
        final var blockedModuleIds = blockedModulesByDependency(failedModuleIds, moduleDependencyGraph);

        for (final var compiledSource : compiledSourceFiles) {
            if (blockedModuleIds.contains(compiledSource.moduleId())) {
                continue;
            }
            irBackendAggregator.merge(compiledSource.irBackendFile());
        }
        return synthesizePublishedLifecycle(irBackendAggregator.emit());
    }

    private IRBackend synthesizePublishedLifecycle(final IRBackend baseBackend) {
        final var callableSignatures = new ArrayList<>(baseBackend.getCallableSignatures().asList());
        final var executableFunctions = new ArrayList<>(baseBackend.getExecutableFunctions().asList());
        final var globals = new ArrayList<>(baseBackend.getGlobals().asList());
        final var fileInitFunctionsByModule = new LinkedHashMap<ModuleId, List<IRBackendExecutableFunction>>();
        final var projectInitAnchorsByModule = new LinkedHashMap<ModuleId, String>();
        final var wrapperNamesByModule = new LinkedHashMap<ModuleId, String>();
        final var frameAnchorsByModule = new LinkedHashMap<ModuleId, String>();

        for (final var executable : executableFunctions) {
            if (!executable.callableName().startsWith("__pbs.file_init$")) {
                continue;
            }
            fileInitFunctionsByModule
                    .computeIfAbsent(normalizeModuleId(executable.moduleId()), ignored -> new ArrayList<>())
                    .add(executable);
        }
        for (final var syntheticFunction : baseBackend.getSyntheticFunctions()) {
            final var moduleId = normalizeModuleId(syntheticFunction.moduleId());
            switch (syntheticFunction.kind()) {
                case PROJECT_INIT -> projectInitAnchorsByModule.put(moduleId, syntheticFunction.origin().anchorCallableName());
                case PUBLISHED_FRAME_WRAPPER -> {
                    wrapperNamesByModule.put(moduleId, syntheticFunction.callableName());
                    frameAnchorsByModule.put(moduleId, syntheticFunction.origin().anchorCallableName());
                }
                default -> {
                }
            }
        }

        final ModuleId entryPointModuleId;
        final String entryPointCallableName;
        if (wrapperNamesByModule.size() == 1) {
            entryPointModuleId = wrapperNamesByModule.keySet().iterator().next();
            entryPointCallableName = wrapperNamesByModule.get(entryPointModuleId);
        } else {
            entryPointModuleId = baseBackend.getEntryPointModuleId();
            entryPointCallableName = baseBackend.getEntryPointCallableName();
        }

        final var sortedModuleIds = new ArrayList<ModuleId>();
        sortedModuleIds.addAll(fileInitFunctionsByModule.keySet());
        for (final var moduleId : wrapperNamesByModule.keySet()) {
            if (!sortedModuleIds.contains(moduleId)) {
                sortedModuleIds.add(moduleId);
            }
        }
        sortedModuleIds.sort(Comparator.comparing(moduleId -> moduleSortKey(baseBackend, moduleId)));

        final var moduleInitCallableIds = new LinkedHashMap<ModuleId, CallableId>();
        for (final var moduleId : sortedModuleIds) {
            final var moduleInitName = PbsFrontendCompiler.moduleInitCallableName(moduleId);
            final var fileInitFunctions = new ArrayList<>(fileInitFunctionsByModule.getOrDefault(moduleId, List.of()));
            if (fileInitFunctions.isEmpty()) {
                continue;
            }
            fileInitFunctions.sort(Comparator.comparingInt(fn -> fn.fileId().getId()));

            final var callableId = new CallableId(callableSignatures.size());
            callableSignatures.add(new CallableSignatureRef(moduleId, moduleInitName, 0, "() -> unit"));
            moduleInitCallableIds.put(moduleId, callableId);

            final var instructions = new ArrayList<IRBackendExecutableFunction.Instruction>();
            var anchorSpan = fileInitFunctions.getFirst().span();
            var anchorFileId = fileInitFunctions.getFirst().fileId();
            for (final var fileInitFunction : fileInitFunctions) {
                instructions.add(callInstruction(fileInitFunction, fileInitFunction.span()));
                anchorSpan = fileInitFunction.span();
                anchorFileId = fileInitFunction.fileId();
            }
            instructions.add(retInstruction(anchorSpan));
            executableFunctions.add(new IRBackendExecutableFunction(
                    anchorFileId,
                    moduleId,
                    moduleInitName,
                    callableId,
                    safeToInt(anchorSpan.getStart()),
                    safeToInt(anchorSpan.getEnd()),
                    0,
                    0,
                    0,
                    1,
                    ReadOnlyList.wrap(instructions),
                    anchorSpan));
        }

        CallableId projectInitCallableId = null;
        if (!entryPointModuleId.isNone()) {
            final var projectInitAnchor = projectInitAnchorsByModule.get(entryPointModuleId);
            final var projectInitTarget = resolveExecutable(executableFunctions, entryPointModuleId, projectInitAnchor);
            if (projectInitTarget != null) {
                final var projectInitName = PbsFrontendCompiler.projectInitCallableName(entryPointModuleId);
                projectInitCallableId = new CallableId(callableSignatures.size());
                callableSignatures.add(new CallableSignatureRef(entryPointModuleId, projectInitName, 0, "() -> unit"));
                executableFunctions.add(new IRBackendExecutableFunction(
                        projectInitTarget.fileId(),
                        entryPointModuleId,
                        projectInitName,
                        projectInitCallableId,
                        projectInitTarget.sourceStart(),
                        projectInitTarget.sourceEnd(),
                        0,
                        0,
                        0,
                        1,
                        ReadOnlyList.from(
                                callInstruction(projectInitTarget, projectInitTarget.span()),
                                retInstruction(projectInitTarget.span())),
                        projectInitTarget.span()));
            }
        }

        final var frameExecutable = resolveExecutable(
                executableFunctions,
                entryPointModuleId,
                frameAnchorsByModule.get(entryPointModuleId));
        if (frameExecutable != null
                && entryPointCallableName != null
                && !entryPointCallableName.isBlank()
                && !entryPointModuleId.isNone()) {
            final var existingBootGuard = globals.stream()
                    .filter(global -> moduleIdsMatch(entryPointModuleId, global.moduleId()))
                    .filter(global -> global.hiddenKind() == IRHiddenGlobalKind.BOOT_GUARD)
                    .findFirst()
                    .orElse(null);
            final var bootGuardSlot = existingBootGuard != null
                    ? existingBootGuard.slot()
                    : nextGlobalSlot(globals, entryPointModuleId);
            final var wrapperCallableId = new CallableId(callableSignatures.size());
            callableSignatures.add(new CallableSignatureRef(entryPointModuleId, entryPointCallableName, 0, "() -> unit"));
            final var instructions = new ArrayList<IRBackendExecutableFunction.Instruction>();
            instructions.add(getGlobalInstruction(bootGuardSlot, frameExecutable.span()));
            instructions.add(pushBoolInstruction(true, frameExecutable.span()));
            instructions.add(eqInstruction(frameExecutable.span()));
            instructions.add(jumpIfTrueInstruction("bootstrap_done", frameExecutable.span()));
            for (final var moduleId : sortedModuleIds) {
                final var moduleInitCallableId = moduleInitCallableIds.get(moduleId);
                if (moduleInitCallableId == null) {
                    continue;
                }
                instructions.add(new IRBackendExecutableFunction.Instruction(
                        IRBackendExecutableFunction.InstructionKind.CALL_FUNC,
                        moduleId,
                        PbsFrontendCompiler.moduleInitCallableName(moduleId),
                        moduleInitCallableId,
                        null,
                        null,
                        0,
                        0,
                        frameExecutable.span()));
            }
            if (projectInitCallableId != null) {
                instructions.add(new IRBackendExecutableFunction.Instruction(
                        IRBackendExecutableFunction.InstructionKind.CALL_FUNC,
                        entryPointModuleId,
                        PbsFrontendCompiler.projectInitCallableName(entryPointModuleId),
                        projectInitCallableId,
                        null,
                        null,
                        0,
                        0,
                        frameExecutable.span()));
            }
            instructions.add(pushBoolInstruction(true, frameExecutable.span()));
            instructions.add(setGlobalInstruction(bootGuardSlot, frameExecutable.span()));
            instructions.add(labelInstruction("bootstrap_done", frameExecutable.span()));
            instructions.add(callInstruction(frameExecutable, frameExecutable.span()));
            instructions.add(retInstruction(frameExecutable.span()));
            executableFunctions.add(new IRBackendExecutableFunction(
                    frameExecutable.fileId(),
                    entryPointModuleId,
                    entryPointCallableName,
                    wrapperCallableId,
                    frameExecutable.sourceStart(),
                    frameExecutable.sourceEnd(),
                    0,
                    0,
                    0,
                    2,
                    ReadOnlyList.wrap(instructions),
                    frameExecutable.span()));

            if (existingBootGuard == null) {
                globals.add(new IRGlobal(
                        frameExecutable.fileId(),
                        entryPointModuleId,
                        PbsFrontendCompiler.bootGuardGlobalName(entryPointModuleId),
                        "bool",
                        bootGuardSlot,
                        IRGlobalVisibility.MODULE,
                        true,
                        IRHiddenGlobalKind.BOOT_GUARD,
                        new IRGlobalOrigin(
                                frameExecutable.fileId(),
                                entryPointModuleId,
                                frameAnchorsByModule.getOrDefault(entryPointModuleId, frameExecutable.callableName()),
                                frameExecutable.span(),
                                "BOOT_GUARD"),
                        frameExecutable.span()));
            }
        }

        final var normalizedGlobals = normalizeGlobalSlots(globals);

        return IRBackend.builder()
                .entryPointCallableName(entryPointCallableName)
                .entryPointModuleId(entryPointModuleId)
                .functions(baseBackend.getFunctions())
                .syntheticFunctions(baseBackend.getSyntheticFunctions())
                .globals(ReadOnlyList.wrap(normalizedGlobals))
                .executableFunctions(ReadOnlyList.wrap(executableFunctions))
                .modulePool(baseBackend.getModulePool())
                .callableSignatures(ReadOnlyList.wrap(callableSignatures))
                .intrinsicPool(baseBackend.getIntrinsicPool())
                .reservedMetadata(baseBackend.getReservedMetadata())
                .build();
    }

    private int nextGlobalSlot(
            final ArrayList<IRGlobal> globals,
            final ModuleId moduleId) {
        var nextSlot = 0;
        for (final var global : globals) {
            if (!moduleIdsMatch(moduleId, global.moduleId())) {
                continue;
            }
            nextSlot = Math.max(nextSlot, global.slot() + 1);
        }
        return nextSlot;
    }

    private ArrayList<IRGlobal> normalizeGlobalSlots(final ArrayList<IRGlobal> globals) {
        final var nextSlotByModule = new LinkedHashMap<ModuleId, Integer>();
        final var normalized = new ArrayList<IRGlobal>(globals.size());
        for (final var global : globals) {
            final var moduleId = normalizeModuleId(global.moduleId());
            final var slot = nextSlotByModule.getOrDefault(moduleId, 0);
            nextSlotByModule.put(moduleId, slot + 1);
            normalized.add(new IRGlobal(
                    global.fileId(),
                    moduleId,
                    global.name(),
                    global.declaredTypeSurface(),
                    slot,
                    global.visibility(),
                    global.isHidden(),
                    global.hiddenKind(),
                    global.origin(),
                    global.span()));
        }
        return normalized;
    }

    private IRBackendExecutableFunction resolveExecutable(
            final ArrayList<IRBackendExecutableFunction> executableFunctions,
            final ModuleId moduleId,
            final String callableName) {
        if (callableName == null || callableName.isBlank()) {
            return null;
        }
        for (final var executable : executableFunctions) {
            if (!callableName.equals(executable.callableName())) {
                continue;
            }
            if (!moduleIdsMatch(moduleId, executable.moduleId())) {
                continue;
            }
            return executable;
        }
        return null;
    }

    private IRBackendExecutableFunction.Instruction callInstruction(
            final IRBackendExecutableFunction callee,
            final Span span) {
        return new IRBackendExecutableFunction.Instruction(
                IRBackendExecutableFunction.InstructionKind.CALL_FUNC,
                callee.moduleId(),
                callee.callableName(),
                callee.callableId(),
                null,
                null,
                callee.paramSlots(),
                callee.returnSlots(),
                span);
    }

    private IRBackendExecutableFunction.Instruction retInstruction(final Span span) {
        return new IRBackendExecutableFunction.Instruction(
                IRBackendExecutableFunction.InstructionKind.RET,
                "",
                null,
                null,
                span);
    }

    private IRBackendExecutableFunction.Instruction getGlobalInstruction(
            final int slot,
            final Span span) {
        return new IRBackendExecutableFunction.Instruction(
                IRBackendExecutableFunction.InstructionKind.GET_GLOBAL,
                "",
                null,
                null,
                null,
                slot,
                null,
                span);
    }

    private IRBackendExecutableFunction.Instruction setGlobalInstruction(
            final int slot,
            final Span span) {
        return new IRBackendExecutableFunction.Instruction(
                IRBackendExecutableFunction.InstructionKind.SET_GLOBAL,
                "",
                null,
                null,
                null,
                slot,
                null,
                span);
    }

    private IRBackendExecutableFunction.Instruction pushBoolInstruction(
            final boolean value,
            final Span span) {
        return new IRBackendExecutableFunction.Instruction(
                IRBackendExecutableFunction.InstructionKind.PUSH_BOOL,
                "",
                null,
                null,
                null,
                value ? "true" : "false",
                null,
                null,
                null,
                span);
    }

    private IRBackendExecutableFunction.Instruction eqInstruction(final Span span) {
        return new IRBackendExecutableFunction.Instruction(
                IRBackendExecutableFunction.InstructionKind.EQ,
                "",
                null,
                null,
                null,
                span);
    }

    private IRBackendExecutableFunction.Instruction labelInstruction(
            final String label,
            final Span span) {
        return new IRBackendExecutableFunction.Instruction(
                IRBackendExecutableFunction.InstructionKind.LABEL,
                "",
                null,
                null,
                null,
                label,
                null,
                null,
                null,
                span);
    }

    private IRBackendExecutableFunction.Instruction jumpIfTrueInstruction(
            final String targetLabel,
            final Span span) {
        return new IRBackendExecutableFunction.Instruction(
                IRBackendExecutableFunction.InstructionKind.JMP_IF_TRUE,
                "",
                null,
                null,
                null,
                null,
                targetLabel,
                null,
                null,
                span);
    }

    private ModuleId normalizeModuleId(final ModuleId moduleId) {
        return moduleId == null ? ModuleId.none() : moduleId;
    }

    private boolean moduleIdsMatch(
            final ModuleId left,
            final ModuleId right) {
        final var normalizedLeft = normalizeModuleId(left);
        final var normalizedRight = normalizeModuleId(right);
        if (normalizedLeft.isNone() && normalizedRight.isNone()) {
            return true;
        }
        return !normalizedLeft.isNone()
                && !normalizedRight.isNone()
                && normalizedLeft.getIndex() == normalizedRight.getIndex();
    }

    private String moduleSortKey(
            final IRBackend backend,
            final ModuleId moduleId) {
        if (moduleId != null && !moduleId.isNone()) {
            final var index = moduleId.getIndex();
            if (index >= 0 && index < backend.getModulePool().size()) {
                final var moduleReference = backend.getModulePool().get(index);
                final var joiner = new StringJoiner("/", moduleReference.project() + ":", "");
                for (final var segment : moduleReference.pathSegments()) {
                    joiner.add(segment);
                }
                return joiner.toString();
            }
        }
        return "";
    }

    private int safeToInt(final long value) {
        if (value > Integer.MAX_VALUE) {
            return Integer.MAX_VALUE;
        }
        if (value < Integer.MIN_VALUE) {
            return Integer.MIN_VALUE;
        }
        return (int) value;
    }

    private Set<ModuleId> blockedModulesByDependency(
            final Set<ModuleId> failedModuleIds,
            final Map<ModuleId, Set<ModuleId>> dependenciesByModule) {
        final Map<ModuleId, Set<ModuleId>> dependentsByModule = new HashMap<>();
        for (final var entry : dependenciesByModule.entrySet()) {
            final var importer = entry.getKey();
            for (final var dependency : entry.getValue()) {
                dependentsByModule.computeIfAbsent(dependency, ignored -> new HashSet<>()).add(importer);
            }
        }

        final var blocked = new HashSet<ModuleId>(failedModuleIds);
        final var pending = new ArrayDeque<ModuleId>(failedModuleIds);
        while (!pending.isEmpty()) {
            final var failedOrBlocked = pending.removeFirst();
            final var dependents = dependentsByModule.getOrDefault(failedOrBlocked, Set.of());
            for (final var dependent : dependents) {
                if (!blocked.add(dependent)) {
                    continue;
                }
                pending.addLast(dependent);
            }
        }
        return blocked;
    }

    private record CompiledSourceFile(
            ModuleId moduleId,
            p.studio.compiler.models.IRBackendFile irBackendFile) {
    }
}
