using System.Globalization;
using ChessCubing.App.Models;

namespace ChessCubing.App.Services;

public static class MatchEngine
{
    public const string PhaseBlock = "block";
    public const string PhaseCube = "cube";
    public const string ColorWhite = "white";
    public const string ColorBlack = "black";
    public const string ModeTwice = "twice";
    public const string ModeTime = "time";
    public const string PresetFast = "fast";
    public const string PresetFreeze = "freeze";
    public const string PresetMasters = "masters";
    public const long DefaultBlockDurationMs = 180_000;
    public const long DefaultMoveLimitMs = 20_000;
    public const long TimeModeInitialClockMs = 600_000;
    public const long CubeTimeCapMs = 120_000;
    public const long CubeStartHoldMs = 2_000;

    public static readonly IReadOnlyDictionary<string, MatchPresetInfo> Presets =
        new Dictionary<string, MatchPresetInfo>(StringComparer.Ordinal)
        {
            [PresetFast] = new("FAST", 6, "6 coups par joueur."),
            [PresetFreeze] = new("FREEZE", 8, "8 coups par joueur."),
            [PresetMasters] = new("MASTERS", 10, "10 coups par joueur."),
        };

    public static readonly IReadOnlyDictionary<string, MatchModeInfo> Modes =
        new Dictionary<string, MatchModeInfo>(StringComparer.Ordinal)
        {
            [ModeTwice] = new("ChessCubing Twice", "Le gagnant du cube ouvre le Block suivant."),
            [ModeTime] = new("ChessCubing Time", "Chronos cumules et alternance bloc - / bloc +."),
        };

    public static MatchState CreateMatch(MatchConfig config)
    {
        var normalizedMode = Modes.ContainsKey(config.Mode) ? config.Mode : ModeTwice;
        var normalizedPreset = Presets.ContainsKey(config.Preset) ? config.Preset : PresetFast;
        config.Mode = normalizedMode;
        config.Preset = normalizedPreset;
        config.BlockDurationMs = GetBlockDurationMs(config);
        config.MoveLimitMs = GetMoveLimitMs(config);
        config.TimeInitialMs = GetTimeInitialMs(config);
        config.MatchLabel = SanitizeText(config.MatchLabel) is { Length: > 0 } label ? label : "Rencontre ChessCubing";
        config.WhiteName = SanitizeText(config.WhiteName) is { Length: > 0 } white ? white : "Blanc";
        config.BlackName = SanitizeText(config.BlackName) is { Length: > 0 } black ? black : "Noir";
        config.ArbiterName = SanitizeText(config.ArbiterName);
        config.EventName = SanitizeText(config.EventName);
        config.Notes = SanitizeText(config.Notes);

        var quota = Presets[config.Preset].Quota;
        var match = new MatchState
        {
            SchemaVersion = 3,
            Config = config,
            Phase = PhaseBlock,
            Running = false,
            LastTickAt = null,
            BlockNumber = 1,
            CurrentTurn = ColorWhite,
            BlockRemainingMs = config.BlockDurationMs,
            MoveRemainingMs = config.MoveLimitMs,
            Quota = quota,
            Moves = new PlayerIntPair(),
            Clocks = normalizedMode == ModeTime
                ? new PlayerLongPair
                {
                    White = GetTimeInitialMs(config),
                    Black = GetTimeInitialMs(config),
                }
                : null,
            LastMover = null,
            AwaitingBlockClosure = false,
            ClosureReason = string.Empty,
            Result = null,
            Cube = CreateCubeState(),
            DoubleCoup = new DoubleCoupState
            {
                Eligible = false,
                Step = 0,
                Starter = ColorWhite,
            },
            History = [],
        };

        LogEvent(
            match,
            UsesMoveLimit(config.Mode)
                ? $"Match cree en mode {Modes[config.Mode].Label}, cadence {Presets[config.Preset].Label}, Block {FormatClock(config.BlockDurationMs)} et coup {FormatClock(config.MoveLimitMs)}."
                : $"Match cree en mode {Modes[config.Mode].Label}, cadence {Presets[config.Preset].Label}, Block {FormatClock(config.BlockDurationMs)} et chrono initial {FormatClock(GetTimeInitialMs(config))} par joueur, sans temps par coup.");
        LogEvent(match, $"Les Blancs commencent {FormatBlockHeading(config, 1)}.");
        return match;
    }

