package p.packer.services;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import p.packer.messages.assets.AssetFamilyCatalog;
import p.packer.messages.assets.OutputCodecCatalog;
import p.packer.messages.diagnostics.PackerDiagnosticCategory;
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
import p.packer.models.PackerAssetDeclaration;
import p.packer.models.PackerAssetDeclarationParseResult;
import p.packer.models.PackerDiagnostic;

import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

public final class PackerAssetDeclarationParser {
    private static final ObjectMapper MAPPER = new ObjectMapper();
    private static final int SUPPORTED_SCHEMA_VERSION = 1;

    public PackerAssetDeclarationParseResult parse(Path assetManifestPath) {
        final Path manifestPath = Objects.requireNonNull(assetManifestPath, "assetManifestPath").toAbsolutePath().normalize();
        final List<PackerDiagnostic> diagnostics = new ArrayList<>();
        final JsonNode root;
        try {
            root = MAPPER.readTree(manifestPath.toFile());
        } catch (IOException exception) {
            diagnostics.add(new PackerDiagnostic(
                    PackerDiagnosticSeverity.ERROR,
                    PackerDiagnosticCategory.STRUCTURAL,
                    "Unable to parse asset.json: " + exception.getMessage(),
                    manifestPath,
                    true));
            return new PackerAssetDeclarationParseResult(null, diagnostics);
        }

        final Integer schemaVersion = requiredInt(root, "schema_version", diagnostics, manifestPath);
        final String assetUuid = requiredText(root, "asset_uuid", diagnostics, manifestPath);
        final String name = requiredText(root, "name", diagnostics, manifestPath);
        final AssetFamilyCatalog assetFamily = requiredAssetFamily(root, diagnostics, manifestPath);
        final Map<String, List<String>> inputsByRole = requiredInputs(root.path("inputs"), diagnostics, manifestPath);
        final String outputFormat = requiredText(root.path("output"), "format", diagnostics, manifestPath);
        final OutputCodecCatalog outputCodec = requiredOutputCodec(root.path("output"), diagnostics, manifestPath);
        final Boolean preloadEnabled = requiredBoolean(root.path("preload"), "enabled", diagnostics, manifestPath);

        if (schemaVersion != null && schemaVersion != SUPPORTED_SCHEMA_VERSION) {
            diagnostics.add(new PackerDiagnostic(
                    PackerDiagnosticSeverity.ERROR,
                    PackerDiagnosticCategory.VERSIONING,
                    "Unsupported asset.json schema_version: " + schemaVersion,
                    manifestPath,
                    true));
        }

        if (diagnostics.stream().anyMatch(PackerDiagnostic::blocking)) {
            return new PackerAssetDeclarationParseResult(null, diagnostics);
        }

        return new PackerAssetDeclarationParseResult(
                new PackerAssetDeclaration(
                        schemaVersion,
                        assetUuid,
                        name,
                        assetFamily,
                        inputsByRole,
                        outputFormat,
                        outputCodec,
                        preloadEnabled),
                diagnostics);
    }

    private Integer requiredInt(JsonNode node, String fieldName, List<PackerDiagnostic> diagnostics, Path manifestPath) {
        final JsonNode field = node.path(fieldName);
        if (!field.isInt()) {
            diagnostics.add(missingOrInvalid(fieldName, "integer", manifestPath));
            return null;
        }
        return field.intValue();
    }

    private String requiredText(JsonNode node, String fieldName, List<PackerDiagnostic> diagnostics, Path manifestPath) {
        final JsonNode field = node.path(fieldName);
        if (!field.isTextual() || field.asText().isBlank()) {
            diagnostics.add(missingOrInvalid(fieldName, "non-blank string", manifestPath));
            return null;
        }
        return field.asText().trim();
    }

