package dev.photosync.core;

import dev.photosync.core.config.ConfigService;
import dev.photosync.core.config.ConfigStore;
import dev.photosync.core.config.PhotoSyncConfig;
import dev.photosync.core.provider.ProviderCatalog;
import dev.photosync.core.provider.ProviderFactory;
import dev.photosync.core.provider.ProviderSession;
import dev.photosync.core.provider.immich.ImmichProviderFactory;
import dev.photosync.core.thumbnail.ThumbnailLoader;
import dev.photosync.core.timeline.TimelineBrowser;
import dev.photosync.core.upload.UploadCoordinator;
import dev.photosync.core.upload.UploadQueue;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

import java.nio.file.Path;
import java.time.Duration;
import java.util.List;

/**
 * Everything the mod does that is not Minecraft, assembled.
 *
 * <p>This is the seam the platform code sees: a Fabric entrypoint builds one of
 * these with a config directory and then only ever talks to the services hanging
 * off it. Nothing below this package knows what a {@code Screen} is, and nothing
 * here reaches back up.
 *
 * <p>The wiring is done in a constructor rather than by a container because
 * there are seven objects and their order is fixed. The one piece of behaviour
 * that lives here is the subscription that rebuilds the provider whenever the
 * player edits their credentials.
 */
@Slf4j
public final class PhotoSync implements AutoCloseable {

    private static final String CONFIG_FILE = "photosync.json";
    private static final String QUEUE_FILE = "uploads.json";

    @Getter
    private final ProviderCatalog catalog;
    @Getter
    private final ConfigService config;
    @Getter
    private final ProviderSession session;
    @Getter
    private final UploadQueue queue;
    @Getter
    private final UploadCoordinator uploads;
    @Getter
    private final TimelineBrowser browser;
    @Getter
    private final ThumbnailLoader thumbnails;

    /** The standard set of backends. */
    public PhotoSync(Path directory) {
        this(directory, List.of(new ImmichProviderFactory()));
    }

    /** Takes the backend list explicitly so tests can run against a fake one. */
    public PhotoSync(Path directory, List<ProviderFactory> providers) {
        this.catalog = new ProviderCatalog(providers);
        this.config = new ConfigService(new ConfigStore(directory.resolve(CONFIG_FILE), catalog.preferred()));
        this.session = new ProviderSession(catalog);
        // Reads and repairs the on-disk queue, so anything interrupted by the
        // last quit is already pending again by the time uploads start.
        this.queue = new UploadQueue(directory.resolve(QUEUE_FILE));
        this.uploads = new UploadCoordinator(queue, session, () -> config.current().upload());
        this.browser = new TimelineBrowser(session);
        this.thumbnails = new ThumbnailLoader(session);
    }

    /** Connects to the configured backend and starts draining the queue. */
    public PhotoSync start() {
        config.onChange(this::applyConnection);
        applyConnection(config.current());
        uploads.start();
        int resumed = queue.activeCount();
        if (resumed > 0) {
            log.info("Resuming {} upload(s) left over from the last session", resumed);
        }
        return this;
    }

    /** True while anything is still queued, uploading or waiting to retry. */
    public boolean isBusy() {
        return queue.hasActiveWork();
    }

    /**
     * Drains the queue, giving up after {@code timeout}.
     *
     * <p>Answers whether it finished, which is what the quit dialog reports back
     * to the player. Whatever is left is already on disk and will be picked up
     * the next time the game starts.
     */
    public boolean drain(Duration timeout) {
        return uploads.shutdown(timeout);
    }

    private void applyConnection(PhotoSyncConfig current) {
        session.configure(current.provider(), current.connection());
        uploads.wake();
    }

    @Override
    public void close() {
        thumbnails.close();
        browser.close();
        uploads.close();
        session.close();
    }
}