    public static bool NormalizeRecoveredMatch(MatchState storedMatch)
    {
        var changed = false;

        storedMatch.Config ??= new MatchConfig();
        storedMatch.History ??= [];
        storedMatch.Cube ??= CreateCubeState();
        storedMatch.Cube.History ??= [];
        storedMatch.Cube.PlayerState ??= new CubePlayerStates();
        storedMatch.Cube.PlayerState.White = NormalizeCubePlayerState(storedMatch.Cube.PlayerState.White);
        storedMatch.Cube.PlayerState.Black = NormalizeCubePlayerState(storedMatch.Cube.PlayerState.Black);
        storedMatch.Cube.Times ??= new PlayerNullableLongPair();
        storedMatch.DoubleCoup ??= new DoubleCoupState
        {
            Eligible = false,
            Step = 0,
            Starter = ColorWhite,
        };
        storedMatch.Moves ??= new PlayerIntPair();

        var blockDurationMs = GetBlockDurationMs(storedMatch);
        var moveLimitMs = GetMoveLimitMs(storedMatch);
        var timeInitialMs = GetTimeInitialMs(storedMatch);

        if (storedMatch.SchemaVersion != 3)
        {
            storedMatch.SchemaVersion = 3;
            changed = true;
        }

        if (storedMatch.Config.Mode is not (ModeTwice or ModeTime))
        {
            storedMatch.Config.Mode = ModeTwice;
            changed = true;
        }

        if (!Presets.ContainsKey(storedMatch.Config.Preset))
        {
            storedMatch.Config.Preset = PresetFast;
            changed = true;
        }

        if (storedMatch.Config.BlockDurationMs != blockDurationMs)
        {
            storedMatch.Config.BlockDurationMs = blockDurationMs;
            changed = true;
        }

        if (storedMatch.Config.MoveLimitMs != moveLimitMs)
        {
            storedMatch.Config.MoveLimitMs = moveLimitMs;
            changed = true;
        }

        if (storedMatch.Config.TimeInitialMs != timeInitialMs)
        {
            storedMatch.Config.TimeInitialMs = timeInitialMs;
            changed = true;
        }

        if (storedMatch.BlockRemainingMs <= 0)
        {
            storedMatch.BlockRemainingMs = blockDurationMs;
            changed = true;
        }

        if (storedMatch.MoveRemainingMs <= 0)
        {
            storedMatch.MoveRemainingMs = moveLimitMs;
            changed = true;
        }

        if (storedMatch.BlockNumber <= 0)
        {
            storedMatch.BlockNumber = 1;
            changed = true;
        }

        if (storedMatch.Quota <= 0)
        {
            storedMatch.Quota = Presets[storedMatch.Config.Preset].Quota;
            changed = true;
        }

        if (storedMatch.CurrentTurn is not (ColorWhite or ColorBlack))
        {
            storedMatch.CurrentTurn = ColorWhite;
            changed = true;
        }

        if (IsTimeMode(storedMatch) && storedMatch.Clocks is null)
        {
            storedMatch.Clocks = new PlayerLongPair
            {
                White = timeInitialMs,
                Black = timeInitialMs,
            };
            changed = true;
        }

        if (!IsTimeMode(storedMatch))
        {
            storedMatch.Clocks = null;
        }

        if (storedMatch.Phase == PhaseBlock && storedMatch.LastTickAt is not null && storedMatch.LastTickAt <= 0)
        {
            storedMatch.LastTickAt = null;
            changed = true;
        }

        if (storedMatch.Cube.Round <= 0)
        {
            storedMatch.Cube.Round = 1;
            changed = true;
        }

        storedMatch.Cube.Running = IsAnyCubeTimerRunning(storedMatch);

        if (storedMatch.AwaitingBlockClosure && storedMatch.Phase == PhaseBlock)
        {
            OpenCubePhase(storedMatch, string.IsNullOrWhiteSpace(storedMatch.ClosureReason) ? "La phase chess etait deja terminee." : storedMatch.ClosureReason);
            changed = true;
        }

        return changed;
    }

