package dev.photosync.core.thumbnail;

import dev.photosync.core.provider.PhotoProvider;
import dev.photosync.core.provider.ProviderException;
import dev.photosync.core.provider.ProviderSession;
import dev.photosync.core.provider.ThumbnailSize;
import lombok.extern.slf4j.Slf4j;

import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Fetches thumbnail bytes off the render thread, one request per asset no matter
 * how many tiles ask for it.
 *
 * <p>Decoding those bytes into a texture is deliberately <em>not</em> done here:
 * that has to happen on the render thread against a version-specific image type,
 * so the caller polls the returned future and uploads the result itself.
 *
 * <p>The loader refuses work once too many requests are outstanding. During a
 * fast scroll most tiles are on screen for a few frames and their thumbnails
 * would arrive long after the player has moved past them; dropping those
 * requests and letting the grid ask again when it settles keeps the queue short
 * and the visible tiles first in line.
 */
@Slf4j
public final class ThumbnailLoader implements AutoCloseable {

    /** Roughly two screens' worth of tiles. Past this, the queue is stale by the time it drains. */
    private static final int MAX_PENDING = 96;
    private static final int WORKERS = 3;

    private final ProviderSession session;
    private final ExecutorService workers;
    private final Map<String, CompletableFuture<byte[]>> inFlight = new ConcurrentHashMap<>();

    public ThumbnailLoader(ProviderSession session) {
        this.session = session;
        this.workers = Executors.newFixedThreadPool(WORKERS, daemonFactory());
    }

    /**
     * Asks for one thumbnail.
     *
     * <p>Returns empty when the loader is saturated, which is a "not now" rather
     * than a failure -- the caller is expected to try again on a later frame.
     * Two callers asking for the same asset share one request and one future.
     */
    public Optional<CompletableFuture<byte[]>> request(String assetId, ThumbnailSize size) {
        String key = assetId + '@' + size;
        CompletableFuture<byte[]> existing = inFlight.get(key);
        if (existing != null) {
            return Optional.of(existing);
        }
        if (inFlight.size() >= MAX_PENDING) {
            return Optional.empty();
        }
        CompletableFuture<byte[]> future = new CompletableFuture<>();
        CompletableFuture<byte[]> raced = inFlight.putIfAbsent(key, future);
        if (raced != null) {
            return Optional.of(raced);
        }
        try {
            workers.execute(() -> {
                try {
                    future.complete(fetch(assetId, size));
                } catch (ProviderException e) {
                    log.debug("Thumbnail {} failed: {}", assetId, e.getMessage());
                    future.completeExceptionally(e);
                } catch (RuntimeException e) {
                    future.completeExceptionally(e);
                } finally {
                    inFlight.remove(key);
                }
            });
        } catch (RuntimeException e) {
            // Shutting down: the screen is closing and nobody will read this.
            inFlight.remove(key);
            future.completeExceptionally(e);
        }
        return Optional.of(future);
    }

    public int pending() {
        return inFlight.size();
    }

    private byte[] fetch(String assetId, ThumbnailSize size) throws ProviderException {
        PhotoProvider provider = session.provider().orElseThrow(() -> new ProviderException(
                ProviderException.Kind.AUTHENTICATION, "No provider is configured"));
        return provider.thumbnail(assetId, size);
    }

    @Override
    public void close() {
        workers.shutdownNow();
        inFlight.values().forEach(future -> future.cancel(false));
        inFlight.clear();
        try {
            workers.awaitTermination(2, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    private static ThreadFactory daemonFactory() {
        AtomicInteger counter = new AtomicInteger();
        return runnable -> {
            Thread thread = new Thread(runnable, "photosync-thumbnail-" + counter.incrementAndGet());
            thread.setDaemon(true);
            return thread;
        };
    }
}
