using BrassAndSigil.Server.Models;

namespace BrassAndSigil.Server.Services;

/// <summary>
/// Reads and writes Minecraft's <c>server.properties</c>. Editable keys are
/// gated by an allowlist so a compromised panel can't flip security-critical
/// fields like <c>online-mode</c> arbitrarily -- only common gameplay knobs.
/// Preserves comments and key order on write; appends new keys at the end.
/// </summary>
public sealed class ServerPropertiesService
{
    private readonly ServerConfig _config;

    public ServerPropertiesService(ServerConfig config) => _config = config;

    /// <summary>
    /// Keys that may be modified via /api/server/settings. Anything outside this
    /// set is silently dropped from the update payload -- admin must SSH for those.
    /// </summary>
    public static readonly IReadOnlySet<string> EditableKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
    {
        "motd",
        "difficulty",
        "gamemode",
        "view-distance",
        "simulation-distance",
        "max-players",
        "pvp",
        "hardcore",
        "white-list",
        "enforce-whitelist",
        "allow-flight",
        "enable-command-block",
        "spawn-protection",
    };

    public string PropertiesPath => Path.Combine(Path.GetFullPath(_config.ServerDir), "server.properties");

    public Dictionary<string, string> ReadAll()
    {
        var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        if (!File.Exists(PropertiesPath)) return dict;
        foreach (var raw in File.ReadAllLines(PropertiesPath))
        {
            var line = raw.TrimStart();
            if (line.Length == 0 || line[0] == '#' || line[0] == '!') continue;
            var idx = line.IndexOf('=');
            if (idx < 0) continue;
            dict[line.Substring(0, idx).Trim()] = line.Substring(idx + 1);
        }
        return dict;
    }

    /// <summary>Returns just the editable subset, with values left as raw strings.</summary>
    public Dictionary<string, string> ReadEditable()
    {
        var all = ReadAll();
        var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        foreach (var key in EditableKeys)
        {
            if (all.TryGetValue(key, out var v)) result[key] = v;
        }
        return result;
    }

    /// <summary>
    /// Read the current <c>level-seed</c> value, or null if absent / empty.
    /// </summary>
    public string? GetLevelSeed()
    {
        if (!File.Exists(PropertiesPath)) return null;
        foreach (var raw in File.ReadAllLines(PropertiesPath))
        {
            var line = raw.TrimStart();
            if (!line.StartsWith("level-seed=", StringComparison.Ordinal)) continue;
            var v = line.Substring("level-seed=".Length).Trim();
            return string.IsNullOrEmpty(v) ? null : v;
        }
        return null;
    }

    /// <summary>
    /// Direct write of <c>level-seed</c>. Bypasses <see cref="EditableKeys"/>
    /// because the seed is set as part of the wipe flow (with confirmation),
    /// not by general settings UI -- exposing it through the regular Update()
    /// path would let it be flipped from any settings save. Empty string
    /// clears the field, which makes Minecraft pick a random seed on next
    /// world generation.
    /// </summary>
    public void SetLevelSeed(string seed)
    {
        var lines = File.Exists(PropertiesPath)
            ? File.ReadAllLines(PropertiesPath).ToList()
            : new List<string>();
        var done = false;
        for (int i = 0; i < lines.Count; i++)
        {
            var trimmed = lines[i].TrimStart();
            if (trimmed.StartsWith("level-seed=", StringComparison.Ordinal))
            {
                lines[i] = $"level-seed={seed}";
                done = true;
                break;
            }
        }
        if (!done) lines.Add($"level-seed={seed}");
        File.WriteAllLines(PropertiesPath, lines);
    }

    /// <summary>
    /// Apply updates to the file. Keys not in <see cref="EditableKeys"/> are
    /// silently dropped. Lines that already exist are updated in-place to
    /// preserve order and comments; new keys are appended at the end.
    /// </summary>
    public void Update(IDictionary<string, string> updates)
    {
        // Filter to allowed keys only.
        var filtered = updates
            .Where(kv => EditableKeys.Contains(kv.Key))
            .ToDictionary(kv => kv.Key, kv => kv.Value, StringComparer.OrdinalIgnoreCase);
        if (filtered.Count == 0) return;

        var lines = File.Exists(PropertiesPath)
            ? File.ReadAllLines(PropertiesPath).ToList()
            : new List<string>();

        var applied = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        for (int i = 0; i < lines.Count; i++)
        {
            var raw = lines[i];
            var trimmed = raw.TrimStart();
            if (trimmed.Length == 0 || trimmed[0] == '#' || trimmed[0] == '!') continue;
            var idx = trimmed.IndexOf('=');
            if (idx < 0) continue;
            var key = trimmed.Substring(0, idx).Trim();
            if (filtered.TryGetValue(key, out var newValue))
            {
                lines[i] = $"{key}={newValue}";
                applied.Add(key);
            }
        }
        foreach (var (key, value) in filtered)
        {
            if (!applied.Contains(key)) lines.Add($"{key}={value}");
        }
        File.WriteAllLines(PropertiesPath, lines);
    }
}