    public static bool SyncRunningState(MatchState? storedMatch, long? nowMs = null)
    {
        if (storedMatch is null || !string.IsNullOrEmpty(storedMatch.Result))
        {
            return false;
        }

        if (!storedMatch.Running || storedMatch.Phase != PhaseBlock || storedMatch.LastTickAt is null)
        {
            return false;
        }

        var now = nowMs ?? NowUnixMs();
        var delta = now - storedMatch.LastTickAt.Value;
        if (delta <= 0)
        {
            return false;
        }

        storedMatch.LastTickAt = now;
        storedMatch.BlockRemainingMs = Math.Max(0, storedMatch.BlockRemainingMs - delta);
        if (UsesMoveLimit(storedMatch))
        {
            storedMatch.MoveRemainingMs = Math.Max(0, storedMatch.MoveRemainingMs - delta);
        }

        if (storedMatch.Clocks is not null)
        {
            if (storedMatch.CurrentTurn == ColorWhite)
            {
                storedMatch.Clocks.White -= delta;
            }
            else
            {
                storedMatch.Clocks.Black -= delta;
            }
        }

        if (storedMatch.BlockRemainingMs == 0)
        {
            RequestBlockClosure(
                storedMatch,
                $"Le temps {GetBlockGenitivePhrase(storedMatch)} {FormatClock(GetBlockDurationMs(storedMatch))} est ecoule.");
        }
        else if (UsesMoveLimit(storedMatch) && storedMatch.MoveRemainingMs == 0)
        {
            RegisterMoveTimeout(storedMatch, true);
        }

        return true;
    }

    public static void StartBlock(MatchState storedMatch)
    {
        if (storedMatch.Phase != PhaseBlock || !string.IsNullOrEmpty(storedMatch.Result))
        {
            return;
        }

        storedMatch.Running = true;
        storedMatch.AwaitingBlockClosure = false;
        storedMatch.ClosureReason = string.Empty;
        storedMatch.LastTickAt = NowUnixMs();
        LogEvent(
            storedMatch,
            storedMatch.BlockNumber == 1 && storedMatch.Moves.White == 0 && storedMatch.Moves.Black == 0
                ? $"{FormatBlockHeading(storedMatch, 1)} demarre."
                : $"{FormatBlockHeading(storedMatch, storedMatch.BlockNumber)} relance.");
    }

    public static void PauseBlock(MatchState storedMatch)
    {
        if (!storedMatch.Running)
        {
            return;
        }

        storedMatch.Running = false;
        storedMatch.LastTickAt = null;
        LogEvent(storedMatch, $"{FormatBlockHeading(storedMatch, storedMatch.BlockNumber)} passe en pause.");
    }

    public static void RequestBlockClosure(MatchState storedMatch, string reason)
    {
        if (storedMatch.Phase != PhaseBlock)
        {
            return;
        }

        storedMatch.Running = false;
        storedMatch.LastTickAt = null;
        storedMatch.AwaitingBlockClosure = false;
        storedMatch.ClosureReason = string.Empty;
        storedMatch.MoveRemainingMs = GetMoveLimitMs(storedMatch);
        LogEvent(storedMatch, $"{reason} Passage automatique vers la page cube.");
        OpenCubePhase(storedMatch, reason);
    }

    public static void OpenCubePhase(MatchState storedMatch, string reason = "")
    {
        storedMatch.Phase = PhaseCube;
        storedMatch.Running = false;
        storedMatch.LastTickAt = null;
        storedMatch.AwaitingBlockClosure = false;
        storedMatch.ClosureReason = string.Empty;
        storedMatch.Cube.Number = PickCubeNumber();
        storedMatch.Cube.Running = false;
        storedMatch.Cube.StartedAt = null;
        storedMatch.Cube.ElapsedMs = 0;
        storedMatch.Cube.PhaseAlertPending = true;
        storedMatch.Cube.Times = new PlayerNullableLongPair();
        storedMatch.Cube.PlayerState = new CubePlayerStates
        {
            White = CreateCubePlayerState(),
            Black = CreateCubePlayerState(),
        };
        storedMatch.Cube.Round = 1;
        LogEvent(
            storedMatch,
            $"{(string.IsNullOrWhiteSpace(reason) ? string.Empty : $"{reason} ")}Page cube ouverte. Cube n{storedMatch.Cube.Number} designe.");
    }

    public static void StartCubeTimer(MatchState storedMatch, string color)
    {
        if (storedMatch.Phase != PhaseCube || !string.IsNullOrEmpty(storedMatch.Result))
        {
            return;
        }

        if (storedMatch.Cube.Number is null)
        {
            storedMatch.Cube.Number = PickCubeNumber();
        }

        if (GetCubeTime(storedMatch.Cube.Times, color) is not null)
        {
            return;
        }

        var playerState = GetCubePlayerState(storedMatch.Cube.PlayerState, color);
        if (playerState.Running)
        {
            return;
        }

        playerState.Running = true;
        playerState.StartedAt = NowUnixMs();
        storedMatch.Cube.Running = true;
        LogEvent(
            storedMatch,
            $"{PlayerName(storedMatch, color)} demarre son chrono cube sur le cube n{storedMatch.Cube.Number}.");
    }

