using AutoMapper;
using SportsDivision.Application.DTOs;
using SportsDivision.Application.Interfaces;
using SportsDivision.Domain.Entities;
using SportsDivision.Domain.Enums;
using SportsDivision.Domain.Interfaces;

namespace SportsDivision.Application.Services;

public class HighJumpService : IHighJumpService
{
    private readonly IUnitOfWork _uow;
    private readonly IMapper _mapper;

    public HighJumpService(IUnitOfWork uow, IMapper mapper)
    {
        _uow = uow;
        _mapper = mapper;
    }

    public async Task<IEnumerable<HighJumpHeightDto>> GetHeightsAsync(int tournamentEventLevelId)
    {
        var heights = await _uow.HighJumpHeights.GetByTournamentEventLevelAsync(tournamentEventLevelId);
        return _mapper.Map<IEnumerable<HighJumpHeightDto>>(heights);
    }

    public async Task<HighJumpHeightDto> AddHeightAsync(HighJumpHeightCreateDto dto)
    {
        var existingHeights = await _uow.HighJumpHeights.GetByTournamentEventLevelAsync(dto.TournamentEventLevelId);
        var maxOrder = existingHeights.Any() ? existingHeights.Max(h => h.SortOrder) : 0;

        var height = new HighJumpHeight
        {
            TournamentEventLevelId = dto.TournamentEventLevelId,
            Height = dto.Height,
            SortOrder = maxOrder + 1
        };

        await _uow.HighJumpHeights.AddAsync(height);
        await _uow.SaveChangesAsync();
        return _mapper.Map<HighJumpHeightDto>(height);
    }

    public async Task RemoveHeightAsync(int heightId)
    {
        var height = await _uow.HighJumpHeights.GetByIdAsync(heightId)
            ?? throw new KeyNotFoundException("Height not found.");
        _uow.HighJumpHeights.Remove(height);
        await _uow.SaveChangesAsync();
    }

    public async Task RecordAttemptAsync(HighJumpAttemptUpdateDto dto)
    {
        var height = await _uow.HighJumpHeights.GetWithAttemptsAsync(dto.HighJumpHeightId)
            ?? throw new KeyNotFoundException("Height not found.");

        var attempt = height.Attempts.FirstOrDefault(a => a.EventRegistrationId == dto.EventRegistrationId);
        if (attempt == null)
        {
            attempt = new HighJumpAttempt
            {
                HighJumpHeightId = dto.HighJumpHeightId,
                EventRegistrationId = dto.EventRegistrationId
            };
            height.Attempts.Add(attempt);
        }

        switch (dto.AttemptNumber)
        {
            case 1: attempt.Attempt1 = dto.Result; break;
            case 2: attempt.Attempt2 = dto.Result; break;
            case 3: attempt.Attempt3 = dto.Result; break;
            default: throw new ArgumentException("Attempt number must be 1, 2, or 3.");
        }

        await _uow.SaveChangesAsync();
    }

    public async Task<bool> IsEliminatedAsync(int tournamentEventLevelId, int eventRegistrationId)
    {
        var heights = await _uow.HighJumpHeights.GetByTournamentEventLevelAsync(tournamentEventLevelId);
        int consecutiveFails = 0;

        foreach (var height in heights.OrderBy(h => h.SortOrder))
        {
            var fullHeight = await _uow.HighJumpHeights.GetWithAttemptsAsync(height.HighJumpHeightId);
            if (fullHeight == null) continue;

            var attempt = fullHeight.Attempts.FirstOrDefault(a => a.EventRegistrationId == eventRegistrationId);
            if (attempt == null) continue;

            if (attempt.HasCleared)
                consecutiveFails = 0;
            else if (attempt.IsEliminated)
                consecutiveFails += 3;
            else
                consecutiveFails += attempt.FailCount;

            if (consecutiveFails >= 3)
                return true;
        }

        return false;
    }

