package dev.photosync.client;

import dev.photosync.core.PhotoSync;
import dev.photosync.core.capture.CapturedScreenshot;
import dev.photosync.core.config.ConfigLocations;
import dev.photosync.core.config.NotificationKind;
import dev.photosync.core.config.PhotoSyncConfig;
import dev.photosync.core.upload.UploadEvent;
import dev.photosync.mcapi.ClientBridge;
import dev.photosync.mcapi.GameContext;
import dev.photosync.mcapi.capture.ScreenshotBus;
import dev.photosync.mcapi.lifecycle.QuitGuard;
import dev.photosync.mcapi.render.RenderBridge;
import dev.photosync.ui.Chrome;
import dev.photosync.ui.PhotoSyncUi;
import dev.photosync.ui.Theme;
import lombok.Getter;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;

import java.time.Duration;

/**
 * The mod, as one object.
 *
 * <p>A platform module builds this with its {@link ClientBridge} and then only
 * ever calls the five methods below: {@link #start()}, {@link #tick()},
 * {@link #renderHud}, {@link #openScreen()} and {@link #close()}. Everything a
 * new Minecraft version can break is on the other side of that bridge.
 *
 * <p>What lives here is the wiring nobody else can own, because it crosses the
 * modules: a screenshot becomes an upload, an upload becomes a message in the
 * corner, and a quit becomes a question.
 */
@Slf4j
@Accessors(fluent = true)
public final class PhotoSyncClient implements AutoCloseable {

    /**
     * How long the game is allowed to hang on shutdown after the player has said
     * they are done waiting. The queue is durable, so this is a courtesy for
     * uploads that are seconds from finishing, not a guarantee.
     */
    private static final Duration SHUTDOWN_GRACE = Duration.ofSeconds(3);

    @Getter
    private final PhotoSync core;
    @Getter
    private final ClientBridge bridge;
    @Getter
    private final PhotoSyncUi ui;

    private final AutoCapture autoCapture;

    public PhotoSyncClient(ClientBridge bridge) {
        this(bridge, new PhotoSync(new ConfigLocations(
                bridge.game().configDirectory(),
                bridge.game().userDataDirectory())), Theme.dark());
    }

    /** Takes its collaborators explicitly so a test can drive it without a game. */
    public PhotoSyncClient(ClientBridge bridge, PhotoSync core, Theme theme) {
        this.bridge = bridge;
        this.core = core;
        this.ui = new PhotoSyncUi(core, bridge, theme);
        this.autoCapture = new AutoCapture(
                bridge.game(),
                bridge.screenshots(),
                () -> core.config().current().autoCapture(),
                this::captured);
    }

    /** Connects everything and starts draining whatever last session left behind. */
    public PhotoSyncClient start() {
        core.start();
        // Manual screenshots arrive here from the platform's Screenshot mixin;
        // automatic ones come from AutoCapture, which already knows their origin.
        ScreenshotBus.get().subscribe(this::captured);
        core.queue().onEvent(this::uploadChanged);
        QuitGuard.get().handler(this::mayQuit);
        log.info("PhotoSync ready on Minecraft {}", bridge.game().minecraftVersion());
        return this;
    }

    public void tick() {
        autoCapture.tick();
    }

    /**
     * Draws the corner messages. Called every frame the in-world HUD is drawn,
     * so not on the title screen and not on top of an open screen.
     */
    public void renderHud(RenderBridge render) {
        ui.notifications().render(render, System.currentTimeMillis());
    }

    /** The key binding's action. */
    public void openScreen() {
        ui.open();
    }

    // -----------------------------------------------------------------------
    // Screenshots in, notifications out
    // -----------------------------------------------------------------------

    /** Called from an IO thread for manual shots and a worker for automatic ones. */
    private void captured(CapturedScreenshot shot) {
        PhotoSyncConfig config = core.config().current();
        ui.notifications().show(NotificationKind.CAPTURED,
                chrome().translate("photosync.notify.captured", shot.fileName()));

        if (!config.upload().uploadOnCapture()) {
            return;
        }
        if (!config.isReady()) {
            // Queuing against a server we have no credentials for would fill the
            // queue screen with failures the player cannot act on from there.
            log.debug("Not queuing {}: no provider configured", shot.fileName());
            return;
        }
        core.queue().enqueue(shot, config.album(), captureDescription());
    }

    /**
     * One line for the upload's description: the window title -- which a
     * modpack replaces with its own name -- and the server being played on, or
     * the word for single player.
     *
     * <p>Snapshot at capture time, because the queue outlives the session: a
     * screenshot retried tomorrow should describe the moment it was taken, not
     * whatever server the player is on when the retry fires.
     */
    private String captureDescription() {
        try {
            GameContext game = bridge.game();
            String where = game.serverAddress()
                    .orElseGet(() -> chrome().translate("photosync.description.singleplayer"));
            return game.clientName() + " · " + where;
        } catch (RuntimeException e) {
            // A description that cannot be captured must not cost the upload.
            log.warn("Could not capture the screenshot context", e);
            return "";
        }
    }

    private void uploadChanged(UploadEvent event) {
        if (event instanceof UploadEvent.Completed completed) {
            ui.notifications().show(NotificationKind.UPLOADED,
                    chrome().translate("photosync.notify.uploaded", completed.job().fileName()));
        } else if (event instanceof UploadEvent.Failed failed && !failed.willRetry()) {
            // Only the final failure is worth a message. The retries are visible
            // on the queue screen for anyone who wants to watch them.
            ui.notifications().show(NotificationKind.FAILED,
                    chrome().translate("photosync.notify.failed", failed.job().fileName()));
        }
    }

    private Chrome chrome() {
        return ui.chrome();
    }

    // -----------------------------------------------------------------------
    // Quitting
    // -----------------------------------------------------------------------

    /**
     * Answers {@code QuitGuard}. Returning false hands the interaction to the
     * quit dialog, which comes back through {@link PhotoSyncUi#quitNow()}.
     */
    private boolean mayQuit() {
        PhotoSyncConfig config = core.config().current();
        if (!config.upload().waitOnQuit() || !core.isBusy()) {
            return true;
        }
        bridge.game().submit(ui::confirmQuit);
        return false;
    }

    @Override
    public void close() {
        // A short drain first: an upload that is nearly done finishes now rather
        // than being re-attempted from the start next launch.
        if (core.isBusy() && !core.drain(SHUTDOWN_GRACE)) {
            log.info("Leaving {} upload(s) queued for the next session", core.queue().activeCount());
        }
        core.close();
    }
}
