package dev.photosync.core;

import dev.photosync.core.config.ConfigLocation;
import dev.photosync.core.config.ConfigLocations;
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.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
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 {@link ConfigLocations} -- where the settings file may live --
 * 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 ConfigLocations locations;
    private volatile ConfigLocation activeLocation;

    @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, stored in the given directory. */
    public PhotoSync(Path directory) {
        this(ConfigLocations.fixed(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(ConfigLocations.fixed(directory), providers);
    }

    /** The standard set of backends, with a switchable storage location. */
    public PhotoSync(ConfigLocations locations) {
        this(locations, List.of(new ImmichProviderFactory()));
    }

    /** Takes the backend list explicitly so tests can run against a fake one. */
    public PhotoSync(ConfigLocations locations, List<ProviderFactory> providers) {
        this.locations = locations;
        this.activeLocation = locations.resolve();
        this.catalog = new ProviderCatalog(providers);
        this.config = new ConfigService(new ConfigStore(
                locations.directory(activeLocation).resolve(CONFIG_FILE), catalog.preferred()));
        this.session = new ProviderSession(catalog);
        // The queue stays in the instance directory even when the settings file
        // moves: its entries reference screenshot paths that are local to the
        // instance, so a shared queue would point at the wrong files.
        this.queue = new UploadQueue(locations.directory(ConfigLocation.INSTANCE).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);
        config.onChange(this::applyLocation);
        applyConnection(config.current());
        reconcileLocation();
        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();
    }

    /** Where the settings file currently lives. */
    public Path dataDirectory() {
        return locations.directory(activeLocation);
    }

    /**
     * Moves the settings file when the player switches storage location.
     *
     * <p>The upload queue is deliberately left in the instance directory; only
     * the settings file follows the choice. The marker is written last, so a
     * failure part-way leaves the old location authoritative for the next
     * launch.
     */
    private void applyLocation(PhotoSyncConfig current) {
        ConfigLocation next = current.location();
        if (next == activeLocation) {
            return;
        }
        try {
            Path from = locations.directory(activeLocation);
            Path to = locations.directory(next);
            if (from.equals(to)) {
                // A catalog with no real user directory (tests): nothing to move.
                return;
            }
            Files.createDirectories(to);
            moveFile(from.resolve(CONFIG_FILE), to.resolve(CONFIG_FILE));
            config.relocate(to.resolve(CONFIG_FILE));
            locations.writeMarker(next);
            activeLocation = next;
            log.info("PhotoSync settings moved to {}", to);
        } catch (IOException e) {
            log.error("Could not move PhotoSync settings to {}; keeping them in {}",
                    locations.directory(next), locations.directory(activeLocation), e);
            config.update(previous -> previous.toBuilder().location(activeLocation).build());
        }
    }

    private static void moveFile(Path source, Path target) throws IOException {
        if (Files.exists(source)) {
            Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
        }
    }

    /**
     * Brings the settings file's own location field in line with the marker.
     * They can only disagree when the file was hand-edited; the marker is the
     * truth about where the file actually is.
     */
    private void reconcileLocation() {
        if (config.current().location() != activeLocation) {
            config.update(previous -> previous.toBuilder().location(activeLocation).build());
        }
    }

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