using GameList.Contracts;
using GameList.Data;
using GameList.Domain;
using Microsoft.EntityFrameworkCore;

namespace GameList.Endpoints;

internal sealed class AdminWorkflowService(AppDbContext db)
{
    public async Task<IResult> SetResultsOpenAsync(ResultsOpenRequest request)
    {
        var state = await db.AppState.FirstAsync();
        state.ResultsOpen = request.ResultsOpen;
        state.UpdatedAt = DateTimeOffset.UtcNow;

        await using var tx = await db.Database.BeginTransactionAsync();

        if (request.ResultsOpen)
        {
            await db.Players.ExecuteUpdateAsync(p => p.SetProperty(x => x.CurrentPhase, Phase.Results));
        }
        else
        {
            await db.Players.ExecuteUpdateAsync(p => p.SetProperty(x => x.CurrentPhase, Phase.Vote).SetProperty(x => x.VotesFinal, false));
        }

        await db.SaveChangesAsync();
        await tx.CommitAsync();
        var currentState = await db.AppState.AsNoTracking().FirstAsync();
        return Results.Ok(new AdminResultsStateResponse(currentState.ResultsOpen, currentState.UpdatedAt));
    }

    public async Task<IResult> GetVoteStatusAsync()
    {
        var voters = await db.Players
            .AsNoTracking()
            .Include(p => p.Suggestions)
            .OrderBy(p => p.DisplayName ?? p.Username)
            .Select(p => new VoteStatusDto(p.Id, p.DisplayName ?? p.Username, p.Username, p.CurrentPhase, p.VotesFinal, p.HasJoker, p.Suggestions.Count, p.Suggestions.Select(s => s.Name).ToList()))
            .ToListAsync();

        var waiting = voters.Where(v => !v.Finalized).Select(v => v.Name).ToList();
        var ready = waiting.Count == 0;
        return Results.Ok(new VoteStatusResponse(voters, ready, waiting));
    }

    public async Task<IResult> GrantJokerAsync(GrantJokerRequest request)
    {
        var player = await db.Players.FirstOrDefaultAsync(p => p.Id == request.PlayerId);
        if (player is null)
            return EndpointHelpers.NotFoundError("Player not found.");

        var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, player.Id);
        if (phase != Phase.Vote)
            return EndpointHelpers.BadRequestError("Player must be in the Vote phase to receive a joker.");

        player.HasJoker = true;
        player.VotesFinal = false;
        await db.SaveChangesAsync();

