package p.studio.shipper;

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 p.packer.PackerWorkspaceService;
import p.packer.dtos.PackerDiagnosticDTO;
import p.packer.dtos.PackerPackValidationAssetDTO;
import p.packer.messages.*;
import p.studio.Container;
import p.studio.compiler.messages.BuilderPipelineConfig;
import p.studio.compiler.models.BuildResult;
import p.studio.compiler.models.BuilderPipelineContext;
import p.studio.compiler.workspaces.BuilderPipelineService;
import p.studio.projects.ProjectReference;
import p.studio.utilities.logs.LogAggregator;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;

public final class StudioShipperService {
    private static final String BUILD_DIR = "build";
    private static final String MANIFEST_FILE = "manifest.json";
    private static final String PROJECT_MANIFEST_FILE = "prometeu.json";
    private static final String ASSET_TABLE_FILE = "asset_table.json";
    private static final String PRELOAD_FILE = "preload.json";
    private static final ObjectMapper MAPPER = new ObjectMapper();
    private final PackerWorkspaceService packerWorkspaceService;

    public StudioShipperService() {
        this(Container.packer().workspaceService());
    }

    public StudioShipperService(final PackerWorkspaceService packerWorkspaceService) {
        this.packerWorkspaceService = Objects.requireNonNull(packerWorkspaceService, "packerWorkspaceService");
    }

    public StudioShipperPrepareResult prepare(final ProjectReference projectReference) {
        return prepare(projectReference, ignored -> { });
    }

    public StudioShipperPrepareResult prepare(
            final ProjectReference projectReference,
            final Consumer<StudioShipperLogEntry> logSink) {
        final ProjectReference safeProjectReference = Objects.requireNonNull(projectReference, "projectReference");
        final Consumer<StudioShipperLogEntry> safeLogSink = Objects.requireNonNull(logSink, "logSink");
        final ArrayList<StudioShipperLogEntry> logs = new ArrayList<>();

        try {
            final BuildResult buildResult = build(safeProjectReference, logs, safeLogSink);
            final ValidatePackWorkspaceResult validationResult = validatePack(safeProjectReference, logs, safeLogSink);
            if (!validationResult.canPack()) {
                emit(logs, safeLogSink, StudioShipperLogEntry.error(
                        StudioShipperLogSource.PACK_VALIDATION,
                        validationResult.summary()));
                logBlockedAssets(validationResult.assets(), logs, safeLogSink);
                return new StudioShipperPrepareResult(
                        StudioShipperPreparationStatus.FAILED,
                        logs,
                        buildResult,
                        validationResult,
                        null,
                        null);
            }

            final PackWorkspaceResult packResult = pack(safeProjectReference, logs, safeLogSink);
            if (packResult.status() != PackerOperationStatus.SUCCESS) {
                emit(logs, safeLogSink, logFor(packResult.status(), StudioShipperLogSource.PACK, packResult.summary()));
                return new StudioShipperPrepareResult(
                        StudioShipperPreparationStatus.FAILED,
                        logs,
                        buildResult,
                        validationResult,
                        packResult,
                        null);
            }

            final Path manifestPath = writeManifest(safeProjectReference, buildResult, logs, safeLogSink);
            return new StudioShipperPrepareResult(
                    StudioShipperPreparationStatus.SUCCESS,
                    logs,
                    buildResult,
                    validationResult,
                    packResult,
                    manifestPath);
        } catch (RuntimeException runtimeException) {
            emit(logs, safeLogSink, StudioShipperLogEntry.error(
                    StudioShipperLogSource.BUILD,
                    runtimeException.getMessage() == null ? runtimeException.getClass().getSimpleName() : runtimeException.getMessage()));
            return new StudioShipperPrepareResult(
                    StudioShipperPreparationStatus.FAILED,
                    logs,
                    null,
                    null,
                    null,
                    null);
        }
    }

    private BuildResult build(
            final ProjectReference projectReference,
            final List<StudioShipperLogEntry> logs,
            final Consumer<StudioShipperLogEntry> logSink) {
        final var config = new BuilderPipelineConfig(false, projectReference.rootPath().toString());
        final var ctx = BuilderPipelineContext.fromConfig(config);
        final var logAggregator = LogAggregator.with(message -> emit(logs, logSink, StudioShipperLogEntry.info(
                StudioShipperLogSource.BUILD,
                normalizeMessage(message))));
        final BuildResult result = BuilderPipelineService.INSTANCE.build(ctx, logAggregator);
        if (result.bytecodeArtifactPath() != null) {
            emit(logs, logSink, StudioShipperLogEntry.info(
                    StudioShipperLogSource.BUILD,
                    "Prepared program bytecode at " + result.bytecodeArtifactPath().toAbsolutePath().normalize()));
        }
        return result;
    }

    private ValidatePackWorkspaceResult validatePack(
            final ProjectReference projectReference,
            final List<StudioShipperLogEntry> logs,
            final Consumer<StudioShipperLogEntry> logSink) {
        final ValidatePackWorkspaceResult result = packerWorkspaceService
                .validatePackWorkspace(new ValidatePackWorkspaceRequest(projectReference.toPackerProjectContext()));
        // keep the validation source explicit even when the downstream packer reports partial status
        emit(logs, logSink, logFor(result.status(), StudioShipperLogSource.PACK_VALIDATION, result.summary()));
        if (!result.canPack()) {
            logBlockedAssets(result.assets(), logs, logSink);
        }
        return result;
    }

