using System.Diagnostics.CodeAnalysis;
using RpgRoller.Components.Pages.HomeControls;
using RpgRoller.Contracts;

namespace RpgRoller.Components.Pages;

[ExcludeFromCodeCoverage]
public sealed class WorkspacePlayCoordinator(
    WorkspaceState state,
    WorkspaceFeedbackService feedback,
    Func<bool> isPlayRoute,
    RpgRollerApiClient apiClient,
    WorkspaceQueryService workspaceQuery,
    Func<CharacterSummary, bool> canEditCharacter,
    Func<Task> requestRefreshAsync)
{
    public async Task RefreshCampaignLogAsync(Guid? afterRollId = null)
    {
        if (!state.SelectedCampaignId.HasValue || !isPlayRoute())
        {
            state.CampaignLog = [];
            state.CampaignLogCursor = null;
            ResetCampaignLogDetailState();
            return;
        }

        var previousLogCount = state.CampaignLog.Count;
        var page = await workspaceQuery.GetCampaignLogPageAsync(state.SelectedCampaignId.Value, afterRollId,
            CampaignLogWindowSize);
        Guid? newestRollId = null;
        if (!afterRollId.HasValue || page.ResetRequired)
            state.CampaignLog = page.Entries.ToList();
        else if (page.Entries.Length > 0)
        {
            state.CampaignLog.AddRange(page.Entries);
            if (state.CampaignLog.Count > CampaignLogWindowSize)
                state.CampaignLog = state.CampaignLog.TakeLast(CampaignLogWindowSize).ToList();
        }

        var shouldAutoExpandNewest = afterRollId.HasValue && page.Entries.Length > 0;
        if (!shouldAutoExpandNewest && !afterRollId.HasValue && state.CurrentCampaignState is not null &&
            previousLogCount == 0 && page.Entries.Length > 0)
            shouldAutoExpandNewest = true;

        if (shouldAutoExpandNewest)
        {
            newestRollId = page.Entries[^1].RollId;
            state.ExpandedCampaignLogRollId = newestRollId;
            state.FreshCampaignLogRollId = newestRollId;
        }
        else if (!afterRollId.HasValue)
            state.FreshCampaignLogRollId = null;

        state.CampaignLogCursor = page.Cursor ?? afterRollId;
        TrimCampaignLogDetails();

        if (newestRollId.HasValue)
            await EnsureRollDetailLoadedAsync(newestRollId.Value);
    }

    public async Task SelectCharacterAsync(Guid characterId)
    {
        state.SelectedCharacterId = characterId;
        await RefreshSelectedCharacterSheetAsync();
        await EnsureSelectedCharacterActiveAsync();
    }

    public async Task RefreshSelectedCharacterSheetAsync()
    {
        if (!state.SelectedCharacterId.HasValue || state.SelectedCampaign is null || !isPlayRoute())
        {
            state.SelectedCharacterSkills = [];
            state.SelectedCharacterSkillGroups = [];
            return;
        }

        var sheet = await workspaceQuery.GetCharacterSheetAsync(state.SelectedCharacterId.Value);
        state.SelectedCharacterSkillGroups =
            sheet.SkillGroups.OrderBy(group => group.Name, StringComparer.OrdinalIgnoreCase).ToList();
        state.SelectedCharacterSkills =
            sheet.Skills.OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase).ToList();
    }

    public Task EnsureSelectedCharacterActiveAsync()
    {
        return EnsureSelectedCharacterActiveCoreAsync();
    }

    public async Task ToggleRollDetailAsync(Guid rollId)
    {
        if (state.ExpandedCampaignLogRollId == rollId)
        {
            state.ExpandedCampaignLogRollId = null;
            if (state.FreshCampaignLogRollId == rollId)
                state.FreshCampaignLogRollId = null;
            return;
        }

        state.ExpandedCampaignLogRollId = rollId;
        state.FreshCampaignLogRollId = null;
        await EnsureRollDetailLoadedAsync(rollId);
    }

    public async Task OnSkillCreatedAsync(Guid _)
    {
        await RefreshSelectedCharacterSheetAsync();
        ResetCampaignStateTracking();
        feedback.SetStatus("Skill created.", false);
    }

    public async Task OnSkillUpdatedAsync(Guid _)
    {
        await RefreshSelectedCharacterSheetAsync();
        ResetCampaignStateTracking();
        feedback.SetStatus("Skill updated.", false);
    }

    public async Task OnSkillGroupCreatedAsync(Guid _)
    {
        await RefreshSelectedCharacterSheetAsync();
        ResetCampaignStateTracking();
        feedback.SetStatus("Skill group created.", false);
    }

    public async Task OnSkillGroupUpdatedAsync(Guid _)
    {
        await RefreshSelectedCharacterSheetAsync();
        ResetCampaignStateTracking();
        feedback.SetStatus("Skill group updated.", false);
    }

    public async Task OnSkillDeletedAsync(Guid _)
    {
        await RefreshSelectedCharacterSheetAsync();
        ResetCampaignStateTracking();
        feedback.SetStatus("Skill deleted.", false);
    }

    public async Task OnSkillGroupDeletedAsync(Guid _)
    {
        await RefreshSelectedCharacterSheetAsync();
        ResetCampaignStateTracking();
        feedback.SetStatus("Skill group deleted.", false);
    }

    public Task OnCharacterPanelErrorAsync(string message)
    {
        feedback.SetStatus(message, true);
        return Task.CompletedTask;
    }

    public Task OnCampaignLogPanelErrorAsync(string message)
    {
        feedback.SetStatus(message, true);
        return Task.CompletedTask;
    }

    public Task RollSkillAsync(CharacterSheetSkill skill)
    {
        if (state.SelectedCampaign is null)
        {
            feedback.SetStatus("No campaign selected.", true);
            return Task.CompletedTask;
        }

        if (string.Equals(state.SelectedCampaign.RulesetId, RulesetFormHelpers.RulesetIds.Rolemaster,
                StringComparison.OrdinalIgnoreCase))
        {
            OpenRolemasterSkillRollModal(skill);
            return Task.CompletedTask;
        }

        return ExecuteSkillRollAsync(skill.Id, 0);
    }

    public async Task SubmitRolemasterSkillRollAsync(string situationalModifierText)
    {
        if (state.PendingRolemasterSkillRoll is null)
            return;

        if (!TryParseSituationalModifier(situationalModifierText, out var situationalModifier, out var errorMessage))
        {
            state.PendingRolemasterSkillRollError = errorMessage;
            return;
        }

        state.PendingRolemasterSituationalModifier = situationalModifierText;
        state.PendingRolemasterSkillRollError = null;
        state.IsSubmittingRolemasterSkillRoll = true;
        try
        {
            await ExecuteSkillRollAsync(state.PendingRolemasterSkillRoll.Id, situationalModifier,
                keepModalOpenOnError: true);
        }
        finally
        {
            state.IsSubmittingRolemasterSkillRoll = false;
        }
    }

    public Task CancelRolemasterSkillRollAsync()
    {
        if (state.IsSubmittingRolemasterSkillRoll)
            return Task.CompletedTask;

        CloseRolemasterSkillRollModal();
        return Task.CompletedTask;
    }

    private async Task ExecuteSkillRollAsync(Guid skillId, int situationalModifier, bool keepModalOpenOnError = false)
    {
        state.IsMutating = true;
        try
        {
            var roll = await apiClient.RequestAsync<RollResult>("POST", $"/api/skills/{skillId}/roll",
                new RollSkillRequest(state.RollVisibility, situationalModifier));
            CloseRolemasterSkillRollModal();
            await HandleRecordedRollAsync(roll);
        }
        catch (ApiRequestException ex)
        {
            if (keepModalOpenOnError)
                state.PendingRolemasterSkillRollError = ex.Message;
            else
                feedback.SetStatus(ex.Message, true);
        }
        finally
        {
            state.IsMutating = false;
        }
    }

    public async Task OnCustomRollCreatedAsync(RollResult roll)
    {
        state.IsMutating = true;
        try
        {
            await HandleRecordedRollAsync(roll);
        }
        finally
        {
            state.IsMutating = false;
        }
    }

    public bool CanEditSkill(CharacterSheetSkill skill)
    {
        if (state.SelectedCharacter is null)
            return false;

        return canEditCharacter(state.SelectedCharacter);
    }

    public CampaignRollDetail? ResolveRollDetail(Guid rollId)
    {
        return state.CampaignLogDetails.GetValueOrDefault(rollId);
    }

    public bool IsRollDetailLoading(Guid rollId)
    {
        return state.CampaignLogDetailsLoading.Contains(rollId);
    }

    public string? GetRollDetailError(Guid rollId)
    {
        return state.CampaignLogDetailErrors.GetValueOrDefault(rollId);
    }

    public void ResetCampaignLogDetailState()
    {
        state.ExpandedCampaignLogRollId = null;
        state.FreshCampaignLogRollId = null;
        state.CampaignLogDetails.Clear();
        state.CampaignLogDetailsLoading.Clear();
        state.CampaignLogDetailErrors.Clear();
    }

    public void ResetCampaignStateTracking()
    {
        state.CurrentCampaignState = null;
    }

    private void OpenRolemasterSkillRollModal(CharacterSheetSkill skill)
    {
        state.PendingRolemasterSkillRoll = skill;
        state.PendingRolemasterSituationalModifier = string.Empty;
        state.PendingRolemasterSkillRollError = null;
        state.ShowRolemasterSkillRollModal = true;
    }

    private void CloseRolemasterSkillRollModal()
    {
        state.ShowRolemasterSkillRollModal = false;
        state.PendingRolemasterSkillRoll = null;
        state.PendingRolemasterSituationalModifier = string.Empty;
        state.PendingRolemasterSkillRollError = null;
        state.IsSubmittingRolemasterSkillRoll = false;
    }

    private static bool TryParseSituationalModifier(string? text, out int situationalModifier, out string? errorMessage)
    {
        if (string.IsNullOrWhiteSpace(text))
        {
            situationalModifier = 0;
            errorMessage = null;
            return true;
        }

        if (!int.TryParse(text.Trim(), out situationalModifier))
        {
            errorMessage = "Enter a whole number like 20, -15, or leave blank for 0.";
            return false;
        }

        if (situationalModifier is < -MaxSituationalModifier or > MaxSituationalModifier)
        {
            errorMessage = $"Enter a whole number between {-MaxSituationalModifier} and {MaxSituationalModifier}.";
            return false;
        }

        errorMessage = null;
        return true;
    }

    private async Task EnsureSelectedCharacterActiveCoreAsync()
    {
        if (!state.SelectedCharacterId.HasValue || state.SelectedCampaign is null)
            return;

        var character = state.SelectedCampaign.Characters.FirstOrDefault(c => c.Id == state.SelectedCharacterId.Value);
        if (character is null || !CanActivateCharacter(character) ||
            state.ActiveCharacterId == character.Id)
            return;

        try
        {
            await apiClient.RequestWithoutPayloadAsync("POST", $"/api/characters/{character.Id}/activate");
            state.ActiveCharacterId = character.Id;
        }
        catch (ApiRequestException ex)
        {
            feedback.SetStatus(ex.Message, true);
        }
    }

    private async Task HandleRecordedRollAsync(RollResult roll)
    {
        state.LastRoll = roll;
        state.CampaignLogDetails[roll.RollId] = ToCampaignRollDetail(roll);
        state.CampaignLogDetailErrors.Remove(roll.RollId);

        await RefreshCampaignLogAsync(state.CampaignLogCursor);
        PromoteFreshRoll(roll.RollId);
        ResetCampaignStateTracking();
        feedback.SetStatus("Roll recorded.", false);
        feedback.Announce("Roll result updated.");
    }

    private void TrimCampaignLogDetails()
    {
        var visibleRollIds = state.CampaignLog.Select(entry => entry.RollId).ToHashSet();

        foreach (var rollId in state.CampaignLogDetails.Keys.Where(rollId => !visibleRollIds.Contains(rollId))
                     .ToArray())
            state.CampaignLogDetails.Remove(rollId);

        foreach (var rollId in state.CampaignLogDetailsLoading.Where(rollId => !visibleRollIds.Contains(rollId))
                     .ToArray())
            state.CampaignLogDetailsLoading.Remove(rollId);

        foreach (var rollId in state.CampaignLogDetailErrors.Keys.Where(rollId => !visibleRollIds.Contains(rollId))
                     .ToArray())
            state.CampaignLogDetailErrors.Remove(rollId);

        if (state.ExpandedCampaignLogRollId.HasValue && !visibleRollIds.Contains(state.ExpandedCampaignLogRollId.Value))
            state.ExpandedCampaignLogRollId = null;

        if (state.FreshCampaignLogRollId.HasValue && !visibleRollIds.Contains(state.FreshCampaignLogRollId.Value))
            state.FreshCampaignLogRollId = null;
    }

    private async Task EnsureRollDetailLoadedAsync(Guid rollId)
    {
        state.CampaignLogDetailErrors.Remove(rollId);
        if (state.CampaignLogDetails.ContainsKey(rollId) || state.CampaignLogDetailsLoading.Contains(rollId))
            return;

        state.CampaignLogDetailsLoading.Add(rollId);
        try
        {
            state.CampaignLogDetails[rollId] = await workspaceQuery.GetRollDetailAsync(rollId);
        }
        catch (ApiRequestException ex)
        {
            state.CampaignLogDetailErrors[rollId] = ex.Message;
        }
        finally
        {
            state.CampaignLogDetailsLoading.Remove(rollId);
            await requestRefreshAsync();
        }
    }

    private void PromoteFreshRoll(Guid rollId)
    {
        if (!state.CampaignLog.Any(entry => entry.RollId == rollId))
            return;

        state.ExpandedCampaignLogRollId = rollId;
        state.FreshCampaignLogRollId = rollId;
    }

    private bool CanActivateCharacter(CharacterSummary character)
    {
        return state.User is not null &&
               (character.OwnerUserId == state.User.Id || state.IsCurrentUserGm);
    }

    private static CampaignRollDetail ToCampaignRollDetail(RollResult roll)
    {
        return new(roll.RollId, roll.Breakdown, roll.Dice.ToArray());
    }

    private const int CampaignLogWindowSize = 25;
    private const int MaxSituationalModifier = 1000;
}