package p.packer.services;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import p.packer.PackerWorkspacePaths;
import p.packer.messages.*;
import p.packer.messages.assets.*;
import p.packer.messages.diagnostics.PackerDiagnosticCategory;
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
import p.packer.models.*;
import p.packer.repositories.PackerRuntimeRegistry;

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

public final class PackerAssetDetailsService {
    private static final ObjectMapper GLYPH_MATERIAL_MAPPER = new ObjectMapper();

    private final PackerRuntimeRegistry runtimeRegistry;
    private final PackerAssetReferenceResolver assetReferenceResolver;

    public PackerAssetDetailsService(
            final PackerRuntimeRegistry runtimeRegistry,
            final PackerAssetReferenceResolver assetReferenceResolver) {
        this.runtimeRegistry = Objects.requireNonNull(runtimeRegistry, "runtimeRegistry");
        this.assetReferenceResolver = Objects.requireNonNull(assetReferenceResolver, "assetReferenceResolver");
    }

    public GetAssetDetailsResult getAssetDetails(final GetAssetDetailsRequest request) {
        final PackerProjectContext project = Objects.requireNonNull(request, "request").project();
        final PackerRuntimeSnapshot snapshot = runtimeRegistry.getOrLoad(project).snapshot();
        final PackerResolvedAssetReference resolved = assetReferenceResolver.resolve(project, snapshot, request.assetReference());
        final List<PackerDiagnostic> diagnostics = new ArrayList<>(resolved.diagnostics());

        if (resolved.runtimeAsset().isEmpty()) {
            diagnostics.add(new PackerDiagnostic(
                    PackerDiagnosticSeverity.ERROR,
                    PackerDiagnosticCategory.STRUCTURAL,
                    "asset.json was not found for the requested asset root.",
                    resolved.assetRoot().resolve("asset.json"),
                    true));
            return failureResult(project, request.assetReference(), resolved, diagnostics);
        }

        final var runtimeAsset = resolved.runtimeAsset().get();
        final var manifestPath = runtimeAsset.manifestPath();
        final var parsed = runtimeAsset.parsedDeclaration();
        diagnostics.addAll(parsed.diagnostics());
        diagnostics.addAll(runtimeAsset.walkDiagnostics());
        appendWalkFileDiagnostics(runtimeAsset, diagnostics);
        diagnostics.addAll(new PackerGlyphMaterialSupport(GLYPH_MATERIAL_MAPPER).validate(runtimeAsset));
        if (!parsed.valid()) {
            return failureResult(project, request.assetReference(), resolved, diagnostics);
        }

        final var declaration = parsed.declaration();
        diagnostics.addAll(identityMismatchDiagnostics(resolved.registryEntry(), declaration, manifestPath));
        final var outputContract = PackerOutputContractCatalog.definitionFor(
                declaration.outputFormat(),
                declaration.outputCodec());
        final var state = resolved.registryEntry().isPresent()
                ? PackerAssetState.REGISTERED
                : PackerAssetState.UNREGISTERED;
        final var summary = new PackerAssetSummary(
                canonicalReference(project, resolved.assetRoot(), resolved.registryEntry()),
                new PackerAssetIdentity(
                        resolved.registryEntry().map(PackerRegistryEntry::assetId).orElse(null),
                        resolved.registryEntry().map(PackerRegistryEntry::assetUuid).orElse(declaration.assetUuid()),
                        declaration.name(),
                        resolved.assetRoot()),
                state,
                resolved.registryEntry()
                        .filter(PackerRegistryEntry::includedInBuild)
                        .map(entry -> PackerBuildParticipation.INCLUDED)
                        .orElse(PackerBuildParticipation.EXCLUDED),
                declaration.assetFamily(),
                declaration.preloadEnabled(),
                !diagnostics.isEmpty());
        final var details = new PackerAssetDetails(
                summary,
                declaration.outputFormat(),
                declaration.outputCodec(),
                outputContract.availableCodecs(),
                outputContract.codecConfigurationFieldsByCodec(),
                metadataFields(outputContract.metadataFields(), declaration.outputMetadata()),
                PackerReadMessageMapper.normalizeMetadata(declaration.outputPipelineMetadata()),
                resolveBankCompositionDetails(snapshot, runtimeAsset, declaration),
                resolvePipelinePalettes(declaration),
                diagnostics);
        return new GetAssetDetailsResult(
                diagnostics.stream().anyMatch(PackerDiagnostic::blocking) ? PackerOperationStatus.PARTIAL : PackerOperationStatus.SUCCESS,
                "Asset details resolved from runtime snapshot.",
                PackerReadMessageMapper.toAssetDetailsDTO(details),
                PackerReadMessageMapper.toDiagnosticDTOs(diagnostics));
    }