        return Results.Ok(new AdminGrantJokerResponse(player.Id, player.HasJoker));
    }

    public async Task<IResult> DeletePlayerAsync(Guid playerId)
    {
        var player = await db.Players.Include(p => p.Suggestions).FirstOrDefaultAsync(p => p.Id == playerId);
        if (player is null)
            return EndpointHelpers.NotFoundError("Player not found.");

        await using var tx = await db.Database.BeginTransactionAsync();

        await db.Votes.Where(v => v.PlayerId == playerId).ExecuteDeleteAsync();

        var suggestionIds = player.Suggestions.Select(s => s.Id).ToList();
        if (suggestionIds.Count > 0)
        {
            await db.Suggestions
                .Where(s => s.ParentSuggestionId != null && suggestionIds.Contains(s.ParentSuggestionId.Value))
                .ExecuteUpdateAsync(s => s.SetProperty(x => x.ParentSuggestionId, (int?)null));

            await db.Votes.Where(v => suggestionIds.Contains(v.SuggestionId)).ExecuteDeleteAsync();
        }

        db.Players.Remove(player);
        await db.SaveChangesAsync();
        await tx.CommitAsync();

        return Results.Ok(new AdminDeletePlayerResponse(playerId));
    }

    public async Task<IResult> LinkSuggestionsAsync(Player adminPlayer, LinkSuggestionsRequest request)
    {
        var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, adminPlayer.Id);
        if (phase != Phase.Vote)
            return EndpointHelpers.PhaseMismatch(Phase.Vote, phase);

        if (request.SourceSuggestionId == request.TargetSuggestionId)
            return EndpointHelpers.BadRequestError("Pick two different games to link.");

        var suggestions = await db.Suggestions.ToListAsync();
        var source = suggestions.FirstOrDefault(s => s.Id == request.SourceSuggestionId);
        var target = suggestions.FirstOrDefault(s => s.Id == request.TargetSuggestionId);
        if (source is null || target is null)
            return EndpointHelpers.NotFoundError("Suggestion not found.");

        var rootIndex = EndpointHelpers.BuildLinkRoots(suggestions.Select(s => (s.Id, s.ParentSuggestionId)));
        if (!rootIndex.TryGetValue(source.Id, out var sourceRoot) || !rootIndex.TryGetValue(target.Id, out var targetRoot))
            return EndpointHelpers.NotFoundError("Suggestion not found.");

        if (sourceRoot == targetRoot)
            return EndpointHelpers.BadRequestError("These games are already linked.");

        var affectedRootIds = new HashSet<int>
        {
            sourceRoot,
            targetRoot
        };
        var affectedIds = rootIndex.Where(kv => affectedRootIds.Contains(kv.Value)).Select(kv => kv.Key).ToList();

        await using var tx = await db.Database.BeginTransactionAsync();

        foreach (var suggestion in suggestions)
        {
            var root = rootIndex.GetValueOrDefault(suggestion.Id);
            if (root == targetRoot)
            {
                suggestion.ParentSuggestionId = suggestion.Id == targetRoot ? null : targetRoot;
            }
            else if (root == sourceRoot)
            {
                suggestion.ParentSuggestionId = targetRoot;
            }
        }

        await db.SaveChangesAsync();

        await db.Votes.Where(v => affectedIds.Contains(v.SuggestionId)).ExecuteDeleteAsync();

        await db.Players.ExecuteUpdateAsync(p => p.SetProperty(x => x.VotesFinal, false));

        await tx.CommitAsync();

        return Results.Ok(new AdminLinkSuggestionsResponse(targetRoot, affectedIds, await db.Players.CountAsync()));
    }

    public async Task<IResult> UnlinkSuggestionsAsync(Player adminPlayer, UnlinkSuggestionsRequest request)
    {
        var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, adminPlayer.Id);
        if (phase != Phase.Vote)
            return EndpointHelpers.PhaseMismatch(Phase.Vote, phase);

        var suggestions = await db.Suggestions.ToListAsync();
        var target = suggestions.FirstOrDefault(s => s.Id == request.SuggestionId);
        if (target is null)
            return Results.Ok(new AdminUnlinkSuggestionsResponse(Array.Empty<int>(), 0));

        var rootIndex = EndpointHelpers.BuildLinkRoots(suggestions.Select(s => (s.Id, s.ParentSuggestionId)));
        if (!rootIndex.TryGetValue(target.Id, out var rootId))
            return Results.Ok(new AdminUnlinkSuggestionsResponse(Array.Empty<int>(), 0));

        var groupIds = rootIndex.Where(kv => kv.Value == rootId).Select(kv => kv.Key).ToList();

        await using var tx = await db.Database.BeginTransactionAsync();

        foreach (var suggestion in suggestions.Where(s => groupIds.Contains(s.Id)))
        {
            suggestion.ParentSuggestionId = null;
        }

        await db.SaveChangesAsync();

        await db.Votes.Where(v => groupIds.Contains(v.SuggestionId)).ExecuteDeleteAsync();

        await db.Players.ExecuteUpdateAsync(p => p.SetProperty(x => x.VotesFinal, false));

        await tx.CommitAsync();

        return Results.Ok(new AdminUnlinkSuggestionsResponse(groupIds, await db.Players.CountAsync()));
    }

    public async Task<IResult> ResetAsync()
    {
        await using var tx = await db.Database.BeginTransactionAsync();

        await db.Votes.ExecuteDeleteAsync();
        await db.Suggestions.ExecuteDeleteAsync();

        await db.Players.ExecuteUpdateAsync(p => p.SetProperty(x => x.CurrentPhase, Phase.Suggest).SetProperty(x => x.VotesFinal, false).SetProperty(x => x.HasJoker, false));
        var state = await db.AppState.FirstAsync();
        state.ResultsOpen = false;
        state.UpdatedAt = DateTimeOffset.UtcNow;
        await db.SaveChangesAsync();
        await tx.CommitAsync();

        return Results.Ok(new AdminResetStateResponse(Phase.Suggest, state.ResultsOpen, state.UpdatedAt));
    }

    public async Task<IResult> FactoryResetAsync()
    {
        await using var tx = await db.Database.BeginTransactionAsync();

        await db.Votes.ExecuteDeleteAsync();
        await db.Suggestions.ExecuteDeleteAsync();
        await db.Players.ExecuteDeleteAsync();
        await db.AppState.ExecuteDeleteAsync();

        var fresh = EndpointHelpers.NewAppState();
        db.AppState.Add(fresh);
        await db.SaveChangesAsync();

        await tx.CommitAsync();

        return Results.Ok(new AdminResetStateResponse(Phase.Suggest, fresh.ResultsOpen, fresh.UpdatedAt));
    }
}