    public static void CaptureCubeTime(MatchState storedMatch, string color)
    {
        if (storedMatch.Phase != PhaseCube)
        {
            return;
        }

        var playerState = GetCubePlayerState(storedMatch.Cube.PlayerState, color);
        if (!playerState.Running || GetCubeTime(storedMatch.Cube.Times, color) is not null)
        {
            return;
        }

        var elapsedMs = playerState.ElapsedMs + (NowUnixMs() - (playerState.StartedAt ?? NowUnixMs()));
        SetCubeTime(storedMatch.Cube.Times, color, elapsedMs);
        playerState.ElapsedMs = elapsedMs;
        playerState.StartedAt = null;
        playerState.Running = false;
        storedMatch.Cube.ElapsedMs = Math.Max(GetCubePlayerElapsed(storedMatch, ColorWhite), GetCubePlayerElapsed(storedMatch, ColorBlack));
        storedMatch.Cube.Running = IsAnyCubeTimerRunning(storedMatch);
        LogEvent(storedMatch, $"{PlayerName(storedMatch, color)} arrete le cube en {FormatStopwatch(elapsedMs)}.");

        if (storedMatch.Cube.Times.White is not null && storedMatch.Cube.Times.Black is not null)
        {
            storedMatch.Cube.Running = false;
            storedMatch.Cube.History.Add(
                new CubeHistoryEntry
                {
                    BlockNumber = storedMatch.BlockNumber,
                    Number = storedMatch.Cube.Number,
                    White = storedMatch.Cube.Times.White,
                    Black = storedMatch.Cube.Times.Black,
                });
        }
    }

    public static void ApplyCubeOutcome(MatchState storedMatch)
    {
        if (storedMatch.Phase != PhaseCube)
        {
            return;
        }

        var white = storedMatch.Cube.Times.White;
        var black = storedMatch.Cube.Times.Black;
        if (white is null || black is null)
        {
            return;
        }

        if (storedMatch.Config.Mode == ModeTwice)
        {
            var winner = white < black ? ColorWhite : ColorBlack;
            PrepareNextTwiceBlock(storedMatch, winner);
            return;
        }

        ApplyTimeAdjustments(storedMatch, white.Value, black.Value);
        PrepareNextTimeBlock(storedMatch);
    }

    public static void PrepareNextTwiceBlock(MatchState storedMatch, string winner)
    {
        var hadDouble = storedMatch.LastMover != winner && !string.IsNullOrEmpty(storedMatch.LastMover);
        LogEvent(storedMatch, $"{PlayerName(storedMatch, winner)} gagne la phase cube et ouvrira le Block suivant.");

        storedMatch.BlockNumber += 1;
        storedMatch.Phase = PhaseBlock;
        storedMatch.Running = false;
        storedMatch.LastTickAt = null;
        storedMatch.BlockRemainingMs = GetBlockDurationMs(storedMatch);
        storedMatch.MoveRemainingMs = GetMoveLimitMs(storedMatch);
        storedMatch.Moves = new PlayerIntPair();
        storedMatch.CurrentTurn = winner;
        storedMatch.DoubleCoup = new DoubleCoupState
        {
            Eligible = hadDouble,
            Step = hadDouble ? 1 : 0,
            Starter = winner,
        };
        ResetCubeState(storedMatch);

        if (hadDouble)
        {
            LogEvent(storedMatch, $"Double coup disponible pour {PlayerName(storedMatch, winner)}.");
        }
        else
        {
            LogEvent(storedMatch, "Aucun double coup n'est accorde sur ce depart.");
        }
    }

    public static void PrepareNextTimeBlock(MatchState storedMatch)
    {
        storedMatch.BlockNumber += 1;
        storedMatch.Phase = PhaseBlock;
        storedMatch.Running = false;
        storedMatch.LastTickAt = null;
        storedMatch.BlockRemainingMs = GetBlockDurationMs(storedMatch);
        storedMatch.MoveRemainingMs = GetMoveLimitMs(storedMatch);
        storedMatch.Moves = new PlayerIntPair();
        storedMatch.DoubleCoup = new DoubleCoupState
        {
            Eligible = false,
            Step = 0,
            Starter = storedMatch.CurrentTurn,
        };
        ResetCubeState(storedMatch);
        LogEvent(
            storedMatch,
            $"{FormatBlockHeading(storedMatch, storedMatch.BlockNumber)} pret. Le trait est conserve : {PlayerName(storedMatch, storedMatch.CurrentTurn)} reprend.");
    }

