package p.studio.workspaces.assets.details.palette;

import p.packer.messages.assets.AssetFamilyCatalog;
import p.studio.controls.forms.StudioFormMode;
import p.studio.controls.forms.StudioFormSession;
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetDetails;
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile;
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialArtifactBinding;
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialMetadata;
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialPalette;
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialSchema;
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialSchemaStatus;
import p.studio.workspaces.assets.details.schema.AssetDetailsColorSchemaCoordinator;

import java.util.*;

public final class AssetDetailsPaletteOverhaulingCoordinator {
    private StudioFormSession<AssetDetailsPaletteOverhaulingDraft> formSession;
    private AssetWorkspaceAssetDetails details;
    private AssetStudioGlyphMaterialMetadata localMaterialMetadata;

    public void replaceDetails(AssetWorkspaceAssetDetails details) {
        this.details = details;
        if (details == null || details.summary().assetFamily() != AssetFamilyCatalog.GLYPH_BANK) {
            formSession = null;
            localMaterialMetadata = null;
            return;
        }
        localMaterialMetadata = details.glyphMaterialMetadata();
        rebuildSession();
    }

    public void replaceMaterialMetadata(AssetStudioGlyphMaterialMetadata metadata) {
        if (details == null || details.summary().assetFamily() != AssetFamilyCatalog.GLYPH_BANK) {
            return;
        }
        localMaterialMetadata = Objects.requireNonNull(metadata, "metadata");
        rebuildSession();
    }

    private void rebuildSession() {
        final List<AssetWorkspaceBankCompositionFile> allFiles = paletteFiles(details);
        final List<AssetWorkspaceBankCompositionFile> previewTileFiles = details.bankComposition().selectedFiles().stream()
                .filter(AssetDetailsPaletteOverhaulingCoordinator::supportsPalettePreview)
                .toList();
        final List<AssetWorkspaceBankCompositionFile> initialSelected = initialSelectedPaletteFiles(details, allFiles);

        final List<AssetWorkspaceBankCompositionFile> available = new ArrayList<>(allFiles);
        available.removeAll(initialSelected);
        final AssetDetailsPaletteOverhaulingDraft source = new AssetDetailsPaletteOverhaulingDraft(
                previewTileFiles,
                allFiles,
                available,
                initialSelected,
                previewTileFiles.isEmpty() ? null : previewTileFiles.getFirst().path(),
                initialSelected.isEmpty()
                        ? allFiles.isEmpty() ? null : allFiles.getFirst().path()
                        : initialSelected.getFirst().path());
        if (formSession == null) {
            formSession = new StudioFormSession<>(source);
            return;
        }
        if (!Objects.equals(formSession.source(), source)) {
            if (formSession.mode() == StudioFormMode.EDITING) {
                final AssetDetailsPaletteOverhaulingDraft currentDraft = formSession.draft();
                formSession = new StudioFormSession<>(source);
                formSession.beginEdit();
                formSession.setDraft(rebindDraft(currentDraft, source));
                return;
            }
            formSession.replaceSource(source);
        }
    }

    public boolean ready() {
        return formSession != null;
    }

    public AssetWorkspaceAssetDetails details() {
        return details;
    }

    public void beginEdit() {
        if (ready()) {
            formSession.beginEdit();
        }
    }

    public void apply() {
        if (ready()) {
            formSession.apply();
        }
    }

    public void reset() {
        if (ready()) {
            formSession.resetDraft();
        }
    }

    public void cancel() {
        if (ready()) {
            formSession.cancelEdit();
        }
    }

    public void moveToSelected(List<AssetWorkspaceBankCompositionFile> files) {
        if (ready() && formSession.mode() == StudioFormMode.EDITING) {
            final List<AssetWorkspaceBankCompositionFile> validFiles = files.stream()
                    .filter(AssetDetailsPaletteOverhaulingCoordinator::paletteIsValid)
                    .toList();
            formSession.updateDraft(current -> current.moveToSelected(validFiles));
        }
    }

    public void moveToAvailable(List<AssetWorkspaceBankCompositionFile> files) {
        if (ready() && formSession.mode() == StudioFormMode.EDITING) {
            formSession.updateDraft(current -> current.moveToAvailable(files));
        }
    }

