package p.studio.compiler.workspaces.phases;

import p.studio.compiler.FrontendRegistryService;
import p.studio.compiler.messages.BuildingIssueSink;
import p.studio.compiler.models.DependencyContext;
import p.studio.compiler.models.ProjectInfo;
import p.studio.compiler.models.ProjectInfoId;
import p.studio.compiler.utilities.PrometeuManifestUtils;
import p.studio.compiler.workspaces.DependencyPhase;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;


public class DiscoverPhase implements DependencyPhase {
    @Override
    public BuildingIssueSink run(final DependencyContext ctx) {
        final var projectsSeen = new HashSet<Path>();
        final var issues = BuildingIssueSink.empty();
        // Discovers projects; registers them; adds dependencies to queue
        while (!ctx.pending.isEmpty()) {
            final var rootPathCanon = ctx.pending.pollFirst();
            if (projectsSeen.contains(rootPathCanon)) {
                continue;
            }
            discoverOne(ctx, rootPathCanon, issues).ifPresent(projectInfoId -> {
                projectsSeen.add(rootPathCanon);
                final var projectInfo = ctx.projectInfoTable.get(projectInfoId);
                projectInfo.manifest.dependencies().forEach(dep -> ctx.pending.add(dep.canonPath()));
                ctx.projectNameAndVersions
                        .computeIfAbsent(projectInfo.manifest.name(), ignored -> new HashSet<>())
                        .add(projectInfo.manifest.version());
            });
        }
        return issues;
    }

    private Optional<ProjectInfoId> discoverOne(
            final DependencyContext ctx,
            final Path rootPathCanon,
            final BuildingIssueSink issues) {
        final var manifestPathCanon = canonicalizeManifestPath(rootPathCanon, issues);
        if (manifestPathCanon.isEmpty()) {
            return Optional.empty();
        }
        final var manifest = PrometeuManifestUtils.buildManifest(ctx.mainProjectRootPathCanon, manifestPathCanon.get(), issues);
        if (manifest.isEmpty()) {
            return Optional.empty();
        }
        if (rootPathCanon.equals(ctx.mainProjectRootPathCanon)) {
            ctx.rootStdlib = manifest.get().stdlib();
        }
        final var frontendSpec = FrontendRegistryService.getFrontendSpec(manifest.get().language());
        // Returns empty when language is unknown
        if (frontendSpec.isEmpty()) {
            issues.report(builder -> builder
                    .error(true)
                    .message("[DEPS]: unknown language " + manifest.get().language() + " for project " + manifest.get().name()));
            return Optional.empty();
        }
        // Enforces consistent language across projects
        if (Objects.isNull(ctx.frontendSpec)) {
            ctx.frontendSpec = frontendSpec.get();
        } else if (!ctx.frontendSpec.getLanguageId().equals(frontendSpec.get().getLanguageId())) {
            // Reports language inconsistency across projects
            issues.report(builder -> builder
                    .error(true)
                    .message(String.format("[DEPS]: inconsistent language: [ %s ] has \"%s\" but should be \"%s\"",
                            manifest.get().name(), frontendSpec.get().getLanguageId(), ctx.frontendSpec.getLanguageId())));
            return Optional.empty();
        }
        final var projectInfo = ProjectInfo
                .builder()
                .rootDirectory(rootPathCanon)
                .manifestPath(manifestPathCanon.get())
                .manifest(manifest.get())
                .build();
        final var projectInfoId = ctx.projectInfoTable.register(projectInfo);
        return Optional.of(projectInfoId);
    }

    private static Optional<Path> canonicalizeManifestPath(
            final Path rootPathCanon,
            final BuildingIssueSink issues) {
        final var manifestPath = rootPathCanon.resolve("prometeu.json");
        final Path manifestPathCanon;
        // Resolves manifest path; reports errors on failure
        try {
            manifestPathCanon = manifestPath.toRealPath();
        } catch (IOException e) {
            issues.report(builder -> builder
                    .error(true)
                    .message("[DEPS]: manifest canonPath does not exist: " + manifestPath)
                    .exception(e));
            return Optional.empty();
        }
        if (!Files.isRegularFile(manifestPathCanon)) {
            issues.report(builder -> builder
                    .error(true)
                    .message("[DEPS]: manifest not found: expected a file " + manifestPathCanon));
            return Optional.empty();
        }
        return Optional.of(manifestPathCanon);
    }
}