    private GetAssetDetailsResult failureResult(
            final PackerProjectContext project,
            final AssetReference requestedReference,
            final PackerResolvedAssetReference resolved,
            final List<PackerDiagnostic> diagnostics) {
        final var state = resolved.registryEntry().isPresent()
                ? PackerAssetState.REGISTERED
                : PackerAssetState.UNREGISTERED;
        final var summary = new PackerAssetSummary(
                canonicalReferenceOrRequested(project, requestedReference, resolved),
                new PackerAssetIdentity(
                        resolved.registryEntry().map(PackerRegistryEntry::assetId).orElse(null),
                        resolved.registryEntry().map(PackerRegistryEntry::assetUuid).orElse(null),
                        resolved.assetRoot().getFileName().toString(),
                        resolved.assetRoot()),
                state,
                resolved.registryEntry()
                        .filter(PackerRegistryEntry::includedInBuild)
                        .map(entry -> PackerBuildParticipation.INCLUDED)
                        .orElse(PackerBuildParticipation.EXCLUDED),
                AssetFamilyCatalog.UNKNOWN,
                false,
                true);
        final var details = new PackerAssetDetails(
                summary,
                OutputFormatCatalog.UNKNOWN,
                OutputCodecCatalog.UNKNOWN,
                List.of(OutputCodecCatalog.NONE),
                Map.of(OutputCodecCatalog.NONE, List.of()),
                List.of(),
                Map.of(),
                PackerBankCompositionDetails.EMPTY,
                List.of(),
                diagnostics);
        return new GetAssetDetailsResult(
                PackerOperationStatus.FAILED,
                "Asset declaration is invalid or unreadable.",
                PackerReadMessageMapper.toAssetDetailsDTO(details),
                PackerReadMessageMapper.toDiagnosticDTOs(diagnostics));
    }

    private PackerBankCompositionDetails resolveBankCompositionDetails(
            final PackerRuntimeSnapshot snapshot,
            final PackerRuntimeAsset runtimeAsset,
            final PackerAssetDeclaration declaration) {
        final var walkProjection = runtimeAsset.walkProjection();
        final Map<String, PackerRuntimeWalkFile> walkFilesByPath = new LinkedHashMap<>();
        walkProjection.buildCandidateFiles().forEach(file -> walkFilesByPath.put(file.relativePath(), file));
        final Optional<PackerAssetCacheEntry> assetCacheEntry = runtimeAsset.registryEntry()
                .flatMap(entry -> snapshot.cacheState().findAsset(entry.assetId()));

        final List<PackerBankCompositionFile> availableFiles = walkProjection.buildCandidateFiles().stream()
                .filter(file -> file.diagnostics().stream().noneMatch(PackerDiagnostic::blocking))
                .map(this::toBankCompositionFile)
                .toList();

        final List<PackerBankCompositionFile> selectedFiles = declaration.artifacts().stream()
                .sorted(Comparator.comparingInt(PackerAssetArtifactSelection::index))
                .map(artifact -> resolveSelectedBankFile(runtimeAsset.assetRoot(), artifact, walkFilesByPath, assetCacheEntry))
                .flatMap(Optional::stream)
                .toList();

        return new PackerBankCompositionDetails(
                availableFiles,
                selectedFiles,
                walkProjection.measuredBankSizeBytes());
    }