    public void selectPreviewTile(AssetWorkspaceBankCompositionFile file) {
        if (ready() && file != null) {
            formSession.updateDraft(current -> current.selectPreviewTile(file.path()));
        }
    }

    public void selectPreviewPalette(AssetWorkspaceBankCompositionFile file) {
        if (ready() && file != null) {
            formSession.updateDraft(current -> current.selectPreviewPalette(file.path()));
        }
    }

    public AssetDetailsPaletteOverhaulingViewModel viewModel() {
        if (!ready()) {
            return new AssetDetailsPaletteOverhaulingViewModel(false, false, List.of(), List.of(), List.of(), List.of(), null, null, 0);
        }

        final AssetDetailsPaletteOverhaulingDraft draft = formSession.draft();
        final AssetWorkspaceBankCompositionFile previewPaletteFile = fileByPath(draft.allFiles(), draft.previewPalettePath());
        final List<AssetWorkspaceBankCompositionFile> schemaPreviewTileFiles = previewTileFilesForPalette(
                draft.previewTileFiles(),
                previewPaletteFile);
        final AssetWorkspaceBankCompositionFile previewTileFile = Optional
                .ofNullable(fileByPath(schemaPreviewTileFiles, draft.previewTilePath()))
                .orElse(schemaPreviewTileFiles.isEmpty() ? null : schemaPreviewTileFiles.getFirst());
        return new AssetDetailsPaletteOverhaulingViewModel(
                formSession.mode() == StudioFormMode.EDITING,
                formSession.isDirty(),
                schemaPreviewTileFiles,
                draft.allFiles(),
                draft.availableFiles(),
                draft.selectedFiles(),
                previewTileFile,
                previewPaletteFile,
                draft.allFiles().size());
    }

    public List<AssetStudioGlyphMaterialSchema> readySchemas() {
        return materialMetadata().schemas().stream()
                .filter(schema -> schema.status() == AssetStudioGlyphMaterialSchemaStatus.OK)
                .toList();
    }

    public String nextPaletteId() {
        int next = 1;
        final Set<String> ids = materialMetadata().palettes().stream()
                .map(AssetStudioGlyphMaterialPalette::paletteId)
                .collect(java.util.stream.Collectors.toSet());
        while (ids.contains("palette-" + next)) {
            next += 1;
        }
        return "palette-" + next;
    }

    public boolean duplicatePaletteExists(String paletteId, String schemaId, List<String> colors) {
        final List<String> normalized = colors.stream().map(AssetDetailsPaletteOverhaulingCoordinator::normalizeHtmlColor).toList();
        return allPaletteRecords().stream()
                .filter(record -> !Objects.equals(record.paletteId(), paletteId))
                .anyMatch(record -> record.schemaId().equals(schemaId) && record.colors().equals(normalized));
    }

    public AssetStudioGlyphMaterialMetadata upsertPalette(String paletteId, String displayName, String schemaId, List<String> colors) {
        final AssetStudioGlyphMaterialMetadata current = materialMetadata();
        final List<String> normalized = colors.stream().map(AssetDetailsPaletteOverhaulingCoordinator::normalizeHtmlColor).toList();
        final int schemaVersion = current.findSchema(schemaId).map(AssetStudioGlyphMaterialSchema::version).orElse(1);
        final String resolvedDisplayName = displayName == null || displayName.trim().isBlank()
                ? generatedDisplayName(normalized)
                : displayName.trim();
        final AssetStudioGlyphMaterialPalette next = new AssetStudioGlyphMaterialPalette(
                paletteId,
                resolvedDisplayName,
                schemaId,
                schemaVersion,
                normalized);
        final List<AssetStudioGlyphMaterialPalette> palettes = new ArrayList<>();
        boolean replaced = false;
        for (AssetStudioGlyphMaterialPalette palette : current.palettes()) {
            if (palette.paletteId().equals(paletteId)) {
                palettes.add(next);
                replaced = true;
            } else {
                palettes.add(palette);
            }
        }
        if (!replaced) {
            palettes.add(next);
        }
        palettes.sort(Comparator.comparing(AssetStudioGlyphMaterialPalette::paletteId, String.CASE_INSENSITIVE_ORDER));
        return new AssetStudioGlyphMaterialMetadata(current.schemas(), palettes, current.supportFile());
    }

