package p.packer.services;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import p.packer.messages.assets.AssetFamilyCatalog;
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.PackerDiagnostic;
import p.packer.models.PackerRuntimeAsset;
import p.packer.models.PackerRuntimeWalkFile;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;

final class PackerGlyphMaterialSupport {
    static final String GLYPH_MATERIAL_SUPPORT_FILE = "glyph-material.json";

    private final ObjectMapper mapper;

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

    List<PackerDiagnostic> validate(PackerRuntimeAsset runtimeAsset) {
        final PackerAssetDeclaration declaration = runtimeAsset.parsedDeclaration().declaration();
        if (declaration == null || declaration.assetFamily() != AssetFamilyCatalog.GLYPH_BANK) {
            return List.of();
        }
        final Path supportFile = runtimeAsset.assetRoot().resolve(GLYPH_MATERIAL_SUPPORT_FILE);
        if (!Files.isRegularFile(supportFile)) {
            return List.of();
        }

        final List<PackerDiagnostic> diagnostics = new ArrayList<>();
        final Optional<GlyphMaterialFile> parsed = readSupportFile(supportFile, diagnostics);
        if (parsed.isEmpty()) {
            return List.copyOf(diagnostics);
        }

        final Map<String, PackerAssetArtifactSelection> selectedArtifactsById = new LinkedHashMap<>();
        for (PackerAssetArtifactSelection artifact : declaration.artifacts()) {
            if (artifact.artifactId() != null) {
                selectedArtifactsById.put(artifact.artifactId(), artifact);
            }
        }
        final Map<String, PackerRuntimeWalkFile> selectedFilesByPath = new LinkedHashMap<>();
        runtimeAsset.walkProjection().buildCandidateFiles().forEach(file -> selectedFilesByPath.put(file.relativePath(), file));

        final Map<String, SchemaEvaluation> schemaEvaluations = new LinkedHashMap<>();
        for (GlyphMaterialSchema schema : parsed.get().schemas()) {
            final boolean declaredInconsistent = schema.status() == GlyphMaterialSchemaStatus.INCONSISTENT;
            boolean drifted = false;
            for (GlyphMaterialArtifactBinding binding : schema.artifactBindings()) {
                final PackerAssetArtifactSelection artifact = selectedArtifactsById.get(binding.artifactId());
                if (artifact == null) {
                    diagnostics.add(blocking(
                            "Glyph material schema '" + schema.schemaId() + "' references unknown selected artifact id '" + binding.artifactId() + "'.",
                            supportFile));
                    drifted = true;
                    continue;
                }
                final PackerRuntimeWalkFile currentFile = selectedFilesByPath.get(artifact.file());
                final String currentFingerprint = currentFile != null
                        ? currentFile.fingerprint()
                        : artifact.fingerprint();
                if (currentFingerprint == null || !currentFingerprint.equals(binding.fingerprint())) {
                    diagnostics.add(blocking(
                            "Glyph material schema '" + schema.schemaId() + "' is inconsistent because artifact '" + binding.artifactId() + "' drifted.",
                            supportFile));
                    drifted = true;
                }
            }
            if (declaredInconsistent) {
                diagnostics.add(blocking(
                        "Glyph material schema '" + schema.schemaId() + "' is marked inconsistent.",
                        supportFile));
            }
            schemaEvaluations.put(schema.schemaId(), new SchemaEvaluation(schema, declaredInconsistent || drifted));
        }

        for (GlyphMaterialPalette palette : parsed.get().palettes()) {
            final SchemaEvaluation schema = schemaEvaluations.get(palette.schemaId());
            if (schema == null) {
                diagnostics.add(blocking(
                        "Glyph material palette '" + palette.paletteId() + "' references unknown schema '" + palette.schemaId() + "'.",
                        supportFile));
                continue;
            }
            if (palette.colors().size() != schema.schema().materialIndices().size()) {
                diagnostics.add(blocking(
                        "Glyph material palette '" + palette.paletteId() + "' must define exactly "
                                + schema.schema().materialIndices().size() + " colors for schema '" + palette.schemaId() + "'.",
                        supportFile));
            }
            if (palette.version() < schema.schema().version()) {
                diagnostics.add(blocking(
                        "Glyph material palette '" + palette.paletteId() + "' is stale because version "
                                + palette.version() + " is older than schema '" + palette.schemaId() + "' version "
                                + schema.schema().version() + ".",
                        supportFile));
            }
            if (schema.inconsistent()) {
                diagnostics.add(blocking(
                        "Glyph material palette '" + palette.paletteId() + "' is invalid because schema '"
                                + palette.schemaId() + "' is inconsistent.",
                        supportFile));
            }
        }

        return List.copyOf(diagnostics);
    }

