package thb.jeanluc.adventure.save;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import thb.jeanluc.adventure.game.GameSession;
import thb.jeanluc.adventure.loader.WorldLoader;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.stream.Stream;

/**
 * Reads and writes save slots as JSON files in a directory (default
 * {@code ./saves}). One slot = one {@code <slug>.json}. Loading reloads the
 * world from YAML and overlays the snapshot via {@link SaveCodec}.
 */
@Slf4j
public class SaveService {

    private static final String SETTINGS_FILENAME = "settings.json";

    private final Path dir;
    private final ObjectMapper mapper = new ObjectMapper();

    /** Uses the default {@code ./saves} directory. */
    public SaveService() {
        this(Path.of("saves"));
    }

    public SaveService(Path dir) {
        this.dir = dir;
    }

    /** Slug used for the on-disk filename of a slot name. */
    public static String slug(String slotName) {
        String s = slotName.trim().toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9_-]+", "_");
        if (s.isBlank()) {
            return "save";
        }
        // Never let a user-named save collide with the settings file.
        return s.equals("settings") ? "settings_" : s;
    }

    /** Writes the session to {@code <slug>.json} atomically. */
    public void save(GameSession session) {
        SaveData data = SaveCodec.capture(session);
        try {
            Files.createDirectories(dir);
            Path target = dir.resolve(slug(session.getSlotName()) + ".json");
            Path tmp = dir.resolve(slug(session.getSlotName()) + ".json.tmp");
            mapper.writerWithDefaultPrettyPrinter().writeValue(tmp.toFile(), data);
            try {
                Files.move(tmp, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
            } catch (IOException atomicFailed) {
                try {
                    Files.move(tmp, target, StandardCopyOption.REPLACE_EXISTING);
                } catch (IOException fallbackFailed) {
                    Files.deleteIfExists(tmp);
                    throw fallbackFailed;
                }
            }
        } catch (IOException e) {
            throw new SaveException("Could not write save '" + session.getSlotName() + "': " + e.getMessage(), e);
        }
    }

    /** Lists all readable slots, newest first; skips unreadable files. */
    public List<SaveSlotInfo> list() {
        if (!Files.isDirectory(dir)) {
            return List.of();
        }
        List<SaveSlotInfo> out = new ArrayList<>();
        try (Stream<Path> files = Files.list(dir)) {
            for (Path f : files
                    .filter(p -> p.toString().endsWith(".json"))
                    .filter(p -> !p.getFileName().toString().equals(SETTINGS_FILENAME))
                    .toList()) {
                try {
                    SaveData d = mapper.readValue(f.toFile(), SaveData.class);
                    String slug = f.getFileName().toString().replaceFirst("\\.json$", "");
                    out.add(new SaveSlotInfo(slug, d.slotName(), d.currentRoomId(),
                            d.turn(), d.savedAtEpochMillis()));
                } catch (IOException badFile) {
                    log.warn("Skipping unreadable save file {}", f, badFile);
                }
            }
        } catch (IOException e) {
            log.warn("Could not list save directory {}", dir, e);
        }
        out.sort(Comparator.comparingLong(SaveSlotInfo::savedAtEpochMillis).reversed());
        return out;
    }

    /** Loads a slot by slug: read JSON, reload world, overlay snapshot. */
    public GameSession load(String slug) {
        Path file = dir.resolve(slug + ".json");
        SaveData data;
        try {
            data = mapper.readValue(file.toFile(), SaveData.class);
        } catch (IOException e) {
            throw new SaveException("Could not read save '" + slug + "': " + e.getMessage(), e);
        }
        if (data.schemaVersion() != SaveData.CURRENT_VERSION) {
            throw new SaveException("Save '" + slug + "' uses an incompatible version ("
                    + data.schemaVersion() + "); expected " + SaveData.CURRENT_VERSION + ".");
        }
        WorldLoader.LoadResult fresh = new WorldLoader().load();
        GameSession session = new GameSession(fresh.world(), fresh.player(), data.slotName());
        SaveCodec.apply(data, session);
        return session;
    }
}