    public static void ApplyTimeAdjustments(MatchState storedMatch, long whiteTime, long blackTime)
    {
        var preview = GetTimeAdjustmentPreview(storedMatch, whiteTime, blackTime);
        if (preview is null || storedMatch.Clocks is null)
        {
            return;
        }

        storedMatch.Clocks.White = preview.WhiteAfter;
        storedMatch.Clocks.Black = preview.BlackAfter;

        if (preview.BlockType == "minus")
        {
            LogEvent(
                storedMatch,
                $"Bloc - : {FormatStopwatch(preview.CappedWhite)} retire au chrono Blanc, {FormatStopwatch(preview.CappedBlack)} retire au chrono Noir.");
        }
        else
        {
            LogEvent(
                storedMatch,
                $"Bloc + : {FormatStopwatch(preview.CappedBlack)} ajoute au chrono Blanc, {FormatStopwatch(preview.CappedWhite)} ajoute au chrono Noir.");
        }
    }

    public static void ReplayCubePhase(MatchState storedMatch)
    {
        if (storedMatch.Phase != PhaseCube)
        {
            return;
        }

        storedMatch.Cube.Running = false;
        storedMatch.Cube.StartedAt = null;
        storedMatch.Cube.ElapsedMs = 0;
        storedMatch.Cube.PhaseAlertPending = true;
        storedMatch.Cube.Times = new PlayerNullableLongPair();
        storedMatch.Cube.PlayerState = new CubePlayerStates
        {
            White = CreateCubePlayerState(),
            Black = CreateCubePlayerState(),
        };
        storedMatch.Cube.Round += 1;
        LogEvent(storedMatch, $"Phase cube relancee (tentative {storedMatch.Cube.Round}).");
    }

    public static void RegisterCountedMove(MatchState storedMatch, string source)
    {
        if (storedMatch.Phase != PhaseBlock)
        {
            return;
        }

        var actor = storedMatch.CurrentTurn;
        if (GetMoveCount(storedMatch.Moves, actor) >= storedMatch.Quota)
        {
            LogEvent(storedMatch, $"{PlayerName(storedMatch, actor)} a deja atteint son quota. Utiliser le mode hors quota si necessaire.");
            return;
        }

        SetMoveCount(storedMatch.Moves, actor, GetMoveCount(storedMatch.Moves, actor) + 1);
        storedMatch.LastMover = actor;
        storedMatch.MoveRemainingMs = GetMoveLimitMs(storedMatch);

        if (source == "double")
        {
            storedMatch.DoubleCoup.Step = 0;
        }

        LogEvent(
            storedMatch,
            $"{PlayerName(storedMatch, actor)} valide son coup ({GetMoveCount(storedMatch.Moves, actor)} / {storedMatch.Quota}).");

        storedMatch.CurrentTurn = OpponentOf(actor);
        VerifyQuotaCompletion(storedMatch);
    }

    public static void RegisterFreeDoubleMove(MatchState storedMatch)
    {
        if (storedMatch.DoubleCoup.Step != 1)
        {
            return;
        }

        var actor = storedMatch.CurrentTurn;
        storedMatch.LastMover = actor;
        storedMatch.MoveRemainingMs = GetMoveLimitMs(storedMatch);
        storedMatch.DoubleCoup.Step = 2;
        LogEvent(storedMatch, $"Premier coup gratuit du double coup joue par {PlayerName(storedMatch, actor)}.");
    }

    public static void RegisterMoveTimeout(MatchState storedMatch, bool automatic)
    {
        if (storedMatch.Phase != PhaseBlock || !UsesMoveLimit(storedMatch))
        {
            return;
        }

        var actor = storedMatch.CurrentTurn;

        if (storedMatch.DoubleCoup.Step == 1)
        {
            storedMatch.DoubleCoup.Step = 0;
            storedMatch.MoveRemainingMs = GetMoveLimitMs(storedMatch);
            LogEvent(storedMatch, $"Depassement sur le premier coup gratuit de {PlayerName(storedMatch, actor)} : le double coup est annule.");
            return;
        }

        if (GetMoveCount(storedMatch.Moves, actor) < storedMatch.Quota)
        {
            SetMoveCount(storedMatch.Moves, actor, GetMoveCount(storedMatch.Moves, actor) + 1);
        }

        if (storedMatch.DoubleCoup.Step == 2)
        {
            storedMatch.DoubleCoup.Step = 0;
        }

        storedMatch.CurrentTurn = OpponentOf(actor);
        storedMatch.MoveRemainingMs = GetMoveLimitMs(storedMatch);
        LogEvent(
            storedMatch,
            $"{(automatic ? "Temps par coup ecoule." : $"Depassement manuel du temps par coup {FormatClock(GetMoveLimitMs(storedMatch))}.")} {PlayerName(storedMatch, actor)} perd son coup, qui reste compte dans le quota.");
        VerifyQuotaCompletion(storedMatch);
    }