    private Optional<GlyphMaterialFile> readSupportFile(Path supportFile, List<PackerDiagnostic> diagnostics) {
        final JsonNode root;
        try {
            root = mapper.readTree(supportFile.toFile());
        } catch (IOException exception) {
            diagnostics.add(blocking("Unable to parse glyph-material.json: " + exception.getMessage(), supportFile));
            return Optional.empty();
        }
        if (root == null || !root.isObject()) {
            diagnostics.add(blocking("glyph-material.json must contain a JSON object at the root.", supportFile));
            return Optional.empty();
        }
        if (!(root.path("schemas") instanceof ArrayNode schemasNode) || !(root.path("palettes") instanceof ArrayNode palettesNode)) {
            diagnostics.add(blocking("glyph-material.json must define array fields 'schemas' and 'palettes'.", supportFile));
            return Optional.empty();
        }

        final List<GlyphMaterialSchema> schemas = new ArrayList<>();
        final Set<String> schemaIds = new HashSet<>();
        for (JsonNode schemaNode : schemasNode) {
            final Optional<GlyphMaterialSchema> schema = parseSchema(schemaNode, supportFile, diagnostics);
            if (schema.isEmpty()) {
                return Optional.empty();
            }
            if (!schemaIds.add(schema.get().schemaId())) {
                diagnostics.add(blocking("glyph-material.json contains duplicate schema_id '" + schema.get().schemaId() + "'.", supportFile));
                return Optional.empty();
            }
            schemas.add(schema.get());
        }

        final List<GlyphMaterialPalette> palettes = new ArrayList<>();
        final Set<String> paletteIds = new HashSet<>();
        for (JsonNode paletteNode : palettesNode) {
            final Optional<GlyphMaterialPalette> palette = parsePalette(paletteNode, schemaIds, supportFile, diagnostics);
            if (palette.isEmpty()) {
                return Optional.empty();
            }
            if (!paletteIds.add(palette.get().paletteId())) {
                diagnostics.add(blocking("glyph-material.json contains duplicate palette_id '" + palette.get().paletteId() + "'.", supportFile));
                return Optional.empty();
            }
            palettes.add(palette.get());
        }
        return Optional.of(new GlyphMaterialFile(List.copyOf(schemas), List.copyOf(palettes)));
    }

    private Optional<GlyphMaterialSchema> parseSchema(
            JsonNode schemaNode,
            Path supportFile,
            List<PackerDiagnostic> diagnostics) {
        if (!schemaNode.isObject()) {
            diagnostics.add(blocking("Each glyph material schema must be an object.", supportFile));
            return Optional.empty();
        }
        final String schemaId = schemaNode.path("schema_id").asText("").trim();
        final GlyphMaterialSchemaStatus status = GlyphMaterialSchemaStatus.fromManifestValue(schemaNode.path("status").asText(null));
        final int version = schemaNode.path("version").asInt(0);
        if (schemaId.isBlank() || status == null || version <= 0) {
            diagnostics.add(blocking("Each glyph material schema must define non-blank 'schema_id', valid 'status', and positive 'version'.", supportFile));
            return Optional.empty();
        }
        final JsonNode materialIndicesNode = schemaNode.path("material_indices");
        if (!materialIndicesNode.isObject()) {
            diagnostics.add(blocking("Schema '" + schemaId + "' must define object 'material_indices'.", supportFile));
            return Optional.empty();
        }
        final Map<Integer, String> materialIndices = new TreeMap<>();
        materialIndicesNode.fields().forEachRemaining(entry -> {
            try {
                final int index = Integer.parseInt(entry.getKey());
                final String color = normalizeHtmlColor(entry.getValue().asText(null));
                if (index >= 1 && index <= 15 && color != null) {
                    materialIndices.put(index, color);
                }
            } catch (NumberFormatException ignored) {
                // validated below
            }
        });
        if (materialIndices.isEmpty()
                || materialIndices.size() != materialIndicesNode.size()
                || !denseIndices(materialIndices.keySet())) {
            diagnostics.add(blocking("Schema '" + schemaId + "' must define dense material_indices in range 1..15 using #RRGGBB values.", supportFile));
            return Optional.empty();
        }
        if (!(schemaNode.path("artifact_bindings") instanceof ArrayNode bindingsNode) || bindingsNode.isEmpty()) {
            diagnostics.add(blocking("Schema '" + schemaId + "' must define non-empty 'artifact_bindings'.", supportFile));
            return Optional.empty();
        }
        final List<GlyphMaterialArtifactBinding> bindings = new ArrayList<>();
        final Set<String> artifactIds = new HashSet<>();
        for (JsonNode bindingNode : bindingsNode) {
            final String artifactId = bindingNode.path("artifact_id").asText("").trim();
            final String fingerprint = bindingNode.path("fingerprint").asText("").trim();
            if (artifactId.isBlank() || fingerprint.isBlank() || !artifactIds.add(artifactId)) {
                diagnostics.add(blocking("Schema '" + schemaId + "' must define unique artifact bindings with 'artifact_id' and 'fingerprint'.", supportFile));
                return Optional.empty();
            }
            bindings.add(new GlyphMaterialArtifactBinding(artifactId, fingerprint));
        }
        return Optional.of(new GlyphMaterialSchema(schemaId, status, version, Map.copyOf(materialIndices), List.copyOf(bindings)));
    }

