using System.Text.Json;
using LittleSim.Sim;

namespace LittleSim.App;

/// <summary>Сериализуемое состояние одного растения: вид (деф), позиция, возраст, стадия, почва и геном.</summary>
public sealed class PlantSave
{
    /// <summary>Имя дефа вида — по нему при загрузке берётся индекс/стадии/текстуры.</summary>
    public string Species { get; set; } = "";

    /// <summary>Позиция X в мировых координатах.</summary>
    public float X { get; set; }

    /// <summary>Позиция Y в мировых координатах.</summary>
    public float Y { get; set; }

    /// <summary>Накопленный возраст в игровых днях.</summary>
    public float AgeDays { get; set; }

    /// <summary>Текущая стадия роста.</summary>
    public int Stage { get; set; }

    /// <summary>Плодородность клетки.</summary>
    public float CellFertility { get; set; }

    /// <summary>Геном особи (аллели сериализуются как поля структуры).</summary>
    public PlantGenome Genome { get; set; }
}

/// <summary>Сериализуемое состояние одного жителя (sim-компоненты; спрайт пересобирается из дефа по <see cref="DefName"/>).</summary>
public sealed class PawnSave
{
    /// <summary>Имя дефа жителя — по нему при загрузке берётся текстура из атласов.</summary>
    public string DefName { get; set; } = "";

    /// <summary>Позиция X в мировых координатах.</summary>
    public float X { get; set; }

    /// <summary>Позиция Y в мировых координатах.</summary>
    public float Y { get; set; }

    /// <summary>Масштаб трансформа (зависит от размера дефа и клетки).</summary>
    public float Scale { get; set; } = 1f;

    /// <summary>Запас энергии [0, 1].</summary>
    public float Energy { get; set; }

    /// <summary>Текущее действие (значение <see cref="Sim.PawnAction"/>).</summary>
    public int Action { get; set; }

    /// <summary>Секунды до следующего пересмотра решения.</summary>
    public float DecideIn { get; set; }

    /// <summary>Направление блуждания X.</summary>
    public float DirX { get; set; }

    /// <summary>Направление блуждания Y.</summary>
    public float DirY { get; set; }

    /// <summary>Секунды до смены направления.</summary>
    public float ChangeIn { get; set; }
}

/// <summary>
/// Полное состояние мира для сохранения/загрузки: конфиг мира (имя/размер/сид/сглаживание),
/// прошедшее время симуляции и снимок всех жителей. Рельеф не сохраняется — он
/// детерминированно регенерируется из сида при загрузке.
/// </summary>
public sealed class WorldSave
{
    /// <summary>Имя мира (и основа имени файла).</summary>
    public string Name { get; set; } = "";

    /// <summary>Ширина мира в клетках.</summary>
    public int Width { get; set; }

    /// <summary>Высота мира в клетках.</summary>
    public int Height { get; set; }

    /// <summary>Сид генерации.</summary>
    public int Seed { get; set; }

    /// <summary>Число проходов сглаживания рельефа.</summary>
    public int SmoothPasses { get; set; } = 4;

    /// <summary>Население на момент сохранения.</summary>
    public int Population { get; set; }

    /// <summary>Момент сохранения (UTC).</summary>
    public DateTime SavedUtc { get; set; }

    /// <summary>Прошедшее время симуляции в секундах.</summary>
    public double ElapsedSeconds { get; set; }

    /// <summary>Снимок жителей.</summary>
    public List<PawnSave> Pawns { get; set; } = [];

    /// <summary>Снимок всех растений (геномы, позиции, возраст, стадия).</summary>
    public List<PlantSave> Plants { get; set; } = [];

    /// <summary>Конфиг мира для пересоздания сцены.</summary>
    public WorldConfig ToConfig() =>
        new()
        {
            Name = Name,
            Width = Width,
            Height = Height,
            Seed = Seed,
            SmoothPasses = SmoothPasses,
            Population = Population,
        };
}

/// <summary>Список/чтение/запись/удаление файлов сохранений <c>Saves/*.json</c>.</summary>
public sealed class SaveStore
{
    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        WriteIndented = true,
        PropertyNameCaseInsensitive = true,
        IncludeFields = true, // аллели генома (PlantGenome) — публичные поля структуры
    };

    private readonly string _directory;

    /// <summary>Создаёт хранилище поверх каталога (по умолчанию <see cref="GamePaths.SavesDirectory"/>).</summary>
    public SaveStore(string? directory = null) =>
        _directory = directory ?? GamePaths.SavesDirectory;

    /// <summary>Запись сохранения: один файл на имя мира (повторное сохранение перезаписывает).</summary>
    public readonly record struct Entry(string FileName, WorldSave Save);

    /// <summary>Все сохранения, новые сверху. Битые файлы пропускаются.</summary>
    public IReadOnlyList<Entry> List()
    {
        if (!Directory.Exists(_directory))
        {
            return [];
        }

        var entries = new List<Entry>();
        foreach (var file in Directory.EnumerateFiles(_directory, "*.json"))
        {
            try
            {
                var save = JsonSerializer.Deserialize<WorldSave>(
                    File.ReadAllText(file),
                    JsonOptions
                );
                if (save is not null)
                {
                    entries.Add(new Entry(Path.GetFileName(file), save));
                }
            }
            catch (Exception exception) when (exception is IOException or JsonException)
            {
                // Битый файл сохранения не должен ломать список — пропускаем.
            }
        }

        entries.Sort((a, b) => b.Save.SavedUtc.CompareTo(a.Save.SavedUtc));
        return entries;
    }

    /// <summary>Пишет сохранение в файл по имени мира и возвращает имя файла.</summary>
    public string Write(WorldSave save)
    {
        Directory.CreateDirectory(_directory);
        var fileName = Sanitize(save.Name) + ".json";
        File.WriteAllText(
            Path.Combine(_directory, fileName),
            JsonSerializer.Serialize(save, JsonOptions)
        );
        return fileName;
    }

    /// <summary>Удаляет файл сохранения по имени.</summary>
    public void Delete(string fileName)
    {
        var path = Path.Combine(_directory, fileName);
        if (File.Exists(path))
        {
            File.Delete(path);
        }
    }

    private static string Sanitize(string name)
    {
        var trimmed = name.Trim();
        var safe = string.Concat(
            trimmed.Select(c => Path.GetInvalidFileNameChars().Contains(c) ? '_' : c)
        );
        return string.IsNullOrEmpty(safe) ? "world" : safe;
    }
}
