using CalledIt.DataAccess;
using CalledIt.DataAccess.Entities;
using CalledIt.Domain.Dtos;
using Microsoft.EntityFrameworkCore;

namespace CalledIt.Domain.Services;

public class MatchService : IMatchService
{
    private readonly CalledItDbContext _dbContext;

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

    public async Task<MatchResponseDto> CreateAsync(CreateMatchDto dto)
    {
        var competition = await _dbContext.Competitions.FindAsync(dto.CompetitionId)
            ?? throw new KeyNotFoundException($"Competition with ID {dto.CompetitionId} not found.");

        var homeTeamExists = await _dbContext.Teams.AnyAsync(t => t.Id == dto.HomeTeamId);
        if (!homeTeamExists)
        {
            throw new KeyNotFoundException($"Team with ID {dto.HomeTeamId} not found.");
        }

        var awayTeamExists = await _dbContext.Teams.AnyAsync(t => t.Id == dto.AwayTeamId);
        if (!awayTeamExists)
        {
            throw new KeyNotFoundException($"Team with ID {dto.AwayTeamId} not found.");
        }

        if (dto.MatchDate < competition.StartDate || dto.MatchDate > competition.EndDate)
        {
            throw new ArgumentException("Match date must be within the competition's date range.");
        }

        var entity = new MatchEntity
        {
            CompetitionId = dto.CompetitionId,
            HomeTeamId = dto.HomeTeamId,
            AwayTeamId = dto.AwayTeamId,
            MatchDate = dto.MatchDate,
            Round = dto.Round
        };

        _dbContext.Matches.Add(entity);
        await _dbContext.SaveChangesAsync();

        return await GetByIdAsync(entity.Id);
    }

    public async Task<MatchResponseDto> GetByIdAsync(int id)
    {
        var result = await _dbContext.Matches
            .AsNoTracking()
            .Where(m => m.Id == id)
            .Select(m => new
            {
                m.Id, m.CompetitionId, m.MatchDate, m.Round, m.HomeScore, m.AwayScore,
                HomeTeamId = m.HomeTeam.Id,
                HomeTeamName = m.HomeTeam.Name,
                HomeTeamLogoFileName = m.HomeTeam.LogoFileName,
                AwayTeamId = m.AwayTeam.Id,
                AwayTeamName = m.AwayTeam.Name,
                AwayTeamLogoFileName = m.AwayTeam.LogoFileName,
            })
            .SingleOrDefaultAsync();

        if (result == null)
        {
            throw new KeyNotFoundException($"Match with ID {id} not found.");
        }

        return ToDto(result.Id, result.CompetitionId, result.HomeTeamId, result.HomeTeamName, result.HomeTeamLogoFileName,
            result.AwayTeamId, result.AwayTeamName, result.AwayTeamLogoFileName, result.MatchDate, result.Round,
            result.HomeScore, result.AwayScore);
    }

    public async Task<IReadOnlyList<MatchResponseDto>> GetByCompetitionIdAsync(int competitionId)
    {
        var items = await _dbContext.Matches
            .AsNoTracking()
            .Where(m => m.CompetitionId == competitionId)
            .OrderBy(m => m.MatchDate)
            .Select(m => new
            {
                m.Id, m.CompetitionId, m.MatchDate, m.Round, m.HomeScore, m.AwayScore,
                HomeTeamId = m.HomeTeam.Id,
                HomeTeamName = m.HomeTeam.Name,
                HomeTeamLogoFileName = m.HomeTeam.LogoFileName,
                AwayTeamId = m.AwayTeam.Id,
                AwayTeamName = m.AwayTeam.Name,
                AwayTeamLogoFileName = m.AwayTeam.LogoFileName,
            })
            .ToListAsync();

        return items.Select(m => ToDto(m.Id, m.CompetitionId, m.HomeTeamId, m.HomeTeamName, m.HomeTeamLogoFileName,
            m.AwayTeamId, m.AwayTeamName, m.AwayTeamLogoFileName, m.MatchDate, m.Round, m.HomeScore, m.AwayScore)).ToList();
    }

    public async Task<IReadOnlyList<MatchResponseDto>> GetPendingResultsAsync()
    {
        var cutoff = DateTime.UtcNow.AddMinutes(-90);
        var items = await _dbContext.Matches
            .AsNoTracking()
            .Where(m => m.MatchDate <= cutoff && m.HomeScore == null)
            .OrderBy(m => m.MatchDate)
            .Select(m => new
            {
                m.Id, m.CompetitionId, m.MatchDate, m.Round, m.HomeScore, m.AwayScore,
                HomeTeamId = m.HomeTeam.Id,
                HomeTeamName = m.HomeTeam.Name,
                HomeTeamLogoFileName = m.HomeTeam.LogoFileName,
                AwayTeamId = m.AwayTeam.Id,
                AwayTeamName = m.AwayTeam.Name,
                AwayTeamLogoFileName = m.AwayTeam.LogoFileName,
            })
            .ToListAsync();

        return items.Select(m => ToDto(m.Id, m.CompetitionId, m.HomeTeamId, m.HomeTeamName, m.HomeTeamLogoFileName,
            m.AwayTeamId, m.AwayTeamName, m.AwayTeamLogoFileName, m.MatchDate, m.Round, m.HomeScore, m.AwayScore)).ToList();
    }

