using System.Security.Cryptography;
using CalledIt.DataAccess;
using CalledIt.DataAccess.Entities;
using CalledIt.Domain.Dtos;
using CalledIt.Domain.Exceptions;
using Microsoft.EntityFrameworkCore;

namespace CalledIt.Domain.Services;

public class GroupService : IGroupService
{
    private readonly CalledItDbContext _dbContext;
    private const string InviteCodeChars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";

    public GroupService(CalledItDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<GroupDto> CreateAsync(int userId, CreateGroupDto dto)
    {
        var competitionExists = await _dbContext.Competitions
            .AnyAsync(c => c.Id == dto.CompetitionId);

        if (!competitionExists)
        {
            throw new KeyNotFoundException($"Competition {dto.CompetitionId} not found.");
        }

        var inviteCode = GenerateInviteCode();

        var group = new GroupEntity
        {
            Name = dto.Name,
            CompetitionId = dto.CompetitionId,
            CreatedByUserId = userId,
            InviteCode = inviteCode,
            CreatedAt = DateTime.UtcNow,
        };

        _dbContext.Groups.Add(group);
        await _dbContext.SaveChangesAsync();

        _dbContext.GroupMembers.Add(new GroupMemberEntity
        {
            GroupId = group.Id,
            UserId = userId,
            JoinedAt = DateTime.UtcNow,
        });
        await _dbContext.SaveChangesAsync();

        var competitionName = await _dbContext.Competitions
            .Where(c => c.Id == dto.CompetitionId)
            .Select(c => c.Name)
            .FirstAsync();

        return new GroupDto(group.Id, group.Name, group.CompetitionId, competitionName, group.InviteCode, 1, true);
    }

    public async Task<GroupDto> JoinByCodeAsync(int userId, string inviteCode)
    {
        var group = await _dbContext.Groups
            .AsNoTracking()
            .Include(g => g.Competition)
            .Include(g => g.Members)
            .FirstOrDefaultAsync(g => g.InviteCode == inviteCode)
            ?? throw new KeyNotFoundException("Group not found.");

        var alreadyMember = group.Members.Any(m => m.UserId == userId);
        if (alreadyMember)
        {
            throw new InvalidOperationException("You are already a member of this group.");
        }

        _dbContext.GroupMembers.Add(new GroupMemberEntity
        {
            GroupId = group.Id,
            UserId = userId,
            JoinedAt = DateTime.UtcNow,
        });
        await _dbContext.SaveChangesAsync();

        return new GroupDto(
            group.Id,
            group.Name,
            group.CompetitionId,
            group.Competition.Name,
            group.InviteCode,
            group.Members.Count + 1,
            group.CreatedByUserId == userId);
    }

    public async Task<IReadOnlyList<GroupDto>> GetByCompetitionAsync(int userId, int competitionId)
    {
        return await _dbContext.GroupMembers
            .AsNoTracking()
            .Where(m => m.UserId == userId && m.Group.CompetitionId == competitionId)
            .Select(m => new GroupDto(
                m.Group.Id,
                m.Group.Name,
                m.Group.CompetitionId,
                m.Group.Competition.Name,
                m.Group.InviteCode,
                m.Group.Members.Count,
                m.Group.CreatedByUserId == userId))
            .ToListAsync();
    }

    public async Task<IReadOnlyList<LeaderboardEntryDto>> GetLeaderboardAsync(int groupId, int userId)
    {
        var isMember = await _dbContext.GroupMembers
            .AnyAsync(m => m.GroupId == groupId && m.UserId == userId);

        if (!isMember)
        {
            throw new ForbiddenAccessException("You are not a member of this group.");
        }

        var group = await _dbContext.Groups
            .AsNoTracking()
            .FirstOrDefaultAsync(g => g.Id == groupId)
            ?? throw new KeyNotFoundException("Group not found.");

        var rows = await _dbContext.GroupMembers
            .AsNoTracking()
            .Where(m => m.GroupId == groupId)
            .Join(
                _dbContext.UserCompetitionStats.Where(s => s.CompetitionId == group.CompetitionId),
                m => m.UserId,
                s => s.UserId,
                (m, s) => new
                {
                    m.User.Username,
                    s.TotalPoints,
                    s.BetsPlaced,
                    s.BetsEvaluated,
                })
            .OrderByDescending(r => r.TotalPoints)
            .ThenBy(r => r.Username)
            .ToListAsync();

        return rows
            .Select((r, index) => new LeaderboardEntryDto(
                Rank: index + 1,
                Username: r.Username,
                TotalPoints: r.TotalPoints,
                BetsPlaced: r.BetsPlaced,
                BetsEvaluated: r.BetsEvaluated))
            .ToList();
    }

    public async Task LeaveAsync(int userId, int groupId)
    {
        var group = await _dbContext.Groups
            .AsNoTracking()
            .FirstOrDefaultAsync(g => g.Id == groupId)
            ?? throw new KeyNotFoundException("Group not found.");

        if (group.CreatedByUserId == userId)
        {
            throw new InvalidOperationException("Owner cannot leave — delete the group instead.");
        }

        var member = await _dbContext.GroupMembers
            .FirstOrDefaultAsync(m => m.GroupId == groupId && m.UserId == userId)
            ?? throw new KeyNotFoundException("You are not a member of this group.");

        _dbContext.GroupMembers.Remove(member);
        await _dbContext.SaveChangesAsync();
    }

    public async Task DeleteAsync(int userId, int groupId)
    {
        var group = await _dbContext.Groups
            .FirstOrDefaultAsync(g => g.Id == groupId)
            ?? throw new KeyNotFoundException("Group not found.");

        if (group.CreatedByUserId != userId)
        {
            throw new ForbiddenAccessException("Only the owner can delete the group.");
        }

        _dbContext.Groups.Remove(group);
        await _dbContext.SaveChangesAsync();
    }

    private static string GenerateInviteCode()
    {
        return new string(Enumerable.Range(0, 8)
            .Select(_ => InviteCodeChars[RandomNumberGenerator.GetInt32(InviteCodeChars.Length)])
            .ToArray());
    }
}
