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.assets.OutputFormatCatalog;
import p.packer.messages.diagnostics.PackerDiagnosticCategory;
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
import p.packer.models.PackerAssetArtifactSelection;
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.*;

public final class PackerAssetDeclarationParser {
    private static final Set<Integer> SUPPORTED_SCHEMA_VERSIONS = Set.of(1);
    private static final int MAX_PIPELINE_PALETTES = 64;

    private final ObjectMapper mapper;

    public PackerAssetDeclarationParser(ObjectMapper mapper) {
        this.mapper = Objects.requireNonNull(mapper, "mapper");
    }

    public PackerAssetDeclarationParseResult parse(final Path assetManifestPath) {
        final var 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 var schemaVersion = requiredInt(root, "schema_version", diagnostics, manifestPath);
        final var assetUuid = requiredText(root, "asset_uuid", diagnostics, manifestPath);
        final var name = requiredText(root, "name", diagnostics, manifestPath);
        final var assetFamily = requiredAssetFamily(root, diagnostics, manifestPath);
        rejectLegacyInputs(root.path("inputs"), diagnostics, manifestPath);
        final var artifacts = optionalArtifacts(root.path("artifacts"), diagnostics, manifestPath);
        final var outputFormat = requiredOutputFormat(root.path("output"), diagnostics, manifestPath);
        final var outputCodec = requiredOutputCodec(root.path("output"), diagnostics, manifestPath);
        final var outputMetadata = optionalOutputMetadata(root.path("output"), diagnostics, manifestPath);
        final var outputPipelineMetadata = optionalOutputPipelineMetadata(root.path("output"), diagnostics, manifestPath);
        final var preloadEnabled = requiredBoolean(root.path("preload"), "enabled", diagnostics, manifestPath);

        if (schemaVersion != null && !SUPPORTED_SCHEMA_VERSIONS.contains(schemaVersion)) {
            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,
                        artifacts,
                        outputFormat,
                        outputCodec,
                        outputMetadata,
                        outputPipelineMetadata,
                        preloadEnabled),
                diagnostics);
    }

    private Integer requiredInt(
            final JsonNode node,
            final String fieldName,
            final List<PackerDiagnostic> diagnostics,
            final 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(
            final JsonNode node,
            final String fieldName,
            final List<PackerDiagnostic> diagnostics,
            final 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(
            final JsonNode node,
            final List<PackerDiagnostic> diagnostics,
            final 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: glyph_bank, scene_bank, sound_bank.",
                    manifestPath,
                    true));
            return null;
        }
        return assetFamily;
    }

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

    private OutputFormatCatalog requiredOutputFormat(
            final JsonNode node,
            final List<PackerDiagnostic> diagnostics,
            final Path manifestPath) {
        final String fmtValue = requiredText(node, "format", diagnostics, manifestPath);
        if (fmtValue == null) {
            return null;
        }
        final OutputFormatCatalog outputFormat = OutputFormatCatalog.fromManifestValue(fmtValue);
        if (outputFormat == OutputFormatCatalog.UNKNOWN) {
            diagnostics.add(new PackerDiagnostic(
                    PackerDiagnosticSeverity.ERROR,
                    PackerDiagnosticCategory.STRUCTURAL,
                    "Field 'output' must have a valid value.",
                    manifestPath,
                    true));
            return null;
        }
        return outputFormat;
    }

    private OutputCodecCatalog requiredOutputCodec(
            final JsonNode node,
            final List<PackerDiagnostic> diagnostics,
            final 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 have a valid value.",
                    manifestPath,
                    true));
            return null;
        }
        return outputCodec;
    }

    private Map<String, String> optionalOutputMetadata(
            final JsonNode node,
            final List<PackerDiagnostic> diagnostics,
            final Path manifestPath) {
        final JsonNode metadataNode = node.path("metadata");
        if (metadataNode.isMissingNode() || metadataNode.isNull()) {
            return Map.of();
        }
        if (!metadataNode.isObject()) {
            diagnostics.add(new PackerDiagnostic(
                    PackerDiagnosticSeverity.ERROR,
                    PackerDiagnosticCategory.STRUCTURAL,
                    "Field 'output.metadata' must be a JSON object.",
                    manifestPath,
                    true));
            return Map.of();
        }

        final Map<String, String> metadata = new LinkedHashMap<>();
        metadataNode.fields().forEachRemaining(entry -> {
            final String key = Objects.requireNonNullElse(entry.getKey(), "").trim();
            if (key.isBlank()) {
                diagnostics.add(new PackerDiagnostic(
                        PackerDiagnosticSeverity.ERROR,
                        PackerDiagnosticCategory.STRUCTURAL,
                        "Field 'output.metadata' has an invalid empty key.",
                        manifestPath,
                        true));
                return;
            }

            final JsonNode valueNode = entry.getValue();
            if (valueNode == null || valueNode.isContainerNode()) {
                diagnostics.add(new PackerDiagnostic(
                        PackerDiagnosticSeverity.ERROR,
                        PackerDiagnosticCategory.STRUCTURAL,
                        "Field 'output.metadata' values must be scalar (text/number/boolean).",
                        manifestPath,
                        true));
                return;
            }
            metadata.put(key, valueNode.asText());
        });

        return Map.copyOf(metadata);
    }

    private Map<String, JsonNode> optionalOutputPipelineMetadata(
            final JsonNode node,
            final List<PackerDiagnostic> diagnostics,
            final Path manifestPath) {
        final JsonNode pipelineNode = node.path("pipeline");
        if (pipelineNode.isMissingNode() || pipelineNode.isNull()) {
            return Map.of();
        }
        if (!pipelineNode.isObject()) {
            diagnostics.add(new PackerDiagnostic(
                    PackerDiagnosticSeverity.ERROR,
                    PackerDiagnosticCategory.STRUCTURAL,
                    "Field 'output.pipeline' must be a JSON object.",
                    manifestPath,
                    true));
            return Map.of();
        }

        final Map<String, JsonNode> pipelineMetadata = new LinkedHashMap<>();
        pipelineNode.fields().forEachRemaining(entry -> {
            final String key = Objects.requireNonNullElse(entry.getKey(), "").trim();
            if (key.isBlank()) {
                diagnostics.add(new PackerDiagnostic(
                        PackerDiagnosticSeverity.ERROR,
                        PackerDiagnosticCategory.STRUCTURAL,
                        "Field 'output.pipeline' has an invalid empty key.",
                        manifestPath,
                        true));
                return;
            }
            final JsonNode valueNode = entry.getValue();
            if (valueNode == null) {
                diagnostics.add(new PackerDiagnostic(
                        PackerDiagnosticSeverity.ERROR,
                        PackerDiagnosticCategory.STRUCTURAL,
                        "Field 'output.pipeline' values must be valid JSON values.",
                        manifestPath,
                        true));
                return;
            }
            if ("palettes".equals(key)) {
                pipelineMetadata.put(key, validatePaletteDeclarations(valueNode, diagnostics, manifestPath));
                return;
            }
            pipelineMetadata.put(key, valueNode.deepCopy());
        });
        return Map.copyOf(pipelineMetadata);
    }

    private JsonNode validatePaletteDeclarations(
            final JsonNode node,
            final List<PackerDiagnostic> diagnostics,
            final Path manifestPath) {
        if (!node.isArray()) {
            diagnostics.add(new PackerDiagnostic(
                    PackerDiagnosticSeverity.ERROR,
                    PackerDiagnosticCategory.STRUCTURAL,
                    "Field 'output.pipeline.palettes' must be an array of palette declarations.",
                    manifestPath,
                    true));
            return node.deepCopy();
        }

        final Set<Integer> seenIndices = new HashSet<>();
        int paletteCount = 0;
        for (JsonNode entry : node) {
            paletteCount += 1;
            if (!entry.isObject()) {
                diagnostics.add(new PackerDiagnostic(
                        PackerDiagnosticSeverity.ERROR,
                        PackerDiagnosticCategory.STRUCTURAL,
                        "Each palette declaration must be an object with non-negative integer 'index' and object 'palette'.",
                        manifestPath,
                        true));
                continue;
            }

            final JsonNode indexNode = entry.path("index");
            final JsonNode paletteNode = entry.path("palette");
            if (!indexNode.isInt() || indexNode.intValue() < 0) {
                diagnostics.add(new PackerDiagnostic(
                        PackerDiagnosticSeverity.ERROR,
                        PackerDiagnosticCategory.STRUCTURAL,
                        "Each palette declaration must define a non-negative integer 'index'.",
                        manifestPath,
                        true));
            } else if (!seenIndices.add(indexNode.intValue())) {
                diagnostics.add(new PackerDiagnostic(
                        PackerDiagnosticSeverity.ERROR,
                        PackerDiagnosticCategory.STRUCTURAL,
                        "Field 'output.pipeline.palettes' contains duplicate palette index " + indexNode.intValue() + ".",
                        manifestPath,
                        true));
            }

            if (!paletteNode.isObject()) {
                diagnostics.add(new PackerDiagnostic(
                        PackerDiagnosticSeverity.ERROR,
                        PackerDiagnosticCategory.STRUCTURAL,
                        "Each palette declaration must define an object 'palette' payload.",
                        manifestPath,
                        true));
                continue;
            }

            final JsonNode originalNode = paletteNode.path("originalArgb8888");
            final JsonNode convertedNode = paletteNode.path("convertedRgb565");
            if (!originalNode.isArray() || !convertedNode.isArray()) {
                diagnostics.add(new PackerDiagnostic(
                        PackerDiagnosticSeverity.ERROR,
                        PackerDiagnosticCategory.STRUCTURAL,
                        "Each palette payload must contain array fields 'originalArgb8888' and 'convertedRgb565'.",
                        manifestPath,
                        true));
                continue;
            }
        }

        if (paletteCount > MAX_PIPELINE_PALETTES) {
            diagnostics.add(new PackerDiagnostic(
                    PackerDiagnosticSeverity.ERROR,
                    PackerDiagnosticCategory.STRUCTURAL,
                    "Field 'output.pipeline.palettes' cannot declare more than " + MAX_PIPELINE_PALETTES + " palettes.",
                    manifestPath,
                    true));
        }

        return node.deepCopy();
    }

    private void rejectLegacyInputs(
            final JsonNode node,
            final List<PackerDiagnostic> diagnostics,
            final Path manifestPath) {
        if (node.isMissingNode() || node.isNull()) {
            return;
        }
        diagnostics.add(new PackerDiagnostic(
                PackerDiagnosticSeverity.ERROR,
                PackerDiagnosticCategory.STRUCTURAL,
                "Field 'inputs' is no longer supported; use 'artifacts' instead.",
                manifestPath,
                true));
    }

    private List<PackerAssetArtifactSelection> optionalArtifacts(
            final JsonNode node,
            final List<PackerDiagnostic> diagnostics,
            final Path manifestPath) {
        if (node.isMissingNode() || node.isNull()) {
            return List.of();
        }
        if (!node.isArray()) {
            diagnostics.add(new PackerDiagnostic(
                    PackerDiagnosticSeverity.ERROR,
                    PackerDiagnosticCategory.STRUCTURAL,
                    "Field 'artifacts' must be an array.",
                    manifestPath,
                    true));
            return List.of();
        }

        final List<PackerAssetArtifactSelection> result = new ArrayList<>();
        for (JsonNode entry : node) {
            if (!entry.isObject()) {
                diagnostics.add(new PackerDiagnostic(
                        PackerDiagnosticSeverity.ERROR,
                        PackerDiagnosticCategory.STRUCTURAL,
                        "Each artifact entry must be an object with 'file' and 'index'.",
                        manifestPath,
                        true));
                continue;
            }
            final JsonNode artifactIdNode = entry.path("artifact_id");
            final JsonNode fileNode = entry.path("file");
            final JsonNode indexNode = entry.path("index");
            final JsonNode fingerprintNode = entry.path("fingerprint");
            if (!fileNode.isTextual() || fileNode.asText().isBlank() || !indexNode.isInt() || indexNode.asInt() < 0) {
                diagnostics.add(new PackerDiagnostic(
                        PackerDiagnosticSeverity.ERROR,
                        PackerDiagnosticCategory.STRUCTURAL,
                        "Each artifact entry must define a non-blank 'file' and a non-negative integer 'index'.",
                        manifestPath,
                        true));
                continue;
            }
            final String relativePath = fileNode.asText().trim();
            if (!isTrustedRelativePath(relativePath)) {
                diagnostics.add(new PackerDiagnostic(
                        PackerDiagnosticSeverity.ERROR,
                        PackerDiagnosticCategory.STRUCTURAL,
                        "Artifact file path must stay inside the asset root.",
                        manifestPath,
                        true));
                continue;
            }
            final String artifactId = artifactIdNode.isTextual() && !artifactIdNode.asText().isBlank()
                    ? artifactIdNode.asText().trim()
                    : null;
            final String fingerprint = fingerprintNode.isTextual() && !fingerprintNode.asText().isBlank()
                    ? fingerprintNode.asText().trim()
                    : null;
            result.add(new PackerAssetArtifactSelection(artifactId, relativePath, indexNode.asInt(), fingerprint));
        }
        result.sort(Comparator.comparingInt(PackerAssetArtifactSelection::index));
        return List.copyOf(result);
    }

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

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