package p.packer.repositories;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import p.packer.exceptions.PackerRegistryException;
import p.packer.messages.PackerProjectContext;
import p.packer.models.PackerRegistryEntry;
import p.packer.models.PackerRegistryState;
import p.packer.PackerWorkspacePaths;

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

public final class FileSystemPackerRegistryRepository implements PackerRegistryRepository {
    private static final int REGISTRY_SCHEMA_VERSION = 1;

    private final ObjectMapper mapper;

    public FileSystemPackerRegistryRepository(final ObjectMapper mapper) {
        this.mapper = Objects.requireNonNull(mapper, "mapper");
    }

    @Override
    public PackerRegistryState load(PackerProjectContext project) {
        final Path registryPath = PackerWorkspacePaths.registryPath(project);
        if (!Files.isRegularFile(registryPath)) {
            return emptyState();
        }

        try {
            final RegistryDocument document = mapper.readValue(registryPath.toFile(), RegistryDocument.class);
            final int schemaVersion = document.schemaVersion <= 0 ? REGISTRY_SCHEMA_VERSION : document.schemaVersion;
            if (schemaVersion != REGISTRY_SCHEMA_VERSION) {
                throw new PackerRegistryException("Unsupported registry schema_version: " + schemaVersion);
            }
            final List<PackerRegistryEntry> entries = new ArrayList<>();
            if (document.assets != null) {
                for (RegistryAssetDocument asset : document.assets) {
                    if (asset == null) {
                        continue;
                    }
                    entries.add(new PackerRegistryEntry(
                            asset.assetId,
                            asset.assetUuid,
                            normalizeRoot(asset.root),
                            asset.includedInBuild == null || asset.includedInBuild));
                }
            }
            validateEntries(entries);
            final int nextAssetId = document.nextAssetId > 0
                    ? document.nextAssetId
                    : entries.stream().mapToInt(PackerRegistryEntry::assetId).max().orElse(0) + 1;
            return new PackerRegistryState(
                    schemaVersion,
                    nextAssetId,
                    entries.stream().sorted(Comparator.comparingInt(PackerRegistryEntry::assetId)).toList());
        } catch (IOException exception) {
            throw new p.packer.exceptions.PackerRegistryException("Unable to load registry: " + registryPath, exception);
        }
    }

    @Override
    public void save(PackerProjectContext project, PackerRegistryState state) {
        final Path registryDirectory = PackerWorkspacePaths.registryDirectory(project);
        final Path registryPath = PackerWorkspacePaths.registryPath(project);
        validateEntries(state.assets());
        try {
            Files.createDirectories(registryDirectory);
            final RegistryDocument document = new RegistryDocument();
            document.schemaVersion = state.schemaVersion();
            document.nextAssetId = state.nextAssetId();
            document.assets = state.assets().stream()
                    .map(entry -> new RegistryAssetDocument(
                            entry.assetId(),
                            entry.assetUuid(),
                            entry.root(),
                            entry.includedInBuild()))
                    .toList();
            mapper.writerWithDefaultPrettyPrinter().writeValue(registryPath.toFile(), document);
        } catch (IOException exception) {
            throw new p.packer.exceptions.PackerRegistryException("Unable to save registry: " + registryPath, exception);
        }
    }

    private PackerRegistryState emptyState() {
        return new PackerRegistryState(REGISTRY_SCHEMA_VERSION, 1, List.of());
    }

    private void validateEntries(List<PackerRegistryEntry> entries) throws PackerRegistryException {
        final Set<Integer> assetIds = new HashSet<>();
        final Set<String> assetUuids = new HashSet<>();
        final Set<String> roots = new HashSet<>();
        for (PackerRegistryEntry entry : entries) {
            if (!assetIds.add(entry.assetId())) {
                throw new PackerRegistryException("Duplicate asset_id in registry: " + entry.assetId());
            }
            if (!assetUuids.add(entry.assetUuid())) {
                throw new PackerRegistryException("Duplicate asset_uuid in registry: " + entry.assetUuid());
            }
            if (!roots.add(entry.root())) {
                throw new PackerRegistryException("Duplicate asset root in registry: " + entry.root());
            }
        }
    }

    private String normalizeRoot(String root) {
        if (root == null || root.isBlank()) {
            throw new PackerRegistryException("Registry asset root must not be blank");
        }
        final String normalized = root.trim().replace('\\', '/');
        final Path normalizedPath = Path.of(normalized).normalize();
        if (normalizedPath.isAbsolute() || normalizedPath.startsWith("..")) {
            throw new PackerRegistryException("Registry asset root is outside the trusted assets boundary: " + normalized);
        }
        return normalized;
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    private static final class RegistryDocument {
        @JsonProperty("schema_version")
        public int schemaVersion;

        @JsonProperty("next_asset_id")
        public int nextAssetId;

        @JsonProperty("assets")
        public List<RegistryAssetDocument> assets = List.of();
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    private record RegistryAssetDocument(
            @JsonProperty("asset_id") int assetId,
            @JsonProperty("asset_uuid") String assetUuid,
            @JsonProperty("root") String root,
            @JsonProperty("included_in_build") Boolean includedInBuild) {
    }
}
