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.PackerWorkspacePaths;
import p.packer.exceptions.PackerRegistryException;
import p.packer.messages.PackerProjectContext;
import p.packer.models.PackerAssetCacheEntry;
import p.packer.models.PackerDiagnostic;
import p.packer.models.PackerFileCacheEntry;
import p.packer.models.PackerWorkspaceCacheState;
import p.packer.messages.diagnostics.PackerDiagnosticCategory;
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;

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

public final class FileSystemPackerCacheRepository {
    private final ObjectMapper mapper;

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

    public PackerWorkspaceCacheState load(PackerProjectContext project) {
        final Path cachePath = PackerWorkspacePaths.cachePath(project);
        if (!Files.isRegularFile(cachePath)) {
            return PackerWorkspaceCacheState.EMPTY;
        }

        try {
            final CacheDocument document = mapper.readValue(cachePath.toFile(), CacheDocument.class);
            final int schemaVersion = document.schemaVersion <= 0
                    ? PackerWorkspaceCacheState.CURRENT_SCHEMA_VERSION
                    : document.schemaVersion;
            if (schemaVersion != PackerWorkspaceCacheState.CURRENT_SCHEMA_VERSION) {
                throw new PackerRegistryException("Unsupported cache schema_version: " + schemaVersion);
            }
            final List<PackerAssetCacheEntry> assets = new ArrayList<>();
            if (document.assets != null) {
                for (CacheAssetDocument asset : document.assets) {
                    if (asset == null) {
                        continue;
                    }
                    final List<PackerFileCacheEntry> files = new ArrayList<>();
                    if (asset.files != null) {
                        for (CacheFileDocument file : asset.files) {
                            if (file == null) {
                                continue;
                            }
                            files.add(new PackerFileCacheEntry(
                                    normalizeRelativePath(file.relativePath),
                                    file.mimeType,
                                    file.size,
                                    file.lastModified,
                                    file.fingerprint,
                                    file.metadata == null ? Map.of() : file.metadata,
                                    loadDiagnostics(file.diagnostics)));
                        }
                    }
                    validateAssetId(asset.assetId);
                    validateDuplicateRelativePaths(asset.assetId, files);
                    assets.add(new PackerAssetCacheEntry(asset.assetId, asset.contractFingerprint, files));
                }
            }
            validateDuplicateAssetIds(assets);
            return new PackerWorkspaceCacheState(schemaVersion, assets.stream()
                    .sorted(Comparator.comparingInt(PackerAssetCacheEntry::assetId))
                    .toList());
        } catch (IOException exception) {
            throw new PackerRegistryException("Unable to load cache: " + cachePath, exception);
        }
    }

    public void save(PackerProjectContext project, PackerWorkspaceCacheState state) {
        final Path cachePath = PackerWorkspacePaths.cachePath(project);
        final Path directory = PackerWorkspacePaths.registryDirectory(project);
        final PackerWorkspaceCacheState safeState = Objects.requireNonNull(state, "state");
        validateDuplicateAssetIds(safeState.assets());
        safeState.assets().forEach(asset -> validateDuplicateRelativePaths(asset.assetId(), asset.files()));
        try {
            Files.createDirectories(directory);
            final CacheDocument document = new CacheDocument();
            document.schemaVersion = safeState.schemaVersion();
            document.assets = safeState.assets().stream()
                    .map(asset -> new CacheAssetDocument(
                            asset.assetId(),
                            asset.contractFingerprint(),
                            asset.files().stream()
                                    .map(file -> new CacheFileDocument(
                                            file.relativePath(),
                                            file.mimeType(),
                                            file.size(),
                                            file.lastModified(),
                                            file.fingerprint(),
                                            file.metadata(),
                                            saveDiagnostics(file.diagnostics())))
                                    .toList()))
                    .toList();
            mapper.writerWithDefaultPrettyPrinter().writeValue(cachePath.toFile(), document);
        } catch (IOException exception) {
            throw new PackerRegistryException("Unable to save cache: " + cachePath, exception);
        }
    }

