package dev.photosync.core.persistence;

import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.PosixFilePermission;
import java.util.Optional;
import java.util.Set;

/**
 * One JSON document on disk, written so that it cannot be found half-written.
 *
 * <p>PhotoSync keeps two of these -- the settings and the upload queue -- and
 * both have the same requirement: the game can be killed at any moment, and what
 * survives has to be either the old file or the new one. Writing in place would
 * make a hard shutdown during a save look identical to "the mod lost my API key"
 * or "the mod lost my queue", so every write goes to a scratch file and is moved
 * over the target.
 */
@Slf4j
public final class JsonFile {

    private final Path path;
    private final Gson gson;

    public JsonFile(Path path) {
        this.path = path;
        this.gson = Adapters.newGson();
    }

    public Path path() {
        return path;
    }

    /** Configured with PhotoSync's persistence conventions -- see {@link Adapters}. */
    public Gson gson() {
        return gson;
    }

    public boolean exists() {
        return Files.isRegularFile(path);
    }

    /**
     * Parses the file, or returns empty when it is missing, unreadable or not
     * valid JSON. Callers fall back to a default rather than failing start-up:
     * losing settings is annoying, refusing to launch is worse.
     */
    public Optional<JsonElement> readTree() {
        if (!exists()) {
            return Optional.empty();
        }
        try {
            String text = Files.readString(path, StandardCharsets.UTF_8);
            JsonElement parsed = JsonParser.parseString(text);
            return parsed.isJsonNull() ? Optional.empty() : Optional.of(parsed);
        } catch (IOException | RuntimeException e) {
            log.warn("Could not read {}; falling back to defaults", path, e);
            quarantine();
            return Optional.empty();
        }
    }

    public void write(JsonElement tree) throws IOException {
        Path directory = path.toAbsolutePath().getParent();
        Files.createDirectories(directory);

        Path scratch = Files.createTempFile(directory, path.getFileName() + ".", ".tmp");
        try {
            Files.writeString(scratch, gson.toJson(tree), StandardCharsets.UTF_8);
            restrictToOwner(scratch);
            try {
                Files.move(scratch, path, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
            } catch (AtomicMoveNotSupportedException e) {
                // Network shares and some Windows setups refuse atomic moves. A
                // plain replace is still better than writing the target in place.
                Files.move(scratch, path, StandardCopyOption.REPLACE_EXISTING);
            }
        } finally {
            Files.deleteIfExists(scratch);
        }
    }

    /**
     * Keeps a corrupt file around under a {@code .broken} name instead of
     * silently overwriting it on the next save, so a player who lost their
     * settings still has something to send us.
     */
    private void quarantine() {
        Path broken = path.resolveSibling(path.getFileName() + ".broken");
        try {
            Files.move(path, broken, StandardCopyOption.REPLACE_EXISTING);
            log.warn("Moved the unreadable file aside as {}", broken);
        } catch (IOException e) {
            log.debug("Could not move {} aside", path, e);
        }
    }

    /**
     * The settings file holds an API key. Where the filesystem can express it,
     * keep the file to its owner; where it cannot, carry on.
     */
    private static void restrictToOwner(Path file) {
        try {
            if (file.getFileSystem().supportedFileAttributeViews().contains("posix")) {
                Files.setPosixFilePermissions(file,
                        Set.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE));
            }
        } catch (IOException | UnsupportedOperationException e) {
            log.debug("Could not restrict permissions on {}", file, e);
        }
    }
}
