using System.Text.RegularExpressions;
using BrassAndSigil.Server.Models;

namespace BrassAndSigil.Server.Services;

/// <summary>
/// Destructive world operations -- wipe and replace. Always single-flight, always
/// stops the server first, and offers a rename-as-backup default so an accidental
/// click doesn't lose data permanently.
/// </summary>
public sealed class WorldService
{
    private readonly ServerConfig _config;
    private readonly ServerProcess _proc;
    private readonly BackupService _backup;
    private readonly Broadcaster _broadcast;
    private readonly RconManager _rcon;
    private readonly BlueMapService? _bluemap;
    private readonly Action<string> _log;
    private readonly SemaphoreSlim _lock = new(1, 1);

    public WorldService(ServerConfig config, ServerProcess proc, BackupService backup,
                        Broadcaster broadcast, RconManager rcon, Action<string> log,
                        BlueMapService? bluemap = null)
    {
        _config = config;
        _proc = proc;
        _backup = backup;
        _broadcast = broadcast;
        _rcon = rcon;
        _bluemap = bluemap;
        _log = log;
    }

    public sealed record WipeResult(bool Ok, string? BackupName, string? SeedUsed, string? Error);

    /// <summary>
    /// What seed strategy a wipe should use:
    /// <list type="bullet">
    /// <item><c>Keep</c> -- capture the live seed via RCON before wipe and reuse it.</item>
    /// <item><c>Random</c> -- clear <c>level-seed</c> so MC picks a fresh random one.</item>
    /// <item><c>Custom</c> -- set <c>level-seed</c> to <see cref="WipeOptions.CustomSeed"/>.</item>
    /// </list>
    /// </summary>
    public enum SeedMode { Keep, Random, Custom }

    public sealed record WipeOptions(bool Backup, SeedMode Mode, string? CustomSeed);

    /// <summary>
    /// Best-effort lookup of the current world seed. Prefers RCON's <c>seed</c>
    /// command (always returns the actual generated seed even when
    /// server.properties has level-seed empty); falls back to the configured
    /// level-seed value if RCON is unavailable (server stopped, no MC, etc.).
    /// Returns null when neither is available.
    /// </summary>
    public async Task<string?> GetCurrentSeedAsync(CancellationToken ct = default)
    {
        if (_proc.IsRunning)
        {
            try
            {
                var resp = await _rcon.SendCommandAsync("seed", ct);
                // Format: "Seed: [<number>]"
                var m = Regex.Match(resp, @"Seed:\s*\[(-?\d+)\]");
                if (m.Success) return m.Groups[1].Value;
            }
            catch { /* fall through to properties */ }
        }
        return new ServerPropertiesService(_config).GetLevelSeed();
    }

    // Caching world size: scanning a large world dir is O(file count). 30 s cache
    // dampens that to roughly once per status poll cycle on busy panels.
    private long _cachedSize;
    private DateTime _cachedAt = DateTime.MinValue;

    public long GetWorldSizeBytes()
    {
        if ((DateTime.UtcNow - _cachedAt) < TimeSpan.FromSeconds(30)) return _cachedSize;
        var levelName = ReadLevelName(_config.ServerDir) ?? "world";
        var worldDir = Path.Combine(Path.GetFullPath(_config.ServerDir), levelName);
        if (!Directory.Exists(worldDir)) { _cachedSize = 0; _cachedAt = DateTime.UtcNow; return 0; }
        long total = 0;
        try
        {
            foreach (var f in Directory.EnumerateFiles(worldDir, "*", SearchOption.AllDirectories))
            {
                try { total += new FileInfo(f).Length; } catch { }
            }
        }
        catch { }
        _cachedSize = total;
        _cachedAt = DateTime.UtcNow;
        return total;
    }