    public static void VerifyQuotaCompletion(MatchState storedMatch)
    {
        if (storedMatch.Moves.White >= storedMatch.Quota && storedMatch.Moves.Black >= storedMatch.Quota)
        {
            RequestBlockClosure(storedMatch, "Les deux joueurs ont atteint leur quota de coups.");
        }
    }

    public static void SetResult(MatchState storedMatch, string result)
    {
        if (!string.IsNullOrEmpty(storedMatch.Result))
        {
            return;
        }

        storedMatch.Running = false;
        storedMatch.LastTickAt = null;
        storedMatch.Result = result;
        LogEvent(storedMatch, $"{ResultText(storedMatch)}.");
    }

    public static string RenderModeContext(MatchState storedMatch)
    {
        if (storedMatch.Config.Mode == ModeTime)
        {
            return GetTimeBlockType(storedMatch.BlockNumber) == "minus"
                ? "Bloc - : temps cube retire a son propre chrono"
                : "Bloc + : temps cube ajoute au chrono adverse";
        }

        if (storedMatch.DoubleCoup.Step == 1)
        {
            return $"Double coup actif pour {PlayerName(storedMatch, storedMatch.DoubleCoup.Starter)}";
        }

        if (storedMatch.DoubleCoup.Step == 2)
        {
            return "Deuxieme coup du double en attente";
        }

        return "Le gagnant du cube ouvrira la prochaine partie";
    }

    public static string RenderLastCube(MatchState storedMatch, string color)
    {
        var last = storedMatch.Cube.History.Count == 0 ? null : storedMatch.Cube.History[^1];
        if (last is null)
        {
            return "--";
        }

        var value = color == ColorWhite ? last.White : last.Black;
        return value is null ? "--" : FormatStopwatch(value.Value);
    }

    public static string RenderCubeElapsed(MatchState storedMatch)
    {
        if (storedMatch.Phase != PhaseCube)
        {
            return "00:00.0";
        }

        return FormatStopwatch(Math.Max(GetCubePlayerElapsed(storedMatch, ColorWhite), GetCubePlayerElapsed(storedMatch, ColorBlack)));
    }

    public static string RenderCubeMeta(MatchState storedMatch, string color)
    {
        var time = color == ColorWhite ? storedMatch.Cube.Times.White : storedMatch.Cube.Times.Black;
        if (time is null)
        {
            return string.Empty;
        }

        if (storedMatch.Config.Mode != ModeTime)
        {
            return "temps capture";
        }

        if (storedMatch.Cube.Times.White is not null && storedMatch.Cube.Times.Black is not null)
        {
            var preview = GetTimeAdjustmentPreview(storedMatch, storedMatch.Cube.Times.White.Value, storedMatch.Cube.Times.Black.Value);
            if (preview is null)
            {
                return string.Empty;
            }

            var delta = color == ColorWhite ? preview.WhiteDelta : preview.BlackDelta;
            var cappedTime = color == ColorWhite ? preview.CappedWhite : preview.CappedBlack;
            var wasCapped = time.Value > cappedTime;
            return wasCapped
                ? $"Impact chrono {FormatSignedStopwatch(delta)} (cap {FormatStopwatch(cappedTime)})"
                : $"Impact chrono {FormatSignedStopwatch(delta)}";
        }

        return time.Value <= CubeTimeCapMs
            ? "impact chrono en attente"
            : $"impact en attente, cap {FormatStopwatch(CubeTimeCapMs)}";
    }

    public static string ResultText(MatchState storedMatch)
    {
        return storedMatch.Result switch
        {
            ColorWhite => $"Victoire de {PlayerName(storedMatch, ColorWhite)}",
            ColorBlack => $"Victoire de {PlayerName(storedMatch, ColorBlack)}",
            _ => "Partie arretee",
        };
    }

    public static string RouteForMatch(MatchState? storedMatch)
    {
        if (storedMatch is null)
        {
            return "/application.html";
        }

        return storedMatch.Phase == PhaseCube && string.IsNullOrEmpty(storedMatch.Result)
            ? "/cube.html"
            : "/chrono.html";
    }

    public static bool IsTimeMode(object? matchOrConfig)
        => ResolveMode(matchOrConfig) == ModeTime;

    public static bool UsesMoveLimit(object? matchOrConfig)
        => !IsTimeMode(matchOrConfig);

