package p.packer.services;

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 java.nio.file.Path;
import java.util.*;

public final class PackerAssetDetailsService {
    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());
        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()),
                resolveInputs(resolved.assetRoot(), declaration.inputsByRole()),
                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(),
                diagnostics);
        return new GetAssetDetailsResult(
                PackerOperationStatus.FAILED,
                "Asset declaration is invalid or unreadable.",
                PackerReadMessageMapper.toAssetDetailsDTO(details),
                PackerReadMessageMapper.toDiagnosticDTOs(diagnostics));
    }

    private Map<String, List<Path>> resolveInputs(
            final Path assetRoot,
            final Map<String, List<String>> inputsByRole) {
        final Map<String, List<Path>> resolved = new LinkedHashMap<>();
        inputsByRole.forEach((role, inputs) -> resolved.put(
                role,
                inputs.stream().map(input -> assetRoot.resolve(input).toAbsolutePath().normalize()).toList()));
        return Map.copyOf(resolved);
    }

    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 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;
    }
}