    public async Task<string?> CalculateResultsAsync(int tournamentEventLevelId, string recordedBy)
    {
        var heights = (await _uow.HighJumpHeights.GetByTournamentEventLevelAsync(tournamentEventLevelId))
            .OrderBy(h => h.SortOrder).ToList();

        var tel = await _uow.TournamentEventLevels.GetWithRegistrationsAsync(tournamentEventLevelId)
            ?? throw new KeyNotFoundException("Tournament event level not found.");

        var results = new List<(int RegId, decimal HighestCleared, int TotalFails, int FailsAtHighest)>();

        foreach (var reg in tel.Registrations)
        {
            decimal highestCleared = 0;
            int totalFails = 0;
            int failsAtHighest = 0;

            foreach (var height in heights)
            {
                var fullHeight = await _uow.HighJumpHeights.GetWithAttemptsAsync(height.HighJumpHeightId);
                if (fullHeight == null) continue;

                var attempt = fullHeight.Attempts.FirstOrDefault(a => a.EventRegistrationId == reg.EventRegistrationId);
                if (attempt == null) continue;

                totalFails += attempt.FailCount;
                if (attempt.HasCleared)
                {
                    highestCleared = height.Height;
                    failsAtHighest = attempt.FailCount;
                }
            }

            if (highestCleared > 0)
                results.Add((reg.EventRegistrationId, highestCleared, totalFails, failsAtHighest));
        }

        // Rank by World Athletics high-jump countback:
        //   1. highest bar cleared (desc)
        //   2. fewest misses at that bar
        //   3. fewest total misses in the competition
        var sorted = results
            .OrderByDescending(r => r.HighestCleared)
            .ThenBy(r => r.FailsAtHighest)
            .ThenBy(r => r.TotalFails)
            .ToList();

        var placementPoints = (await _uow.PlacementPointConfigs.GetAllAsync())
            .ToDictionary(p => p.Placement, p => p.Points);
        var constant = await _uow.ScoringConstants.GetByEventAsync(tel.EventId);

        // Assign placements with proper tie handling. Athletes identical on all three
        // countback keys are a genuine tie: they share one placement (standard competition
        // ranking, e.g. 1, 2, 3, 3, 5) and share placement points — the points for the
        // positions the tie-group occupies are pooled and divided equally.
        static bool SameRank(
            (int RegId, decimal HighestCleared, int TotalFails, int FailsAtHighest) a,
            (int RegId, decimal HighestCleared, int TotalFails, int FailsAtHighest) b)
            => a.HighestCleared == b.HighestCleared
               && a.FailsAtHighest == b.FailsAtHighest
               && a.TotalFails == b.TotalFails;

        var placeByIndex = new int[sorted.Count];
        var pointsByIndex = new int[sorted.Count];
        var firstPlaceTie = new List<int>(); // RegIds sharing 1st place

        int i = 0;
        while (i < sorted.Count)
        {
            int start = i;
            int place = start + 1;
            while (i + 1 < sorted.Count && SameRank(sorted[i + 1], sorted[start])) i++;
            int size = i - start + 1;

            // Pool the points for the positions this tie-group occupies, then split equally.
            int pooled = 0;
            for (int p = place; p < place + size; p++)
                pooled += placementPoints.TryGetValue(p, out var pp) ? pp : 0;
            int shared = (int)Math.Round((double)pooled / size, MidpointRounding.AwayFromZero);

            for (int k = start; k <= i; k++)
            {
                placeByIndex[k] = place;
                pointsByIndex[k] = shared;
            }
            if (place == 1 && size > 1)
                firstPlaceTie.AddRange(sorted.GetRange(start, size).Select(r => r.RegId));
            i++;
        }

        for (int j = 0; j < sorted.Count; j++)
        {
            decimal cleared = sorted[j].HighestCleared;
            int calc = (constant != null && cleared > constant.B)
                ? (int)Math.Floor((double)constant.A * Math.Pow((double)(cleared - constant.B), (double)constant.C))
                : 0;

            var score = await _uow.Scores.GetByRegistrationAsync(sorted[j].RegId);
            var isNew = score == null;
            score ??= new Score { EventRegistrationId = sorted[j].RegId };

            score.RawPerformance = cleared;
            score.CalculatedPoints = calc;
            score.Placement = placeByIndex[j];
            score.PlacementPoints = pointsByIndex[j];
            score.RecordedBy = recordedBy;
            score.RecordedAt = DateTime.UtcNow;

            if (isNew) await _uow.Scores.AddAsync(score);
            else _uow.Scores.Update(score);
        }
        await _uow.SaveChangesAsync();

        // A tie for 1st cannot be settled by countback — World Athletics requires a jump-off.
        // Flag it so an official can arrange one; the tied athletes currently share 1st place.
        if (firstPlaceTie.Count > 1)
        {
            var names = tel.Registrations
                .Where(r => firstPlaceTie.Contains(r.EventRegistrationId))
                .Select(r => r.Student?.FullName ?? "Unknown")
                .OrderBy(n => n);
            return $"Tie for 1st place between {string.Join(" and ", names)}. "
                 + "Countback cannot separate them — a jump-off is required to decide the winner. "
                 + "They currently share 1st place.";
        }
        return null;
    }
}