    public AssetStudioGlyphMaterialMetadata removePalette(String paletteId) {
        final AssetWorkspaceBankCompositionFile paletteFile = paletteFileById(paletteId);
        if (paletteFile != null && Boolean.TRUE.equals(paletteFile.metadata().get("derivedOriginal"))) {
            throw new IllegalStateException("Original schema palettes are derived and cannot be removed.");
        }
        final AssetStudioGlyphMaterialMetadata current = materialMetadata();
        final List<AssetStudioGlyphMaterialPalette> palettes = current.palettes().stream()
                .filter(palette -> !palette.paletteId().equals(paletteId))
                .toList();
        return new AssetStudioGlyphMaterialMetadata(current.schemas(), palettes, current.supportFile());
    }

    public AssetWorkspaceBankCompositionFile paletteFileById(String paletteId) {
        if (paletteId == null || !ready()) {
            return null;
        }
        return paletteFiles(details).stream()
                .filter(file -> paletteId.equals(file.metadata().get("paletteId")))
                .findFirst()
                .orElse(null);
    }

    public static boolean supportsPalettePreview(AssetWorkspaceBankCompositionFile file) {
        if (file == null) {
            return false;
        }
        return nestedMap(file.metadata(), "tile") != null && nestedMap(file.metadata(), "palette") != null;
    }

    public static boolean paletteIsValid(AssetWorkspaceBankCompositionFile file) {
        return !Boolean.FALSE.equals(file.metadata().get("valid"));
    }

    public static boolean paletteIsOriginal(AssetWorkspaceBankCompositionFile file) {
        return file != null && Boolean.TRUE.equals(file.metadata().get("derivedOriginal"));
    }

    @SuppressWarnings("unchecked")
    static Map<String, Object> nestedMap(Map<String, Object> metadata, String key) {
        final Object value = metadata.get(key);
        if (value instanceof Map<?, ?> nested) {
            return (Map<String, Object>) nested;
        }
        return null;
    }

    private AssetStudioGlyphMaterialMetadata materialMetadata() {
        if (details == null) {
            return new AssetStudioGlyphMaterialMetadata(List.of(), List.of(), null);
        }
        return localMaterialMetadata == null
                ? new AssetStudioGlyphMaterialMetadata(List.of(), List.of(), null)
                : localMaterialMetadata;
    }

    private List<AssetWorkspaceBankCompositionFile> paletteFiles(AssetWorkspaceAssetDetails details) {
        final Map<String, AssetWorkspaceBankCompositionFile> ordered = new LinkedHashMap<>();
        for (PaletteRecord record : allPaletteRecords()) {
            final AssetWorkspaceBankCompositionFile file = toPaletteFile(record);
            ordered.putIfAbsent(file.path(), file);
        }
        return List.copyOf(ordered.values());
    }