    private List<Map<String, Object>> resolvePipelinePalettes(PackerAssetDeclaration declaration) {
        final JsonNode palettesNode = declaration.outputPipelineMetadata().get("palettes");
        if (!(palettesNode instanceof ArrayNode palettesArray)) {
            return List.of();
        }
        final List<Map<String, Object>> palettes = new ArrayList<>();
        for (JsonNode paletteNode : palettesArray) {
            final Map<String, Object> normalized = palettePayloadFromNode(paletteNode);
            if (!normalized.isEmpty()) {
                palettes.add(normalized);
            }
        }
        palettes.sort(Comparator.comparingInt(palette -> ((Number) palette.get("index")).intValue()));
        return List.copyOf(palettes);
    }

    private Map<String, Object> palettePayloadFromNode(JsonNode paletteNode) {
        if (!(paletteNode instanceof ObjectNode paletteObject)) {
            return Map.of();
        }
        final JsonNode indexNode = paletteObject.path("index");
        final JsonNode payloadNode = paletteObject.path("palette");
        if (!indexNode.isInt() || indexNode.intValue() < 0 || !(payloadNode instanceof ObjectNode payloadObject)) {
            return Map.of();
        }
        final JsonNode originalNode = payloadObject.path("originalArgb8888");
        final JsonNode convertedNode = payloadObject.path("convertedRgb565");
        if (!originalNode.isArray() || !convertedNode.isArray()) {
            return Map.of();
        }
        final List<Integer> originalArgb8888 = new ArrayList<>();
        for (JsonNode colorNode : originalNode) {
            if (!colorNode.canConvertToInt()) {
                return Map.of();
            }
            originalArgb8888.add(colorNode.intValue());
        }
        final List<Integer> convertedRgb565 = new ArrayList<>();
        for (JsonNode colorNode : convertedNode) {
            if (!colorNode.canConvertToInt()) {
                return Map.of();
            }
            convertedRgb565.add(colorNode.intValue());
        }
        return Map.of(
                "index", indexNode.intValue(),
                "originalArgb8888", List.copyOf(originalArgb8888),
                "convertedRgb565", List.copyOf(convertedRgb565));
    }

    private Optional<PackerBankCompositionFile> resolveSelectedBankFile(
            Path assetRoot,
            PackerAssetArtifactSelection artifact,
            Map<String, PackerRuntimeWalkFile> walkFilesByPath,
            Optional<PackerAssetCacheEntry> assetCacheEntry) {
        final String relativePath = artifact.file();
        final PackerRuntimeWalkFile runtimeWalkFile = walkFilesByPath.get(relativePath);
        if (runtimeWalkFile != null) {
            return Optional.of(toSelectedBankCompositionFile(runtimeWalkFile, artifact));
        }
        final Optional<PackerFileCacheEntry> cachedFile = assetCacheEntry.flatMap(entry -> entry.findFile(relativePath));
        if (cachedFile.isPresent()) {
            return Optional.of(toSelectedBankCompositionFile(cachedFile.get(), artifact));
        }

        final Path filePath = assetRoot.resolve(relativePath).toAbsolutePath().normalize();
        if (!Files.isRegularFile(filePath)) {
            return Optional.empty();
        }

        try {
            return Optional.of(new PackerBankCompositionFile(
                    relativePath,
                    filePath.getFileName().toString(),
                    Files.size(filePath),
                    Files.getLastModifiedTime(filePath).toMillis(),
                    artifact.fingerprint(),
                    selectedArtifactMetadata(artifact, Map.of())));
        } catch (Exception exception) {
            return Optional.empty();
        }
    }

    private PackerBankCompositionFile toBankCompositionFile(PackerRuntimeWalkFile file) {
        return new PackerBankCompositionFile(
                file.relativePath(),
                Path.of(file.relativePath()).getFileName().toString(),
                file.size(),
                file.lastModified(),
                file.fingerprint(),
                file.metadata());
    }

    private PackerBankCompositionFile toBankCompositionFile(PackerFileCacheEntry file) {
        return new PackerBankCompositionFile(
                file.relativePath(),
                Path.of(file.relativePath()).getFileName().toString(),
                file.size(),
                file.lastModified(),
                file.fingerprint(),
                file.metadata());
    }

