package dev.photosync.core.config;

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import dev.photosync.core.persistence.JsonFile;
import dev.photosync.core.provider.ProviderId;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;

/**
 * Reads and writes {@code photosync.json}.
 *
 * <p>Loading merges the stored file <em>onto</em> the serialized defaults rather
 * than deserializing it directly. A setting added in a later release is therefore
 * present with its default in an old config file, and a setting removed in a
 * later release is ignored -- no schema version, no migration step. This is not
 * a nicety: Gson gives every absent record component {@code null} or {@code 0},
 * so without the merge a newly added {@code lingerMillis} would arrive as zero
 * and notifications would vanish for everyone upgrading.
 */
@Slf4j
public final class ConfigStore {

    private JsonFile file;
    private final ProviderId fallbackProvider;

    public ConfigStore(Path path, ProviderId fallbackProvider) {
        this.file = new JsonFile(path);
        this.fallbackProvider = fallbackProvider;
    }

    public Path path() {
        return file.path();
    }

    /** Points the store at a new file, after the settings file has been moved there. */
    public void relocate(Path path) {
        this.file = new JsonFile(path);
    }

    /** Never throws: a broken config falls back to defaults rather than blocking start-up. */
    public PhotoSyncConfig load() {
        PhotoSyncConfig defaults = PhotoSyncConfig.defaults(fallbackProvider);
        Optional<JsonElement> stored = file.readTree();
        if (stored.isEmpty() || !stored.get().isJsonObject()) {
            return defaults;
        }
        try {
            JsonObject merged = file.gson().toJsonTree(defaults).getAsJsonObject();
            overlay(merged, stored.get().getAsJsonObject());
            return file.gson().fromJson(merged, PhotoSyncConfig.class).normalized(fallbackProvider);
        } catch (RuntimeException e) {
            log.warn("{} could not be understood; using defaults", file.path(), e);
            return defaults;
        }
    }

    public void save(PhotoSyncConfig config) throws IOException {
        file.write(file.gson().toJsonTree(config));
    }

    /** Recursively copies {@code stored} over {@code base}, object by object. */
    private static void overlay(JsonObject base, JsonObject stored) {
        for (Map.Entry<String, JsonElement> entry : stored.entrySet()) {
            JsonElement current = base.get(entry.getKey());
            JsonElement incoming = entry.getValue();
            if (current != null && current.isJsonObject() && incoming.isJsonObject()) {
                overlay(current.getAsJsonObject(), incoming.getAsJsonObject());
            } else {
                base.add(entry.getKey(), incoming);
            }
        }
    }
}
