package p.packer.services;

import p.packer.messages.PackerOperationStatus;
import p.packer.messages.PackerProjectContext;
import p.packer.messages.assets.AssetFamilyCatalog;
import p.packer.messages.AssetReference;
import p.packer.messages.assets.OutputCodecCatalog;
import p.packer.messages.assets.PackerAssetState;
import p.packer.messages.assets.PackerBuildParticipation;
import p.packer.messages.diagnostics.PackerDiagnosticCategory;
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
import p.packer.messages.GetAssetDetailsRequest;
import p.packer.messages.GetAssetDetailsResult;
import p.packer.models.PackerAssetDeclaration;
import p.packer.models.PackerAssetDeclarationParseResult;
import p.packer.models.PackerAssetDetails;
import p.packer.models.PackerAssetIdentity;
import p.packer.models.PackerAssetSummary;
import p.packer.models.PackerDiagnostic;
import p.packer.models.PackerRegistryEntry;
import p.packer.models.PackerResolvedAssetReference;
import p.packer.models.PackerRuntimeAsset;
import p.packer.models.PackerRuntimeSnapshot;

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

public final class PackerAssetDetailsService {
    private final PackerRuntimeRegistry runtimeRegistry;
    private final PackerAssetReferenceResolver assetReferenceResolver;

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

    public GetAssetDetailsResult getAssetDetails(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 PackerRuntimeAsset runtimeAsset = resolved.runtimeAsset().get();
        final Path manifestPath = runtimeAsset.manifestPath();
        final PackerAssetDeclarationParseResult parsed = runtimeAsset.parsedDeclaration();
        diagnostics.addAll(parsed.diagnostics());
        if (!parsed.valid()) {
            return failureResult(project, request.assetReference(), resolved, diagnostics);
        }

        final PackerAssetDeclaration declaration = parsed.declaration();
        diagnostics.addAll(identityMismatchDiagnostics(resolved.registryEntry(), declaration, manifestPath));
        final PackerOutputContractCatalog.OutputContractDefinition outputContract = PackerOutputContractCatalog.definitionFor(
                declaration.outputFormat(),
                declaration.outputCodec());
        final PackerAssetState state = resolved.registryEntry().isPresent()
                ? PackerAssetState.REGISTERED
                : PackerAssetState.UNREGISTERED;
        final PackerAssetSummary 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().map(entry -> entry.includedInBuild()
                        ? PackerBuildParticipation.INCLUDED
                        : PackerBuildParticipation.EXCLUDED).orElse(PackerBuildParticipation.EXCLUDED),
                declaration.assetFamily(),
                declaration.preloadEnabled(),
                !diagnostics.isEmpty());
        final PackerAssetDetails details = new PackerAssetDetails(
                summary,
                declaration.outputFormat(),
                declaration.outputCodec(),
                outputContract.availableCodecs(),
                outputContract.codecConfigurationFieldsByCodec(),
                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(
            PackerProjectContext project,
            AssetReference requestedReference,
            PackerResolvedAssetReference resolved,
            List<PackerDiagnostic> diagnostics) {
        final PackerAssetState state = resolved.registryEntry().isPresent()
                ? PackerAssetState.REGISTERED
                : PackerAssetState.UNREGISTERED;
        final PackerAssetSummary 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().map(entry -> entry.includedInBuild()
                        ? PackerBuildParticipation.INCLUDED
                        : PackerBuildParticipation.EXCLUDED).orElse(PackerBuildParticipation.EXCLUDED),
                AssetFamilyCatalog.UNKNOWN,
                false,
                true);
        final PackerAssetDetails details = new PackerAssetDetails(
                summary,
                "unknown",
                OutputCodecCatalog.UNKNOWN,
                List.of(OutputCodecCatalog.NONE),
                Map.of(OutputCodecCatalog.NONE, 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(Path assetRoot, 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<PackerDiagnostic> identityMismatchDiagnostics(
            Optional<PackerRegistryEntry> registryEntry,
            PackerAssetDeclaration declaration,
            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(
            PackerProjectContext project,
            Path assetRoot,
            Optional<PackerRegistryEntry> registryEntry) {
        if (registryEntry.isPresent()) {
            return AssetReference.forAssetId(registryEntry.get().assetId());
        }
        return AssetReference.forRelativeAssetRoot(PackerWorkspacePaths.assetsRoot(project)
                .relativize(assetRoot.toAbsolutePath().normalize())
                .toString()
                .replace('\\', '/'));
    }

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