    private List<PackerDiagnostic> loadDiagnostics(List<CacheDiagnosticDocument> diagnostics) {
        if (diagnostics == null || diagnostics.isEmpty()) {
            return List.of();
        }
        final List<PackerDiagnostic> loaded = new ArrayList<>();
        for (CacheDiagnosticDocument diagnostic : diagnostics) {
            if (diagnostic == null || diagnostic.severity == null || diagnostic.category == null || diagnostic.message == null) {
                continue;
            }
            loaded.add(new PackerDiagnostic(
                    diagnostic.severity,
                    diagnostic.category,
                    diagnostic.message,
                    diagnostic.evidencePath == null || diagnostic.evidencePath.isBlank() ? null : Path.of(diagnostic.evidencePath),
                    diagnostic.blocking));
        }
        return List.copyOf(loaded);
    }

    private List<CacheDiagnosticDocument> saveDiagnostics(List<PackerDiagnostic> diagnostics) {
        if (diagnostics == null || diagnostics.isEmpty()) {
            return List.of();
        }
        return diagnostics.stream()
                .map(diagnostic -> new CacheDiagnosticDocument(
                        diagnostic.severity(),
                        diagnostic.category(),
                        diagnostic.message(),
                        diagnostic.evidencePath() == null ? null : diagnostic.evidencePath().toString(),
                        diagnostic.blocking()))
                .toList();
    }

    private void validateDuplicateAssetIds(List<PackerAssetCacheEntry> assets) {
        final Set<Integer> assetIds = new HashSet<>();
        for (PackerAssetCacheEntry asset : assets) {
            validateAssetId(asset.assetId());
            if (!assetIds.add(asset.assetId())) {
                throw new PackerRegistryException("Duplicate asset_id in cache: " + asset.assetId());
            }
        }
    }

    private void validateDuplicateRelativePaths(int assetId, List<PackerFileCacheEntry> files) {
        final Set<String> relativePaths = new HashSet<>();
        for (PackerFileCacheEntry file : files) {
            final String relativePath = normalizeRelativePath(file.relativePath());
            if (!relativePaths.add(relativePath)) {
                throw new PackerRegistryException("Duplicate cached file path for asset_id " + assetId + ": " + relativePath);
            }
        }
    }

    private void validateAssetId(int assetId) {
        if (assetId <= 0) {
            throw new PackerRegistryException("Cache asset_id must be positive");
        }
    }

    private String normalizeRelativePath(String relativePath) {
        final String normalized = Objects.requireNonNull(relativePath, "relativePath").trim().replace('\\', '/');
        if (normalized.isBlank()) {
            throw new PackerRegistryException("Cached relative path must not be blank");
        }
        final Path normalizedPath = Path.of(normalized).normalize();
        if (normalizedPath.isAbsolute() || normalizedPath.startsWith("..")) {
            throw new PackerRegistryException("Cached relative path is outside the trusted asset boundary: " + normalized);
        }
        return normalizedPath.toString().replace('\\', '/');
    }

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

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

    @JsonIgnoreProperties(ignoreUnknown = true)
    private record CacheAssetDocument(
            @JsonProperty("asset_id") int assetId,
            @JsonProperty("contract_fingerprint") String contractFingerprint,
            @JsonProperty("files") List<CacheFileDocument> files) {
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    private record CacheFileDocument(
            @JsonProperty("relative_path") String relativePath,
            @JsonProperty("mime_type") String mimeType,
            @JsonProperty("size") long size,
            @JsonProperty("last_modified") long lastModified,
            @JsonProperty("fingerprint") String fingerprint,
            @JsonProperty("metadata") Map<String, Object> metadata,
            @JsonProperty("diagnostics") List<CacheDiagnosticDocument> diagnostics) {
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    private record CacheDiagnosticDocument(
            @JsonProperty("severity") PackerDiagnosticSeverity severity,
            @JsonProperty("category") PackerDiagnosticCategory category,
            @JsonProperty("message") String message,
            @JsonProperty("evidence_path") String evidencePath,
            @JsonProperty("blocking") boolean blocking) {
    }
}