    private AssetFamilyCatalog requiredAssetFamily(JsonNode node, List<PackerDiagnostic> diagnostics, Path manifestPath) {
        final String manifestType = requiredText(node, "type", diagnostics, manifestPath);
        if (manifestType == null) {
            return null;
        }
        final AssetFamilyCatalog assetFamily = AssetFamilyCatalog.fromManifestType(manifestType);
        if (assetFamily == AssetFamilyCatalog.UNKNOWN) {
            diagnostics.add(new PackerDiagnostic(
                    PackerDiagnosticSeverity.ERROR,
                    PackerDiagnosticCategory.STRUCTURAL,
                    "Field 'type' must be one of: image_bank, palette_bank, sound_bank.",
                    manifestPath,
                    true));
            return null;
        }
        return assetFamily;
    }

    private Boolean requiredBoolean(JsonNode node, String fieldName, List<PackerDiagnostic> diagnostics, Path manifestPath) {
        final JsonNode field = node.path(fieldName);
        if (!field.isBoolean()) {
            diagnostics.add(missingOrInvalid(fieldName, "boolean", manifestPath));
            return null;
        }
        return field.booleanValue();
    }

    private OutputCodecCatalog requiredOutputCodec(JsonNode node, List<PackerDiagnostic> diagnostics, Path manifestPath) {
        final String codecValue = requiredText(node, "codec", diagnostics, manifestPath);
        if (codecValue == null) {
            return null;
        }
        final OutputCodecCatalog outputCodec = OutputCodecCatalog.fromManifestValue(codecValue);
        if (outputCodec == OutputCodecCatalog.UNKNOWN) {
            diagnostics.add(new PackerDiagnostic(
                    PackerDiagnosticSeverity.ERROR,
                    PackerDiagnosticCategory.STRUCTURAL,
                    "Field 'codec' must be one of: NONE.",
                    manifestPath,
                    true));
            return null;
        }
        return outputCodec;
    }

    private Map<String, List<String>> requiredInputs(JsonNode inputsNode, List<PackerDiagnostic> diagnostics, Path manifestPath) {
        if (!inputsNode.isObject()) {
            diagnostics.add(missingOrInvalid("inputs", "object of input roles", manifestPath));
            return Map.of();
        }

        final Map<String, List<String>> result = new LinkedHashMap<>();
        inputsNode.fields().forEachRemaining(entry -> {
            if (!entry.getValue().isArray()) {
                diagnostics.add(new PackerDiagnostic(
                        PackerDiagnosticSeverity.ERROR,
                        PackerDiagnosticCategory.STRUCTURAL,
                        "Input role '" + entry.getKey() + "' must be an array of relative paths.",
                        manifestPath,
                        true));
                return;
            }
            final List<String> values = new ArrayList<>();
            entry.getValue().forEach(value -> {
                if (!value.isTextual() || value.asText().isBlank()) {
                    diagnostics.add(new PackerDiagnostic(
                            PackerDiagnosticSeverity.ERROR,
                            PackerDiagnosticCategory.STRUCTURAL,
                            "Input role '" + entry.getKey() + "' must contain only non-blank path strings.",
                            manifestPath,
                            true));
                    return;
                }
                final String relativePath = value.asText().trim();
                if (!isTrustedRelativePath(relativePath)) {
                    diagnostics.add(new PackerDiagnostic(
                            PackerDiagnosticSeverity.ERROR,
                            PackerDiagnosticCategory.STRUCTURAL,
                            "Input role '" + entry.getKey() + "' contains an untrusted path outside the asset root.",
                            manifestPath,
                            true));
                    return;
                }
                values.add(relativePath);
            });
            result.put(entry.getKey(), List.copyOf(values));
        });
        return Map.copyOf(result);
    }

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

    private PackerDiagnostic missingOrInvalid(String fieldName, String expected, Path manifestPath) {
        return new PackerDiagnostic(
                PackerDiagnosticSeverity.ERROR,
                PackerDiagnosticCategory.STRUCTURAL,
                "Field '" + fieldName + "' must be a " + expected + ".",
                manifestPath,
                true);
    }
}