    public static long GetTimeInitialMs(object? matchOrConfig)
    {
        return matchOrConfig switch
        {
            MatchState match => NormalizeDurationMs(match.Config.TimeInitialMs, TimeModeInitialClockMs),
            MatchConfig config => NormalizeDurationMs(config.TimeInitialMs, TimeModeInitialClockMs),
            _ => TimeModeInitialClockMs,
        };
    }

    public static string GetBlockLabel(object? _)
        => "Block";

    public static string GetBlockPhrase(object? _)
        => "Le Block";

    public static string GetBlockGenitivePhrase(object? _)
        => "du Block";

    public static string FormatBlockHeading(object? matchOrConfig, int blockNumber)
        => $"{GetBlockLabel(matchOrConfig)} {blockNumber}";

    public static string PlayerName(MatchState storedMatch, string color)
        => color == ColorWhite ? storedMatch.Config.WhiteName : storedMatch.Config.BlackName;

    public static string OpponentOf(string color)
        => color == ColorWhite ? ColorBlack : ColorWhite;

    public static string GetTimeBlockType(int blockNumber)
        => blockNumber % 2 == 1 ? "minus" : "plus";

    public static void LogEvent(MatchState storedMatch, string message)
    {
        storedMatch.History.Add(
            new MatchHistoryEntry
            {
                Message = message,
                Time = DateTime.Now.ToString("HH:mm:ss", CultureInfo.GetCultureInfo("fr-FR")),
            });
    }

    public static string FormatClock(long ms)
    {
        var totalSeconds = Math.Max(0, (long)Math.Floor(ms / 1000d));
        var minutes = (totalSeconds / 60).ToString("00", CultureInfo.InvariantCulture);
        var seconds = (totalSeconds % 60).ToString("00", CultureInfo.InvariantCulture);
        return $"{minutes}:{seconds}";
    }

    public static string FormatSignedClock(long ms)
    {
        var negative = ms < 0 ? "-" : string.Empty;
        var totalSeconds = (long)Math.Floor(Math.Abs(ms) / 1000d);
        var minutes = (totalSeconds / 60).ToString("00", CultureInfo.InvariantCulture);
        var seconds = (totalSeconds % 60).ToString("00", CultureInfo.InvariantCulture);
        return $"{negative}{minutes}:{seconds}";
    }

    public static string FormatSignedStopwatch(long ms)
        => $"{(ms < 0 ? "-" : "+")}{FormatStopwatch(ms)}";

    public static string FormatStopwatch(long ms)
    {
        var absolute = Math.Abs(ms);
        var totalSeconds = (long)Math.Floor(absolute / 1000d);
        var minutes = (totalSeconds / 60).ToString("00", CultureInfo.InvariantCulture);
        var seconds = (totalSeconds % 60).ToString("00", CultureInfo.InvariantCulture);
        var tenths = (absolute % 1000) / 100;
        return $"{minutes}:{seconds}.{tenths}";
    }

    public static string SanitizeText(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return string.Empty;
        }