    private List<PaletteRecord> allPaletteRecords() {
        final AssetStudioGlyphMaterialMetadata metadata = materialMetadata();
        final List<PaletteRecord> records = new ArrayList<>();
        for (AssetStudioGlyphMaterialSchema schema : metadata.schemas()) {
            final AssetStudioGlyphMaterialSchemaStatus effectiveSchemaStatus = AssetDetailsColorSchemaCoordinator.effectiveStatus(
                    schema,
                    details.bankComposition().selectedFiles());
            final List<String> originalColors = schema.materialIndices().entrySet().stream()
                    .sorted(Map.Entry.comparingByKey())
                    .map(Map.Entry::getValue)
                    .toList();
            records.add(new PaletteRecord(
                    "schema-" + schema.schemaId() + "-original",
                    null,
                    schema.schemaId(),
                    schema.version(),
                    originalColors,
                    schema.materialIndices(),
                    schemaArtifactIds(schema),
                    true,
                    effectiveSchemaStatus == AssetStudioGlyphMaterialSchemaStatus.OK,
                    effectiveSchemaStatus == AssetStudioGlyphMaterialSchemaStatus.OK
                            ? null
                            : "Schema is inconsistent."));
        }
        for (AssetStudioGlyphMaterialPalette palette : metadata.palettes()) {
            final AssetStudioGlyphMaterialSchema schema = metadata.findSchema(palette.schemaId()).orElse(null);
            final AssetStudioGlyphMaterialSchemaStatus effectiveSchemaStatus = schema == null
                    ? AssetStudioGlyphMaterialSchemaStatus.INCONSISTENT
                    : AssetDetailsColorSchemaCoordinator.effectiveStatus(schema, details.bankComposition().selectedFiles());
            final boolean valid = schema != null
                    && effectiveSchemaStatus == AssetStudioGlyphMaterialSchemaStatus.OK
                    && palette.version() >= schema.version();
            final String invalidReason;
            if (schema == null) {
                invalidReason = "Schema is missing.";
            } else if (effectiveSchemaStatus != AssetStudioGlyphMaterialSchemaStatus.OK) {
                invalidReason = "Schema is inconsistent.";
            } else if (palette.version() < schema.version()) {
                invalidReason = "Palette is stale and must be updated.";
            } else {
                invalidReason = null;
            }
            records.add(new PaletteRecord(
                    palette.paletteId(),
                    palette.displayName(),
                    palette.schemaId(),
                    palette.version(),
                    palette.colors(),
                    schema == null ? Map.of() : schema.materialIndices(),
                    schema == null ? Set.of() : schemaArtifactIds(schema),
                    false,
                    valid,
                    invalidReason));
        }
        records.sort(Comparator.comparing(PaletteRecord::paletteId, String.CASE_INSENSITIVE_ORDER));
        return List.copyOf(records);
    }

    private AssetWorkspaceBankCompositionFile toPaletteFile(PaletteRecord record) {
        final List<Integer> argb = record.colors().stream()
                .map(AssetDetailsPaletteOverhaulingCoordinator::htmlToArgb8888)
                .toList();
        final List<Integer> rgb565 = record.colors().stream()
                .map(AssetDetailsPaletteOverhaulingCoordinator::htmlToRgb565)
                .toList();
        final Map<String, Object> palette = Map.of(
                "originalArgb8888", argb,
                "convertedRgb565", rgb565);
        final Map<String, Object> metadata = new LinkedHashMap<>();
        metadata.put("palette", palette);
        metadata.put("paletteId", record.paletteId());
        metadata.put("paletteDisplayName", displayName(record));
        if (record.displayName() != null) {
            metadata.put("explicitPaletteDisplayName", record.displayName());
        }
        metadata.put("schemaId", record.schemaId());
        metadata.put("schemaMaterialIndices", record.materialIndices());
        metadata.put("schemaArtifactIds", List.copyOf(record.artifactIds()));
        metadata.put("paletteVersion", record.version());
        metadata.put("derivedOriginal", record.derivedOriginal());
        metadata.put("valid", record.valid());
        if (record.invalidReason() != null) {
            metadata.put("invalidReason", record.invalidReason());
        }
        return new AssetWorkspaceBankCompositionFile(
                record.paletteId(),
                displayName(record),
                0L,
                0L,
                null,
                metadata);
    }

    static String generatedDisplayName(List<String> colors) {
        final List<ColorSample> samples = colors.stream()
                .map(AssetDetailsPaletteOverhaulingCoordinator::toColorSample)
                .toList();
        if (samples.isEmpty()) {
            return "quiet shade";
        }
        final ColorSample anchor = samples.stream()
                .min(Comparator.comparingDouble(sample -> sample.saturation < 0.16 ? sample.lightness : -sample.saturation))
                .orElse(samples.getFirst());
        final double averageLightness = samples.stream()
                .mapToDouble(sample -> sample.lightness)
                .average()
                .orElse(anchor.lightness);
        final double averageSaturation = samples.stream()
                .mapToDouble(sample -> sample.saturation)
                .average()
                .orElse(anchor.saturation);
        final double lightnessRange = samples.stream().mapToDouble(sample -> sample.lightness).max().orElse(anchor.lightness)
                - samples.stream().mapToDouble(sample -> sample.lightness).min().orElse(anchor.lightness);
        return baseName(anchor, averageLightness, averageSaturation) + " " + moodName(averageLightness, averageSaturation, lightnessRange, samples.size());
    }