    public async Task<MatchResponseDto> UpdateAsync(int id, UpdateMatchDto dto)
    {
        var entity = await _dbContext.Matches.FindAsync(id)
            ?? throw new KeyNotFoundException($"Match with ID {id} not found.");

        var competition = await _dbContext.Competitions.FindAsync(dto.CompetitionId)
            ?? throw new KeyNotFoundException($"Competition with ID {dto.CompetitionId} not found.");

        var homeTeamExists = await _dbContext.Teams.AnyAsync(t => t.Id == dto.HomeTeamId);
        if (!homeTeamExists)
        {
            throw new KeyNotFoundException($"Team with ID {dto.HomeTeamId} not found.");
        }

        var awayTeamExists = await _dbContext.Teams.AnyAsync(t => t.Id == dto.AwayTeamId);
        if (!awayTeamExists)
        {
            throw new KeyNotFoundException($"Team with ID {dto.AwayTeamId} not found.");
        }

        if (dto.MatchDate < competition.StartDate || dto.MatchDate > competition.EndDate)
        {
            throw new ArgumentException("Match date must be within the competition's date range.");
        }

        var identityChanged =
            entity.CompetitionId != dto.CompetitionId ||
            entity.HomeTeamId != dto.HomeTeamId ||
            entity.AwayTeamId != dto.AwayTeamId ||
            entity.MatchDate != dto.MatchDate;

        if (identityChanged)
        {
            var hasBets = await _dbContext.Bets.AnyAsync(b => b.MatchId == id);
            if (hasBets)
            {
                throw new InvalidOperationException(
                    "Cannot change competition, teams, or match date once bets have been placed.");
            }
        }

        entity.CompetitionId = dto.CompetitionId;
        entity.HomeTeamId = dto.HomeTeamId;
        entity.AwayTeamId = dto.AwayTeamId;
        entity.MatchDate = dto.MatchDate;
        entity.Round = dto.Round;
        entity.HomeScore = dto.HomeScore;
        entity.AwayScore = dto.AwayScore;

        await _dbContext.SaveChangesAsync();

        if (entity.HomeScore.HasValue && entity.AwayScore.HasValue)
        {
            var bets = await _dbContext.Bets
                .Where(b => b.MatchId == id)
                .ToListAsync();
            foreach (var bet in bets)
            {
                bet.Points = BetScoringCalculator.Calculate(
                    bet.HomeScore, bet.AwayScore,
                    entity.HomeScore.Value, entity.AwayScore.Value);
            }

            if (bets.Count > 0)
            {
                await _dbContext.SaveChangesAsync();
                await UpdateCompetitionStatsAsync(entity.CompetitionId, bets.Select(b => b.UserId).Distinct());
            }
        }

        return await GetByIdAsync(entity.Id);
    }

    public async Task DeleteAsync(int id)
    {
        var entity = await _dbContext.Matches.FindAsync(id)
            ?? throw new KeyNotFoundException($"Match with ID {id} not found.");

        _dbContext.Matches.Remove(entity);
        await _dbContext.SaveChangesAsync();
    }

    private async Task UpdateCompetitionStatsAsync(int competitionId, IEnumerable<int> userIds)
    {
        var userIdsList = userIds.ToList();
        if (userIdsList.Count == 0)
        {
            return;
        }

        var computedRows = await _dbContext.Bets
            .AsNoTracking()
            .Where(b => userIdsList.Contains(b.UserId) && b.Match.CompetitionId == competitionId)
            .GroupBy(b => b.UserId)
            .Select(g => new
            {
                UserId = g.Key,
                TotalPoints = g.Sum(b => b.Points ?? 0),
                BetsPlaced = g.Count(),
                BetsEvaluated = g.Count(b => b.Points != null),
            })
            .ToListAsync();

        if (computedRows.Count == 0)
        {
            return;
        }

        var affectedUserIds = computedRows.Select(r => r.UserId).ToList();

        var usernames = await _dbContext.Users
            .Where(u => affectedUserIds.Contains(u.Id))
            .ToDictionaryAsync(u => u.Id, u => u.Username);

        var existingStats = await _dbContext.UserCompetitionStats
            .Where(s => s.CompetitionId == competitionId && affectedUserIds.Contains(s.UserId))
            .ToDictionaryAsync(s => s.UserId);

        foreach (var row in computedRows)
        {
            if (!existingStats.TryGetValue(row.UserId, out var stats))
            {
                stats = new CalledIt.DataAccess.Entities.UserCompetitionStatsEntity
                {
                    UserId = row.UserId,
                    CompetitionId = competitionId,
                    Username = usernames.GetValueOrDefault(row.UserId, string.Empty),
                };
                _dbContext.UserCompetitionStats.Add(stats);
            }

            stats.TotalPoints = row.TotalPoints;
            stats.BetsPlaced = row.BetsPlaced;
            stats.BetsEvaluated = row.BetsEvaluated;
        }

        await _dbContext.SaveChangesAsync();
    }

    private static MatchResponseDto ToDto(
        int id, int competitionId,
        int homeTeamId, string homeTeamName, string? homeTeamLogoFileName,
        int awayTeamId, string awayTeamName, string? awayTeamLogoFileName,
        DateTime matchDate, string round, int? homeScore, int? awayScore) => new(
            id,
            competitionId,
            new TeamResponseDto(homeTeamId, homeTeamName, homeTeamLogoFileName != null ? $"/uploads/teams/{homeTeamLogoFileName}" : null),
            new TeamResponseDto(awayTeamId, awayTeamName, awayTeamLogoFileName != null ? $"/uploads/teams/{awayTeamLogoFileName}" : null),
            matchDate,
            round,
            homeScore,
            awayScore);
}