    private Optional<GlyphMaterialPalette> parsePalette(
            JsonNode paletteNode,
            Set<String> schemaIds,
            Path supportFile,
            List<PackerDiagnostic> diagnostics) {
        if (!paletteNode.isObject()) {
            diagnostics.add(blocking("Each glyph material palette must be an object.", supportFile));
            return Optional.empty();
        }
        final String paletteId = paletteNode.path("palette_id").asText("").trim();
        final String schemaId = paletteNode.path("schema_id").asText("").trim();
        final int version = paletteNode.path("version").asInt(0);
        if (paletteId.isBlank() || schemaId.isBlank() || version <= 0 || !schemaIds.contains(schemaId)) {
            diagnostics.add(blocking("Each glyph material palette must define non-blank 'palette_id', valid 'schema_id', and positive 'version'.", supportFile));
            return Optional.empty();
        }
        if (!(paletteNode.path("colors") instanceof ArrayNode colorsNode)) {
            diagnostics.add(blocking("Palette '" + paletteId + "' must define array 'colors'.", supportFile));
            return Optional.empty();
        }
        final List<String> colors = new ArrayList<>();
        for (JsonNode colorNode : colorsNode) {
            final String color = normalizeHtmlColor(colorNode.asText(null));
            if (color == null) {
                diagnostics.add(blocking("Palette '" + paletteId + "' must use #RRGGBB colors.", supportFile));
                return Optional.empty();
            }
            colors.add(color);
        }
        return Optional.of(new GlyphMaterialPalette(paletteId, schemaId, version, List.copyOf(colors)));
    }

    private static PackerDiagnostic blocking(String message, Path path) {
        return new PackerDiagnostic(
                PackerDiagnosticSeverity.ERROR,
                PackerDiagnosticCategory.STRUCTURAL,
                message,
                path,
                true);
    }

    private boolean denseIndices(Set<Integer> indices) {
        int expected = 1;
        for (Integer index : new TreeSet<>(indices)) {
            if (index == null || index != expected) {
                return false;
            }
            expected += 1;
        }
        return !indices.isEmpty();
    }

    private String normalizeHtmlColor(String value) {
        if (value == null) {
            return null;
        }
        final String normalized = value.trim().toUpperCase();
        return normalized.matches("^#[0-9A-F]{6}$") ? normalized : null;
    }

    private record GlyphMaterialFile(
            List<GlyphMaterialSchema> schemas,
            List<GlyphMaterialPalette> palettes) {
    }

    private record GlyphMaterialSchema(
            String schemaId,
            GlyphMaterialSchemaStatus status,
            int version,
            Map<Integer, String> materialIndices,
            List<GlyphMaterialArtifactBinding> artifactBindings) {
    }

    private record GlyphMaterialPalette(
            String paletteId,
            String schemaId,
            int version,
            List<String> colors) {
    }

    private record GlyphMaterialArtifactBinding(
            String artifactId,
            String fingerprint) {
    }

    private record SchemaEvaluation(
            GlyphMaterialSchema schema,
            boolean inconsistent) {
    }

    private enum GlyphMaterialSchemaStatus {
        OK("ok"),
        INCONSISTENT("inconsistent");

        private final String manifestValue;

        GlyphMaterialSchemaStatus(String manifestValue) {
            this.manifestValue = manifestValue;
        }

        static GlyphMaterialSchemaStatus fromManifestValue(String value) {
            if (value == null) {
                return null;
            }
            for (GlyphMaterialSchemaStatus candidate : values()) {
                if (candidate.manifestValue.equalsIgnoreCase(value.trim())) {
                    return candidate;
                }
            }
            return null;
        }
    }
}
