package p.packer.repositories;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.tika.Tika;
import p.packer.messages.diagnostics.PackerDiagnosticCategory;
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
import p.packer.models.PackerAssetCacheEntry;
import p.packer.models.PackerDiagnostic;
import p.packer.models.PackerFileCacheEntry;
import p.packer.models.PackerFileProbe;
import p.packer.models.PackerProbeResult;
import p.packer.models.PackerWalkResult;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

public abstract class PackerAbstractBankWalker<R> {
    private final Tika tika = new Tika();
    protected final ObjectMapper mapper;

    protected PackerAbstractBankWalker(ObjectMapper mapper) {
        this.mapper = mapper;
    }

    protected abstract Set<String> getSupportedMimeTypes();
    protected abstract PackerProbeResult processFileProbe(PackerFileProbe fileProbe, Path assetRoot, R requirements);

    private Optional<PackerFileProbe> getFileProbeIfSupported(
            final Path filePath,
            final Set<String> supportedMimeTypes) throws IOException {
        final var bytes = Files.readAllBytes(filePath);
        final var mimeType = tika.detect(bytes);
        if (!supportedMimeTypes.contains(mimeType)) {
            return Optional.empty();
        }
        final var lastModified = filePath.toFile().lastModified();
        final var fileProbe = new PackerFileProbe(filePath, mimeType, bytes.length, lastModified, bytes);
        return Optional.of(fileProbe);
    }

    private List<Path> retrieveValidPaths(final Path assetRoot) throws IOException {
        try (final var paths = Files.list(assetRoot)
                .filter(Files::isRegularFile)
                .filter(path -> !isAssetManifest(path))) {
            return paths.toList();
        }
    }

    private boolean isAssetManifest(final Path path) {
        return path.getFileName().toString().equalsIgnoreCase("asset.json");
    }

    private List<PackerProbeResult> processFileProbeWhenSupported(
            final Path assetRoot,
            final Optional<PackerAssetCacheEntry> priorAssetCache,
            final R requirements,
            final Set<String> supportedMimeTypes,
            final List<PackerDiagnostic> diagnostics) {
        final List<PackerProbeResult> probeResults = new ArrayList<>();
        try {
            final var validPaths = retrieveValidPaths(assetRoot);
            for (final var filePath : validPaths) {
                final var fileProbeMaybe = getFileProbeIfSupported(filePath, supportedMimeTypes);
                if (fileProbeMaybe.isEmpty()) {
                    continue;
                }
                final var fileProbe = fileProbeMaybe.get();
                final var cachedEntry = resolvePriorFileCache(assetRoot, fileProbe.path(), priorAssetCache);
                if (cachedEntry.isPresent() && canReuseMetadata(fileProbe, cachedEntry.get())) {
                    probeResults.add(new PackerProbeResult(
                            fileProbe,
                            cachedEntry.get().metadata(),
                            cachedEntry.get().diagnostics()));
                    continue;
                }
                probeResults.add(processFileProbe(fileProbe, assetRoot, requirements));
            }
        } catch (IOException exception) {
            diagnostics.add(new PackerDiagnostic(
                    PackerDiagnosticSeverity.ERROR,
                    PackerDiagnosticCategory.STRUCTURAL,
                    "Unable to walk tile asset root: " + exception.getMessage(),
                    assetRoot,
                    true
            ));
        }
        return probeResults;
    }

    public PackerWalkResult walk(
            final Path assetRoot,
            final R requirements) {
        return walk(assetRoot, requirements, Optional.empty());
    }

    public PackerWalkResult walk(
            final Path assetRoot,
            final R requirements,
            final Optional<PackerAssetCacheEntry> priorAssetCache) {
        final List<PackerDiagnostic> rootDiagnostics = new ArrayList<>();
        final List<PackerProbeResult> probeResults = processFileProbeWhenSupported(
                assetRoot,
                Objects.requireNonNull(priorAssetCache, "priorAssetCache"),
                requirements,
                getSupportedMimeTypes(),
                rootDiagnostics);
        return new PackerWalkResult(probeResults, rootDiagnostics);
    }

    protected boolean canReuseMetadata(
            final PackerFileProbe fileProbe,
            final PackerFileCacheEntry cachedEntry) {
        if (fileProbe.size() != cachedEntry.size()) {
            return false;
        }
        if (fileProbe.lastModified() > cachedEntry.lastModified()) {
            return false;
        }
        if (cachedEntry.mimeType() != null && !cachedEntry.mimeType().equals(fileProbe.mimeType())) {
            return false;
        }
        if (cachedEntry.fingerprint() == null || cachedEntry.fingerprint().isBlank()) {
            return true;
        }
        return cachedEntry.fingerprint().equals(computeFingerprint(fileProbe));
    }

    protected String computeFingerprint(final PackerFileProbe fileProbe) {
        return PackerFileFingerprint.sha256(fileProbe);
    }

    private Optional<PackerFileCacheEntry> resolvePriorFileCache(
            final Path assetRoot,
            final Path filePath,
            final Optional<PackerAssetCacheEntry> priorAssetCache) {
        if (priorAssetCache.isEmpty()) {
            return Optional.empty();
        }
        final String relativePath = assetRoot.toAbsolutePath()
                .normalize()
                .relativize(filePath.toAbsolutePath().normalize())
                .toString()
                .replace('\\', '/');
        return priorAssetCache.get().findFile(relativePath);
    }
}