    private static String baseName(ColorSample sample, double averageLightness, double averageSaturation) {
        if (averageSaturation < 0.12) {
            if (averageLightness > 0.88) {
                return "snow";
            }
            if (averageLightness > 0.68) {
                return "fog";
            }
            if (averageLightness < 0.24) {
                return "ink";
            }
            return "ash";
        }
        final double hue = sample.hue;
        if (hue < 18 || hue >= 345) {
            return sample.lightness > 0.62 ? "rose" : "ember";
        }
        if (hue < 42) {
            return sample.lightness > 0.62 ? "honey" : "copper";
        }
        if (hue < 72) {
            return sample.lightness > 0.68 ? "sun" : "ochre";
        }
        if (hue < 155) {
            return sample.lightness > 0.58 ? "moss" : "pine";
        }
        if (hue < 195) {
            return sample.lightness > 0.62 ? "mint" : "jade";
        }
        if (hue < 245) {
            return sample.lightness > 0.62 ? "sky" : "cobalt";
        }
        if (hue < 290) {
            return sample.lightness > 0.62 ? "lilac" : "violet";
        }
        return sample.lightness > 0.62 ? "orchid" : "plum";
    }

    private static String moodName(double averageLightness, double averageSaturation, double lightnessRange, int colorCount) {
        if (averageSaturation < 0.12 && averageLightness > 0.86) {
            return "white";
        }
        if (colorCount > 1 && lightnessRange > 0.42) {
            return "twist";
        }
        if (averageLightness > 0.78) {
            return averageSaturation > 0.45 ? "glow" : "veil";
        }
        if (averageLightness < 0.26) {
            return averageSaturation > 0.34 ? "deep" : "shade";
        }
        if (averageSaturation > 0.62) {
            return "bloom";
        }
        if (averageSaturation < 0.22) {
            return "haze";
        }
        return "drift";
    }

    private static ColorSample toColorSample(String htmlColor) {
        final int rgb = Integer.parseInt(normalizeHtmlColor(htmlColor).substring(1), 16);
        final double red = ((rgb >> 16) & 0xFF) / 255.0;
        final double green = ((rgb >> 8) & 0xFF) / 255.0;
        final double blue = (rgb & 0xFF) / 255.0;
        final double max = Math.max(red, Math.max(green, blue));
        final double min = Math.min(red, Math.min(green, blue));
        final double delta = max - min;
        final double lightness = (max + min) / 2.0;
        final double saturation = delta == 0.0
                ? 0.0
                : delta / (1.0 - Math.abs((2.0 * lightness) - 1.0));
        final double hue;
        if (delta == 0.0) {
            hue = 0.0;
        } else if (max == red) {
            hue = 60.0 * (((green - blue) / delta) % 6.0);
        } else if (max == green) {
            hue = 60.0 * (((blue - red) / delta) + 2.0);
        } else {
            hue = 60.0 * (((red - green) / delta) + 4.0);
        }
        return new ColorSample(hue < 0.0 ? hue + 360.0 : hue, saturation, lightness);
    }

    private static String displayName(PaletteRecord record) {
        if (record.derivedOriginal()) {
            return record.schemaId() + " / original";
        }
        return record.displayName() == null || record.displayName().isBlank()
                ? generatedDisplayName(record.colors())
                : record.displayName();
    }

    private List<AssetWorkspaceBankCompositionFile> initialSelectedPaletteFiles(
            AssetWorkspaceAssetDetails details,
            List<AssetWorkspaceBankCompositionFile> allFiles) {
        final List<AssetWorkspaceBankCompositionFile> selected = new ArrayList<>();
        for (Map<String, Object> palette : details.pipelinePalettes()) {
            for (AssetWorkspaceBankCompositionFile file : allFiles) {
                if (!selected.contains(file) && paletteEquals(palette, nestedMap(file.metadata(), "palette"))) {
                    selected.add(file);
                    break;
                }
            }
        }
        return List.copyOf(selected);
    }

    private static boolean paletteEquals(Map<String, Object> left, Map<String, Object> right) {
        if (left == null || right == null) {
            return false;
        }
        return Objects.equals(left.get("originalArgb8888"), right.get("originalArgb8888"))
                && Objects.equals(left.get("convertedRgb565"), right.get("convertedRgb565"));
    }