    private PackerBankCompositionFile toSelectedBankCompositionFile(
            PackerRuntimeWalkFile file,
            PackerAssetArtifactSelection artifact) {
        return new PackerBankCompositionFile(
                file.relativePath(),
                Path.of(file.relativePath()).getFileName().toString(),
                file.size(),
                file.lastModified(),
                file.fingerprint(),
                selectedArtifactMetadata(artifact, file.metadata()));
    }

    private PackerBankCompositionFile toSelectedBankCompositionFile(
            PackerFileCacheEntry file,
            PackerAssetArtifactSelection artifact) {
        return new PackerBankCompositionFile(
                file.relativePath(),
                Path.of(file.relativePath()).getFileName().toString(),
                file.size(),
                file.lastModified(),
                file.fingerprint(),
                selectedArtifactMetadata(artifact, file.metadata()));
    }

    private Map<String, Object> selectedArtifactMetadata(
            PackerAssetArtifactSelection artifact,
            Map<String, Object> metadata) {
        final Map<String, Object> decorated = new LinkedHashMap<>(metadata);
        if (artifact.artifactId() != null) {
            decorated.put("artifactId", artifact.artifactId());
        }
        if (artifact.fingerprint() != null) {
            decorated.put("selectedFingerprint", artifact.fingerprint());
        }
        return Map.copyOf(decorated);
    }

    private List<PackerCodecConfigurationField> metadataFields(
            List<PackerCodecConfigurationField> definitions,
            Map<String, String> outputMetadata) {
        final Map<String, String> valuesByKey = new LinkedHashMap<>(outputMetadata);
        final List<PackerCodecConfigurationField> fields = new ArrayList<>();

        for (PackerCodecConfigurationField definition : definitions) {
            final String value = valuesByKey.getOrDefault(definition.key(), definition.value());
            fields.add(new PackerCodecConfigurationField(
                    definition.key(),
                    definition.label(),
                    definition.fieldType(),
                    value,
                    definition.required(),
                    definition.options()));
            valuesByKey.remove(definition.key());
        }

        valuesByKey.forEach((key, value) -> fields.add(new PackerCodecConfigurationField(
                key,
                key,
                PackerCodecConfigurationFieldType.TEXT,
                value,
                false,
                List.of())));
        return List.copyOf(fields);
    }

    private List<PackerDiagnostic> identityMismatchDiagnostics(
            final Optional<PackerRegistryEntry> registryEntry,
            final PackerAssetDeclaration declaration,
            final Path manifestPath) {
        if (registryEntry.isEmpty() || registryEntry.get().assetUuid().equals(declaration.assetUuid())) {
            return List.of();
        }
        return List.of(new PackerDiagnostic(
                PackerDiagnosticSeverity.ERROR,
                PackerDiagnosticCategory.STRUCTURAL,
                "Field 'asset_uuid' does not match the registered asset identity.",
                manifestPath,
                true));
    }

    private void appendWalkFileDiagnostics(
            PackerRuntimeAsset runtimeAsset,
            List<PackerDiagnostic> diagnostics) {
        for (PackerRuntimeWalkFile file : runtimeAsset.walkProjection().buildCandidateFiles()) {
            for (PackerDiagnostic diagnostic : file.diagnostics()) {
                if (!diagnostics.contains(diagnostic)) {
                    diagnostics.add(diagnostic);
                }
            }
        }
    }

    private AssetReference canonicalReference(
            final PackerProjectContext project,
            final Path assetRoot,
            final Optional<PackerRegistryEntry> registryEntry) {
        return registryEntry
                .map(packerRegistryEntry -> AssetReference.forAssetId(packerRegistryEntry.assetId()))
                .orElseGet(() -> AssetReference.forRelativeAssetRoot(PackerWorkspacePaths.assetsRoot(project)
                        .relativize(assetRoot.toAbsolutePath().normalize())
                        .toString()
                        .replace('\\', '/')));
    }

    private AssetReference canonicalReferenceOrRequested(
            final PackerProjectContext project,
            final AssetReference requestedReference,
            final PackerResolvedAssetReference resolved) {
        if (resolved.registryEntry().isPresent() || resolved.runtimeAsset().isPresent()) {
            return canonicalReference(project, resolved.assetRoot(), resolved.registryEntry());
        }
        return requestedReference;
    }
}
