using System.Text.Json;
using RpgRoller.Contracts;
using RpgRoller.Domain;

namespace RpgRoller.Services;

public sealed class GameRollService(GameStateStore stateStore, GamePersistenceService persistenceService, IDiceRoller diceRoller)
{
    public ServiceResult<RollResult> RollSkill(string sessionToken, Guid skillId, string visibility, int situationalModifier = 0)
    {
        lock (stateStore.Gate)
        {
            var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken);
            if (user is null)
                return ServiceResult<RollResult>.Failure("unauthorized", "You must be logged in.");

            if (!stateStore.SkillsById.TryGetValue(skillId, out var skill))
                return ServiceResult<RollResult>.Failure("skill_not_found", "Skill was not found.");

            var character = stateStore.CharactersById[skill.CharacterId];
            if (!GameContextResolver.TryResolveCharacterCampaignLocked(stateStore, character, out var campaign, out var campaignError))
                return ServiceResult<RollResult>.Failure(campaignError!.Code, campaignError.Message);

            if (!GameAuthorization.CanEditCharacter(user.Id, character, campaign))
                return ServiceResult<RollResult>.Failure("forbidden", "Only the owner or GM can roll this skill.");

            var parsedExpression = DiceRules.ParseExpression(campaign.Ruleset, skill.DiceRollDefinition);
            if (!parsedExpression.Succeeded)
                return ServiceResult<RollResult>.Failure(parsedExpression.Error!.Code, parsedExpression.Error.Message);

            if (situationalModifier != 0 && campaign.Ruleset != RulesetKind.Rolemaster)
                return ServiceResult<RollResult>.Failure("invalid_situational_modifier", "Situational modifiers are supported only for Rolemaster skill rolls.");

            if (campaign.Ruleset == RulesetKind.Rolemaster && (situationalModifier < -MaxSituationalModifier || situationalModifier > MaxSituationalModifier))
                return ServiceResult<RollResult>.Failure("invalid_situational_modifier", $"Situational modifier must be between {-MaxSituationalModifier} and {MaxSituationalModifier}.");

            var parsedVisibility = RollVisibilityParser.Parse(visibility);
            if (!parsedVisibility.Succeeded)
                return ServiceResult<RollResult>.Failure(parsedVisibility.Error!.Code, parsedVisibility.Error.Message);

            var roll = m_RollEngine.Roll(campaign.Ruleset, parsedExpression.Value!, skill.WildDice, skill.AllowFumble, skill.FumbleRange, skill.RolemasterAutoRetry, situationalModifier);
            return RecordRollLocked(user, campaign, character, skill.Id, parsedVisibility.Value, roll, parsedExpression.Value!.Canonical);
        }
    }

    public ServiceResult<RollResult> RollCustom(string sessionToken, Guid characterId, string expression, string visibility)
    {
        lock (stateStore.Gate)
        {
            var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken);
            if (user is null)
                return ServiceResult<RollResult>.Failure("unauthorized", "You must be logged in.");

            if (!stateStore.CharactersById.TryGetValue(characterId, out var character))
                return ServiceResult<RollResult>.Failure("character_not_found", "Character was not found.");

            if (!GameContextResolver.TryResolveCharacterCampaignLocked(stateStore, character, out var campaign, out var campaignError))
                return ServiceResult<RollResult>.Failure(campaignError!.Code, campaignError.Message);

            if (!GameAuthorization.CanEditCharacter(user.Id, character, campaign))
                return ServiceResult<RollResult>.Failure("forbidden", "Only the owner or GM can make a custom roll for this character.");

            var parsedExpression = DiceRules.ParseExpression(campaign.Ruleset, expression);
            if (!parsedExpression.Succeeded)
                return ServiceResult<RollResult>.Failure(parsedExpression.Error!.Code, parsedExpression.Error.Message);

            var parsedVisibility = RollVisibilityParser.Parse(visibility);
            if (!parsedVisibility.Succeeded)
                return ServiceResult<RollResult>.Failure(parsedVisibility.Error!.Code, parsedVisibility.Error.Message);

            var (wildDice, allowFumble, fumbleRange) = CustomRollOptionsResolver.Resolve(campaign.Ruleset);
            var roll = m_RollEngine.Roll(campaign.Ruleset, parsedExpression.Value!, wildDice, allowFumble, fumbleRange);
            return RecordRollLocked(user, campaign, character, CustomRollSkillId, parsedVisibility.Value, roll, parsedExpression.Value!.Canonical);
        }
    }

    public ServiceResult<IReadOnlyList<CampaignLogEntry>> GetCampaignLog(string sessionToken, Guid campaignId)
    {
        lock (stateStore.Gate)
        {
            var context = GameContextResolver.ResolveCampaignContextLocked(stateStore, sessionToken, campaignId);
            if (!context.Succeeded)
                return ServiceResult<IReadOnlyList<CampaignLogEntry>>.Failure(context.Error!.Code, context.Error.Message);

            var (user, campaign) = context.Value;
            var entries = GetVisibleCampaignLogEntriesLocked(user, campaign).TakeLast(CampaignLogHistoryWindowSize).Select(ToLogEntry).ToArray();

            return ServiceResult<IReadOnlyList<CampaignLogEntry>>.Success(entries);
        }
    }

    public ServiceResult<CampaignLogPage> GetCampaignLogPage(string sessionToken, Guid campaignId, Guid? afterRollId = null, int? limit = null)
    {
        lock (stateStore.Gate)
        {
            var context = GameContextResolver.ResolveCampaignContextLocked(stateStore, sessionToken, campaignId);
            if (!context.Succeeded)
                return ServiceResult<CampaignLogPage>.Failure(context.Error!.Code, context.Error.Message);

            var (user, campaign) = context.Value;
            var pageSize = NormalizeCampaignLogPageSize(limit);
            var visibleEntries = GetVisibleCampaignLogEntriesLocked(user, campaign).ToArray();

            if (!afterRollId.HasValue)
            {
                var initialEntries = visibleEntries.TakeLast(pageSize).Select(entry => ToLogListEntry(user, campaign, entry)).ToArray();
                return ServiceResult<CampaignLogPage>.Success(new(initialEntries, initialEntries.LastOrDefault()?.RollId, visibleEntries.Length > pageSize, false));
            }

            var afterIndex = Array.FindIndex(visibleEntries, entry => entry.Id == afterRollId.Value);
            if (afterIndex < 0)
            {
                var replacementEntries = visibleEntries.TakeLast(pageSize).Select(entry => ToLogListEntry(user, campaign, entry)).ToArray();
                return ServiceResult<CampaignLogPage>.Success(new(replacementEntries, replacementEntries.LastOrDefault()?.RollId, visibleEntries.Length > pageSize, true));
            }

            var newEntries = visibleEntries.Skip(afterIndex + 1).ToArray();
            if (newEntries.Length == 0)
                return ServiceResult<CampaignLogPage>.Success(new([], afterRollId, false, false));

            if (newEntries.Length > pageSize)
            {
                var replacementEntries = newEntries.TakeLast(pageSize).Select(entry => ToLogListEntry(user, campaign, entry)).ToArray();
                return ServiceResult<CampaignLogPage>.Success(new(replacementEntries, replacementEntries[^1].RollId, true, true));
            }

            var appendedEntries = newEntries.Select(entry => ToLogListEntry(user, campaign, entry)).ToArray();
            return ServiceResult<CampaignLogPage>.Success(new(appendedEntries, appendedEntries[^1].RollId, false, false));
        }
    }

    public ServiceResult<CampaignRollDetail> GetRollDetail(string sessionToken, Guid rollId)
    {
        lock (stateStore.Gate)
        {
            var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken);
            if (user is null)
                return ServiceResult<CampaignRollDetail>.Failure("unauthorized", "You must be logged in.");

            var entry = stateStore.RollLog.FirstOrDefault(candidate => candidate.Id == rollId);
            if (entry is null)
                return ServiceResult<CampaignRollDetail>.Failure("roll_not_found", "Roll was not found.");

            if (!stateStore.CampaignsById.TryGetValue(entry.CampaignId, out var campaign) || !GameAuthorization.CanViewRoll(stateStore, user.Id, campaign, entry))
                return ServiceResult<CampaignRollDetail>.Failure("roll_not_found", "Roll was not found.");

            return ServiceResult<CampaignRollDetail>.Success(GameDtoMapper.ToCampaignRollDetail(entry, DeserializeDice(entry.Dice).ToArray()));
        }
    }

    public ServiceResult<CampaignStateSnapshot> GetCampaignStateSnapshot(string sessionToken, Guid campaignId)
    {
        lock (stateStore.Gate)
        {
            var context = GameContextResolver.ResolveCampaignContextLocked(stateStore, sessionToken, campaignId);
            if (!context.Succeeded)
                return ServiceResult<CampaignStateSnapshot>.Failure(context.Error!.Code, context.Error.Message);

            return ServiceResult<CampaignStateSnapshot>.Success(GameDtoMapper.ToCampaignStateSnapshot(stateStore, context.Value.Campaign.Id));
        }
    }

    private ServiceResult<RollResult> RecordRollLocked(UserAccount user, Campaign campaign, Character character, Guid skillId, RollVisibility visibility, (int Total, string Breakdown, IReadOnlyList<RollDieResult> Dice) roll, string canonicalExpression)
    {
        var entry = new RollLogEntry
        {
            Id = Guid.NewGuid(),
            CampaignId = campaign.Id,
            CharacterId = character.Id,
            SkillId = skillId,
            RollerUserId = user.Id,
            Visibility = visibility,
            Result = roll.Total,
            Breakdown = FormatLoggedBreakdown(skillId, canonicalExpression, roll.Breakdown),
            Dice = SerializeDice(roll.Dice),
            TimestampUtc = DateTimeOffset.UtcNow
        };

        stateStore.RollLog.Add(entry);
        stateStore.TouchLogLocked(campaign.Id);

        persistenceService.PersistStateLocked();
        return ServiceResult<RollResult>.Success(GameDtoMapper.ToRollResult(entry, roll.Dice));
    }

    private static string FormatLoggedBreakdown(Guid skillId, string canonicalExpression, string breakdown)
    {
        return skillId == CustomRollSkillId ? $"{canonicalExpression}{CustomRollBreakdownSeparator}{breakdown}" : breakdown;
    }

    private IEnumerable<RollLogEntry> GetVisibleCampaignLogEntriesLocked(UserAccount user, Campaign campaign)
    {
        return stateStore.RollLog.Where(r => r.CampaignId == campaign.Id).Where(r => r.Visibility == RollVisibility.Public || r.RollerUserId == user.Id || campaign.GmUserId == user.Id).OrderBy(r => r.TimestampUtc).ThenBy(r => r.Id);
    }

    private CampaignLogEntry ToLogEntry(RollLogEntry entry)
    {
        var dice = DeserializeDice(entry.Dice);
        var characterName = stateStore.CharactersById.TryGetValue(entry.CharacterId, out var character) ? character.Name : "Unknown character";
        var skillName = ResolveLoggedSkillName(entry);
        var rollerDisplayName = GameDtoMapper.ResolveOwnerDisplayName(stateStore, entry.RollerUserId, "Unknown owner");

        return GameDtoMapper.ToCampaignLogEntry(entry, characterName, skillName, rollerDisplayName, dice);
    }

    private CampaignLogListEntry ToLogListEntry(UserAccount user, Campaign campaign, RollLogEntry entry)
    {
        var dice = DeserializeDice(entry.Dice);
        var characterName = stateStore.CharactersById.TryGetValue(entry.CharacterId, out var character) ? character.Name : "Unknown character";
        var skillName = ResolveLoggedSkillName(entry);
        var loggedExpression = ResolveLoggedExpression(entry);
        var eventBadges = CampaignLogSummaryBuilder.BuildCompactLogEventBadges(campaign.Ruleset, loggedExpression, dice, entry.Breakdown);

        return GameDtoMapper.ToCampaignLogListEntry(entry, characterName, skillName, ResolveLogRollerLabel(user, campaign, entry), ResolveLogVisibilityLabel(user, campaign, entry), ResolveLogVisibilityStyle(user, campaign, entry), CampaignLogSummaryBuilder.BuildCompactLogSummary(dice, entry.Breakdown), eventBadges);
    }

    private static string SerializeDice(IReadOnlyList<RollDieResult> dice)
    {
        return JsonSerializer.Serialize(dice, DiceJsonOptions);
    }

    private string ResolveLoggedSkillName(RollLogEntry entry)
    {
        if (entry.SkillId == CustomRollSkillId)
            return CustomRollLabel;

        return stateStore.SkillsById.TryGetValue(entry.SkillId, out var skill) ? skill.Name : "Unknown skill";
    }

    private string? ResolveLoggedExpression(RollLogEntry entry)
    {
        if (entry.SkillId == CustomRollSkillId)
            return CampaignLogSummaryBuilder.ExtractCustomRollExpression(entry.Breakdown, CustomRollBreakdownSeparator);

        return stateStore.SkillsById.TryGetValue(entry.SkillId, out var skill) ? skill.DiceRollDefinition : null;
    }

    private string ResolveLogRollerLabel(UserAccount user, Campaign campaign, RollLogEntry entry)
    {
        if (entry.RollerUserId == user.Id)
            return "You";

        if (entry.RollerUserId == campaign.GmUserId)
            return "GM";

        return GameDtoMapper.ResolveOwnerDisplayName(stateStore, entry.RollerUserId, "Unknown owner");
    }

    private static string ResolveLogVisibilityLabel(UserAccount user, Campaign campaign, RollLogEntry entry)
    {
        if (entry.Visibility != RollVisibility.Private)
            return "Public";

        if (entry.RollerUserId == user.Id)
            return "Private (you)";

        return campaign.GmUserId == user.Id ? "Private (GM view)" : "Private";
    }

    private static string ResolveLogVisibilityStyle(UserAccount user, Campaign campaign, RollLogEntry entry)
    {
        if (entry.Visibility != RollVisibility.Private)
            return "public";

        if (entry.RollerUserId == user.Id)
            return "private-self";

        return campaign.GmUserId == user.Id ? "private-gm" : "private-generic";
    }

    private static IReadOnlyList<RollDieResult> DeserializeDice(string serializedDice)
    {
        if (string.IsNullOrWhiteSpace(serializedDice))
            return [];

        try
        {
            return JsonSerializer.Deserialize<IReadOnlyList<RollDieResult>>(serializedDice, DiceJsonOptions) ?? [];
        }
        catch (JsonException)
        {
            return [];
        }
    }

    private static int NormalizeCampaignLogPageSize(int? limit)
    {
        if (!limit.HasValue)
            return CampaignLogLivePageSize;

        return Math.Clamp(limit.Value, 1, CampaignLogLivePageSize);
    }

    private const int CampaignLogHistoryWindowSize = 100;
    private const int CampaignLogLivePageSize = 25;
    private const int MaxSituationalModifier = 1000;
    private const string CustomRollBreakdownSeparator = " => ";
    private const string CustomRollLabel = "Custom roll";
    private static readonly Guid CustomRollSkillId = Guid.Empty;
    private static readonly JsonSerializerOptions DiceJsonOptions = RpgRollerJson.CreateSerializerOptions();
    private readonly IDiceRoller m_DiceRoller = diceRoller;
    private readonly RollEngine m_RollEngine = new(new(diceRoller), new(diceRoller), new(diceRoller));
}