    private static int htmlToArgb8888(String htmlColor) {
        final int rgb = Integer.parseInt(normalizeHtmlColor(htmlColor).substring(1), 16);
        return 0xFF000000 | rgb;
    }

    private static int htmlToRgb565(String htmlColor) {
        final int rgb = Integer.parseInt(normalizeHtmlColor(htmlColor).substring(1), 16);
        final int red = (rgb >> 16) & 0xFF;
        final int green = (rgb >> 8) & 0xFF;
        final int blue = rgb & 0xFF;
        return ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3);
    }

    private static String normalizeHtmlColor(String color) {
        return Objects.requireNonNull(color, "color").trim().toUpperCase();
    }

    private static AssetWorkspaceBankCompositionFile fileByPath(List<AssetWorkspaceBankCompositionFile> files, String path) {
        if (path == null) {
            return null;
        }
        return files.stream()
                .filter(file -> path.equals(file.path()))
                .findFirst()
                .orElse(null);
    }

    private static AssetDetailsPaletteOverhaulingDraft rebindDraft(
            AssetDetailsPaletteOverhaulingDraft draft,
            AssetDetailsPaletteOverhaulingDraft source) {
        final Set<String> selectedPaths = draft.selectedFiles().stream()
                .map(AssetWorkspaceBankCompositionFile::path)
                .collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
        final List<AssetWorkspaceBankCompositionFile> selected = source.allFiles().stream()
                .filter(file -> selectedPaths.contains(file.path()))
                .toList();
        final List<AssetWorkspaceBankCompositionFile> available = source.allFiles().stream()
                .filter(file -> !selectedPaths.contains(file.path()))
                .toList();
        final String previewPalettePath = fileByPath(source.allFiles(), draft.previewPalettePath()) == null
                ? source.previewPalettePath()
                : draft.previewPalettePath();
        final String previewTilePath = fileByPath(source.previewTileFiles(), draft.previewTilePath()) == null
                ? source.previewTilePath()
                : draft.previewTilePath();
        return new AssetDetailsPaletteOverhaulingDraft(
                source.previewTileFiles(),
                source.allFiles(),
                available,
                selected,
                previewTilePath,
                previewPalettePath);
    }

    private static Set<String> schemaArtifactIds(AssetStudioGlyphMaterialSchema schema) {
        return schema.artifactBindings().stream()
                .map(AssetStudioGlyphMaterialArtifactBinding::artifactId)
                .collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
    }

    private static List<AssetWorkspaceBankCompositionFile> previewTileFilesForPalette(
            List<AssetWorkspaceBankCompositionFile> previewTileFiles,
            AssetWorkspaceBankCompositionFile previewPaletteFile) {
        if (previewPaletteFile == null) {
            return previewTileFiles;
        }
        final Set<String> schemaArtifactIds = schemaArtifactIds(previewPaletteFile);
        if (schemaArtifactIds.isEmpty()) {
            return previewTileFiles;
        }
        return previewTileFiles.stream()
                .filter(file -> schemaArtifactIds.contains(artifactId(file)))
                .toList();
    }

    private static Set<String> schemaArtifactIds(AssetWorkspaceBankCompositionFile previewPaletteFile) {
        final Object artifactIds = previewPaletteFile.metadata().get("schemaArtifactIds");
        if (!(artifactIds instanceof Iterable<?> iterable)) {
            return Set.of();
        }
        final Set<String> ids = new LinkedHashSet<>();
        for (Object artifactId : iterable) {
            if (artifactId instanceof String value && !value.isBlank()) {
                ids.add(value.trim());
            }
        }
        return Set.copyOf(ids);
    }

    private static String artifactId(AssetWorkspaceBankCompositionFile file) {
        if (file == null) {
            return null;
        }
        final Object artifactId = file.metadata().get("artifactId");
        if (artifactId instanceof String value && !value.isBlank()) {
            return value.trim();
        }
        return null;
    }

    private record PaletteRecord(
            String paletteId,
            String displayName,
            String schemaId,
            int version,
            List<String> colors,
            Map<Integer, String> materialIndices,
            Set<String> artifactIds,
            boolean derivedOriginal,
            boolean valid,
            String invalidReason) {
    }

    private record ColorSample(double hue, double saturation, double lightness) {
    }
}