        var sanitized = value.Replace("<", string.Empty, StringComparison.Ordinal)
            .Replace(">", string.Empty, StringComparison.Ordinal);
        return string.Join(" ", sanitized.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries)).Trim();
    }

    public static bool IsSupportedSchemaVersion(int version)
        => version is 2 or 3;

    public static long GetBlockDurationMs(object? matchOrConfig)
    {
        return matchOrConfig switch
        {
            MatchState match => NormalizeDurationMs(match.Config.BlockDurationMs, DefaultBlockDurationMs),
            MatchConfig config => NormalizeDurationMs(config.BlockDurationMs, DefaultBlockDurationMs),
            _ => DefaultBlockDurationMs,
        };
    }

    public static long GetMoveLimitMs(object? matchOrConfig)
    {
        return matchOrConfig switch
        {
            MatchState match => NormalizeDurationMs(match.Config.MoveLimitMs, DefaultMoveLimitMs),
            MatchConfig config => NormalizeDurationMs(config.MoveLimitMs, DefaultMoveLimitMs),
            _ => DefaultMoveLimitMs,
        };
    }

    public static long NormalizeDurationMs(long value, long fallbackMs)
        => value <= 0 ? fallbackMs : value;

    public static TimeAdjustmentPreview? GetTimeAdjustmentPreview(MatchState storedMatch, long whiteTime, long blackTime)
    {
        if (storedMatch.Clocks is null)
        {
            return null;
        }

        var cappedWhite = Math.Min(whiteTime, CubeTimeCapMs);
        var cappedBlack = Math.Min(blackTime, CubeTimeCapMs);
        var blockType = GetTimeBlockType(storedMatch.BlockNumber);
        var whiteDelta = blockType == "minus" ? -cappedWhite : cappedBlack;
        var blackDelta = blockType == "minus" ? -cappedBlack : cappedWhite;

        return new TimeAdjustmentPreview(
            blockType,
            whiteTime < blackTime ? ColorWhite : blackTime < whiteTime ? ColorBlack : null,
            cappedWhite,
            cappedBlack,
            whiteDelta,
            blackDelta,
            storedMatch.Clocks.White + whiteDelta,
            storedMatch.Clocks.Black + blackDelta);
    }

    public static CubePlayerState CreateCubePlayerState()
        => new()
        {
            Running = false,
            StartedAt = null,
            ElapsedMs = 0,
        };

    public static CubePlayerState NormalizeCubePlayerState(CubePlayerState? playerState)
    {
        return new CubePlayerState
        {
            Running = playerState?.Running ?? false,
            StartedAt = playerState?.StartedAt,
            ElapsedMs = playerState?.ElapsedMs ?? 0,
        };
    }

    public static bool IsAnyCubeTimerRunning(MatchState storedMatch)
        => storedMatch.Cube.PlayerState.White.Running || storedMatch.Cube.PlayerState.Black.Running;

    public static long GetCubePlayerElapsed(MatchState storedMatch, string color)
    {
        var playerState = GetCubePlayerState(storedMatch.Cube.PlayerState, color);
        var capturedTime = GetCubeTime(storedMatch.Cube.Times, color);

        if (capturedTime is not null)
        {
            return capturedTime.Value;
        }

        if (playerState.Running && playerState.StartedAt is not null)
        {
            return playerState.ElapsedMs + (NowUnixMs() - playerState.StartedAt.Value);
        }

        return playerState.ElapsedMs;
    }

    public static string FormatCubePlayerTime(MatchState storedMatch, string color)
    {
        var elapsed = GetCubePlayerElapsed(storedMatch, color);
        var playerState = GetCubePlayerState(storedMatch.Cube.PlayerState, color);
        var captured = GetCubeTime(storedMatch.Cube.Times, color);

        if (playerState.Running)
        {
            return FormatStopwatch(elapsed);
        }

        if (elapsed <= 0 && captured is null)
        {
            return "--";
        }

        return FormatStopwatch(elapsed);
    }

    public static long NowUnixMs()
        => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

    private static string ResolveMode(object? matchOrConfig)
    {
        return matchOrConfig switch
        {
            MatchState match => match.Config.Mode,
            MatchConfig config => config.Mode,
            string mode => mode,
            _ => ModeTwice,
        };
    }

    private static CubeState CreateCubeState()
    {
        return new CubeState
        {
            Number = null,
            Running = false,
            StartedAt = null,
            ElapsedMs = 0,
            PhaseAlertPending = false,
            Times = new PlayerNullableLongPair(),
            PlayerState = new CubePlayerStates
            {
                White = CreateCubePlayerState(),
                Black = CreateCubePlayerState(),
            },
            Round = 1,
            History = [],
        };
    }

    private static void ResetCubeState(MatchState storedMatch)
    {
        storedMatch.Cube.Running = false;
        storedMatch.Cube.StartedAt = null;
        storedMatch.Cube.ElapsedMs = 0;
        storedMatch.Cube.PhaseAlertPending = false;
        storedMatch.Cube.Times = new PlayerNullableLongPair();
        storedMatch.Cube.PlayerState = new CubePlayerStates
        {
            White = CreateCubePlayerState(),
            Black = CreateCubePlayerState(),
        };
        storedMatch.Cube.Number = null;
    }

    private static int PickCubeNumber()
        => Random.Shared.Next(1, 5);

    private static int GetMoveCount(PlayerIntPair pair, string color)
        => color == ColorWhite ? pair.White : pair.Black;

    private static void SetMoveCount(PlayerIntPair pair, string color, int value)
    {
        if (color == ColorWhite)
        {
            pair.White = value;
        }
        else
        {
            pair.Black = value;
        }
    }

    private static long? GetCubeTime(PlayerNullableLongPair pair, string color)
        => color == ColorWhite ? pair.White : pair.Black;

    private static void SetCubeTime(PlayerNullableLongPair pair, string color, long value)
    {
        if (color == ColorWhite)
        {
            pair.White = value;
        }
        else
        {
            pair.Black = value;
        }
    }

    private static CubePlayerState GetCubePlayerState(CubePlayerStates states, string color)
        => color == ColorWhite ? states.White : states.Black;
}