    /// <summary>
    /// Stop the server, optionally archive the world via BackupService, delete the
    /// world folder(s), apply the chosen seed strategy, then restart. Backups go
    /// to the configured backup dir (typically a slower-but-bigger drive) rather
    /// than next to the world dir.
    /// </summary>
    public async Task<WipeResult> WipeWorldAsync(WipeOptions options, int warningSeconds = 30, CancellationToken ct = default)
    {
        if (!await _lock.WaitAsync(0, ct))
            return new WipeResult(false, null, null, "Another wipe is already in progress.");

        try
        {
            var serverDir = Path.GetFullPath(_config.ServerDir);
            var levelName = ReadLevelName(serverDir) ?? "world";
            var primaryWorld = Path.Combine(serverDir, levelName);
            var props = new ServerPropertiesService(_config);

            // ── Decide what seed the new world will use, BEFORE we stop the server ──
            // Keep mode needs RCON, which only works while MC is alive.
            string seedToWrite;     // value for level-seed= line; "" means random
            string? capturedSeed = null;
            switch (options.Mode)
            {
                case SeedMode.Keep:
                    capturedSeed = await GetCurrentSeedAsync(ct);
                    if (string.IsNullOrEmpty(capturedSeed))
                    {
                        return new WipeResult(false, null, null,
                            "Couldn't read current seed (RCON unreachable + level-seed empty). Stop the server, set level-seed manually, or pick Random/Custom.");
                    }
                    seedToWrite = capturedSeed;
                    _log($"[wipe] Keep-seed mode: captured current seed {capturedSeed} for reuse.");
                    break;
                case SeedMode.Custom:
                    var custom = options.CustomSeed?.Trim() ?? "";
                    if (string.IsNullOrEmpty(custom))
                        return new WipeResult(false, null, null, "Custom seed mode selected but no seed provided.");
                    seedToWrite = custom;
                    _log($"[wipe] Custom-seed mode: new world will use seed {custom}.");
                    break;
                default: // Random
                    seedToWrite = "";
                    _log("[wipe] Random-seed mode: clearing level-seed so MC generates a fresh seed.");
                    break;
            }

            // Loud, urgent player warning before any irreversible action -- wipe is
            // destructive even with a backup (admins still need to restore manually).
            if (warningSeconds > 0 && _proc.IsRunning)
            {
                _log($"[wipe] Announcing {warningSeconds}s wipe warning to players...");
                try
                {
                    await _broadcast.SayAsync(
                        $"WORLD WIPE in {warningSeconds} seconds. Disconnect now if you want to keep your current world!", ct);
                    await _broadcast.ActionBarCountdownAsync("WORLD WIPING", warningSeconds, ct);
                    await _broadcast.SayAsync("Wiping world now.");
                }
                catch (OperationCanceledException) { throw; }
                catch { /* don't abort wipe over a broadcast error */ }
            }

            string? backupName = null;
            if (options.Backup && Directory.Exists(primaryWorld))
            {
                _log("[wipe] Creating backup before wipe...");
                // flush:true here -- we're about to delete the world. Capture every block
                // and every move up to right now, even at the cost of a tick spike.
                var br = await _backup.CreateAsync("pre-wipe", flush: true, ct: ct);
                if (!br.Ok)
                    return new WipeResult(false, null, null, $"Backup failed: {br.Error}. Wipe aborted to preserve data.");
                backupName = br.Name;
            }

            if (_proc.IsRunning)
            {
                _log("[wipe] Stopping server...");
                await _proc.StopAsync(TimeSpan.FromSeconds(30), ct);
            }

            if (Directory.Exists(primaryWorld))
            {
                _log($"[wipe] Deleting {primaryWorld}");
                Directory.Delete(primaryWorld, recursive: true);
            }
            // Legacy sibling dirs (rare on modern NeoForge but cheap to handle)
            foreach (var altSuffix in new[] { "_nether", "_the_end" })
            {
                var altDir = Path.Combine(serverDir, levelName + altSuffix);
                if (!Directory.Exists(altDir)) continue;
                _log($"[wipe] Deleting {altDir}");
                Directory.Delete(altDir, recursive: true);
            }

            // Apply the seed AFTER deletion but BEFORE restart -- MC reads
            // server.properties at startup to determine the new world's seed.
            props.SetLevelSeed(seedToWrite);

            // Map output for the now-deleted world is stale -- clear it so the next
            // render starts fresh against the new terrain.
            _bluemap?.ClearRenderOutput();

            _log("[wipe] World wiped. Restarting server -- Minecraft will generate a fresh world.");
            _proc.Start();
            // For Random mode we don't know the exact new seed yet (MC picks at startup);
            // return the level-seed value we wrote, which is "" for random.
            return new WipeResult(true, backupName, string.IsNullOrEmpty(seedToWrite) ? null : seedToWrite, null);
        }
        catch (Exception ex)
        {
            _log($"[wipe] Failed: {ex.Message}");
            try { if (!_proc.IsRunning) _proc.Start(); } catch { }
            return new WipeResult(false, null, null, ex.Message);
        }
        finally
        {
            _lock.Release();
        }
    }

    private static string? ReadLevelName(string serverDir)
    {
        var path = Path.Combine(serverDir, "server.properties");
        if (!File.Exists(path)) return null;
        foreach (var line in File.ReadAllLines(path))
        {
            if (line.StartsWith("level-name=", StringComparison.Ordinal))
                return line.Substring("level-name=".Length).Trim();
        }
        return null;
    }
}
