package com.livingworld.data.migration;

import com.livingworld.debug.DiagnosticCategory;
import com.livingworld.debug.LivingWorldLogger;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;

/**
 * Detects the schema version of region save data and applies registered
 * {@link RegionMigration} steps in order until the data reaches the target version.
 *
 * <p>Usage:
 * <ol>
 *   <li>Construct with the path to the migrations log file (or {@code null} to disable logging).
 *   <li>Register one {@link RegionMigration} per version gap via {@link #register}.
 *   <li>Call {@link #migrateIfNeeded} before decoding saved region properties.
 * </ol>
 */
public final class MigrationManager {

    private final Map<Integer, RegionMigration> migrations = new TreeMap<>();
    private final Path logFile;

    /**
     * @param logFile path to the append-only migrations log; {@code null} to disable file logging
     */
    public MigrationManager(Path logFile) {
        this.logFile = logFile;
    }

    /**
     * Registers a migration step.
     *
     * @throws IllegalArgumentException if the migration is null, if its version span is not
     *                                  exactly one, or if a migration for the same
     *                                  {@code fromVersion} is already registered
     */
    public void register(RegionMigration migration) {
        if (migration == null) {
            throw new IllegalArgumentException("migration must not be null");
        }
        int from = migration.fromVersion();
        int to = migration.toVersion();
        if (to != from + 1) {
            throw new IllegalArgumentException(
                    "Migration must advance exactly one version; got " + from + " → " + to);
        }
        if (migrations.containsKey(from)) {
            throw new IllegalArgumentException(
                    "Migration already registered for fromVersion " + from);
        }
        migrations.put(from, migration);
    }

    /**
     * Returns the number of registered migration steps.
     */
    public int getRegisteredMigrationCount() {
        return migrations.size();
    }

    /**
     * Returns {@code true} when the data's {@code schemaVersion} already equals
     * {@code targetVersion} and no migration is needed.
     */
    public boolean isUpToDate(Properties data, int targetVersion) {
        return readVersion(data) == targetVersion;
    }

    /**
     * Migrates the given save data to {@code targetVersion} if it is behind, and returns
     * the (possibly modified) properties. Returns the original object unchanged when already
     * at the target version.
     *
     * @throws IllegalStateException if the data is ahead of {@code targetVersion}, if
     *                               {@code schemaVersion} is missing or invalid, or if the
     *                               registered migrations do not cover the required range
     */
    public Properties migrateIfNeeded(Properties data, int targetVersion) {
        int currentVersion = readVersion(data);
        if (currentVersion == targetVersion) {
            return data;
        }
        if (currentVersion > targetVersion) {
            throw new IllegalStateException(
                    "Cannot downgrade save data from schema version "
                            + currentVersion + " to " + targetVersion);
        }

        List<RegionMigration> path = buildMigrationPath(currentVersion, targetVersion);

        Properties result = copyProperties(data);
        for (RegionMigration step : path) {
            result = step.apply(result);
            result.setProperty("schemaVersion", Integer.toString(step.toVersion()));
            recordStep(step.fromVersion(), step.toVersion());
        }
        return result;
    }

    // -------------------------------------------------------------------------
    // private helpers
    // -------------------------------------------------------------------------

    private List<RegionMigration> buildMigrationPath(int from, int to) {
        List<RegionMigration> path = new ArrayList<>();
        int version = from;
        while (version < to) {
            RegionMigration step = migrations.get(version);
            if (step == null) {
                throw new IllegalStateException(
                        "No migration registered for schema version " + version
                                + " (target version: " + to + ")");
            }
            path.add(step);
            version++;
        }
        return path;
    }

    private static int readVersion(Properties data) {
        if (data == null) {
            throw new IllegalArgumentException("data must not be null");
        }
        String raw = data.getProperty("schemaVersion");
        if (raw == null || raw.isBlank()) {
            throw new IllegalStateException("Save data is missing required key: schemaVersion");
        }
        int version;
        try {
            version = Integer.parseInt(raw.trim());
        } catch (NumberFormatException e) {
            throw new IllegalStateException("Invalid schemaVersion value: " + raw, e);
        }
        if (version <= 0) {
            throw new IllegalStateException("schemaVersion must be > 0, got: " + version);
        }
        return version;
    }

    private static Properties copyProperties(Properties source) {
        Properties copy = new Properties();
        for (String key : source.stringPropertyNames()) {
            copy.setProperty(key, source.getProperty(key));
        }
        return copy;
    }

    private void recordStep(int from, int to) {
        LivingWorldLogger.info(
                DiagnosticCategory.PERSISTENCE,
                "Applied region schema migration: " + from + " → " + to);
        if (logFile == null) {
            return;
        }
        String entry = Instant.now() + " Migrated region schema " + from + " → " + to
                + System.lineSeparator();
        try {
            if (logFile.getParent() != null) {
                Files.createDirectories(logFile.getParent());
            }
            Files.writeString(
                    logFile,
                    entry,
                    StandardCharsets.UTF_8,
                    StandardOpenOption.CREATE,
                    StandardOpenOption.APPEND);
        } catch (IOException e) {
            LivingWorldLogger.warn(
                    DiagnosticCategory.PERSISTENCE,
                    "Failed to write to migrations log: " + e.getMessage());
        }
    }
}