    private PackWorkspaceResult pack(
            final ProjectReference projectReference,
            final List<StudioShipperLogEntry> logs,
            final Consumer<StudioShipperLogEntry> logSink) {
        final PackWorkspaceResult result = packerWorkspaceService
                .packWorkspace(new PackWorkspaceRequest(projectReference.toPackerProjectContext()));
        emit(logs, logSink, logFor(result.status(), StudioShipperLogSource.PACK, result.summary()));
        result.result().emittedArtifacts().forEach(artifact -> emit(logs, logSink, StudioShipperLogEntry.info(
                StudioShipperLogSource.PACK,
                "Emitted " + artifact.label() + " at " + artifact.path())));
        return result;
    }

    private Path writeManifest(
            final ProjectReference projectReference,
            final BuildResult buildResult,
            final List<StudioShipperLogEntry> logs,
            final Consumer<StudioShipperLogEntry> logSink) {
        final Path buildRoot = projectReference.rootPath().resolve(BUILD_DIR).toAbsolutePath().normalize();
        final Path manifestPath = buildRoot.resolve(MANIFEST_FILE);
        try {
            Files.createDirectories(buildRoot);
            final ObjectNode manifest = createManifest(projectReference, buildResult);
            MAPPER.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
            emit(logs, logSink, StudioShipperLogEntry.info(
                    StudioShipperLogSource.MANIFEST,
                    "Generated manifest at " + manifestPath));
            return manifestPath;
        } catch (IOException ioException) {
            emit(logs, logSink, StudioShipperLogEntry.error(
                    StudioShipperLogSource.MANIFEST,
                    "Failed to write manifest: " + manifestPath));
            throw new RuntimeException("failed to write build manifest: " + manifestPath, ioException);
        }
    }

    private ObjectNode createManifest(
            final ProjectReference projectReference,
            final BuildResult buildResult) throws IOException {
        final ObjectNode manifest = MAPPER.createObjectNode();
        manifest.put("magic", "PMTU");
        manifest.put("cartridge_version", 1);
        manifest.put("app_id", stableAppId(projectReference));
        manifest.put("title", projectReference.name());
        manifest.put("app_version", projectReference.version());
        manifest.put("app_mode", "Game");
        manifest.set("capabilities", capabilitiesNode(buildResult));
        manifest.set("asset_table", readArrayOrEmpty(projectReference.rootPath().resolve(BUILD_DIR).resolve(ASSET_TABLE_FILE)));
        manifest.set("preload", readArrayOrEmpty(projectReference.rootPath().resolve(BUILD_DIR).resolve(PRELOAD_FILE)));
        return manifest;
    }

    private ArrayNode capabilitiesNode(final BuildResult buildResult) {
        final ArrayNode capabilities = MAPPER.createArrayNode();
        final var irBackend = buildResult.compileResult().analysisSnapshot().irBackend();
        final LinkedHashSet<String> distinct = new LinkedHashSet<>(irBackend.getReservedMetadata().requiredCapabilities().asList());
        distinct.forEach(capabilities::add);
        return capabilities;
    }

    private JsonNode readArrayOrEmpty(final Path path) throws IOException {
        if (!Files.isRegularFile(path)) {
            return MAPPER.createArrayNode();
        }
        final JsonNode loaded = MAPPER.readTree(path.toFile());
        return loaded != null && loaded.isArray() ? loaded : MAPPER.createArrayNode();
    }

    private int stableAppId(final ProjectReference projectReference) {
        final int hash = Objects.hash(
                projectReference.name(),
                projectReference.version(),
                projectReference.languageId(),
                projectReference.stdlibVersion());
        return hash == Integer.MIN_VALUE ? 1 : Math.max(1, Math.abs(hash));
    }

    private void putIfTextual(
            final ObjectNode node,
            final String fieldName,
            final JsonNode value) {
        if (value != null && value.isTextual() && !value.asText().isBlank()) {
            node.put(fieldName, value.asText());
        }
    }

    private void logBlockedAssets(
            final List<PackerPackValidationAssetDTO> assets,
            final List<StudioShipperLogEntry> logs,
            final Consumer<StudioShipperLogEntry> logSink) {
        for (final PackerPackValidationAssetDTO asset : assets) {
            emit(logs, logSink, StudioShipperLogEntry.error(
                    StudioShipperLogSource.PACK_VALIDATION,
                    "Blocked asset #" + asset.assetId() + " (" + asset.assetName() + ")"));
            for (final PackerDiagnosticDTO diagnostic : asset.diagnostics()) {
                emit(logs, logSink, StudioShipperLogEntry.error(
                        StudioShipperLogSource.PACK_VALIDATION,
                        diagnostic.message()));
            }
        }
    }

    private StudioShipperLogEntry logFor(
            final PackerOperationStatus status,
            final StudioShipperLogSource source,
            final String message) {
        return status == PackerOperationStatus.FAILED || status == PackerOperationStatus.PARTIAL
                ? StudioShipperLogEntry.error(source, message)
                : StudioShipperLogEntry.info(source, message);
    }

    private void emit(
            final List<StudioShipperLogEntry> logs,
            final Consumer<StudioShipperLogEntry> logSink,
            final StudioShipperLogEntry entry) {
        logs.add(entry);
        logSink.accept(entry);
    }

    private String normalizeMessage(final String message) {
        if (message == null) {
            return "(empty build log entry)";
        }
        final String trimmed = message.strip();
        return trimmed.isEmpty() ? "(empty build log entry)" : trimmed;
    }
}
