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

namespace SportsDivision.Application.Services;

public class ScoringService : IScoringService
{
    private readonly IUnitOfWork _uow;
    private readonly IMapper _mapper;

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

    // World Athletics scoring formula
    // Track: Points = A * (B - T)^C  where T = time in seconds
    // Field/Jump: Points = A * (P - B)^C  where P = performance
    public int CalculatePoints(decimal rawPerformance, decimal a, decimal b, decimal c, bool isTrack)
    {
        double diff = isTrack
            ? (double)(b - rawPerformance)
            : (double)(rawPerformance - b);

        if (diff <= 0) return 0;

        double points = (double)a * Math.Pow(diff, (double)c);
        return (int)Math.Floor(points);
    }

    public async Task<ScoreDto> RecordScoreAsync(ScoreCreateDto dto, string recordedBy)
    {
        var reg = await _uow.EventRegistrations.GetByIdAsync(dto.EventRegistrationId)
            ?? throw new KeyNotFoundException("Registration not found.");

        var existingScore = await _uow.Scores.GetByRegistrationAsync(dto.EventRegistrationId);
        if (existingScore != null)
        {
            existingScore.RawPerformance = dto.RawPerformance;
            existingScore.RecordedBy = recordedBy;
            existingScore.RecordedAt = DateTime.UtcNow;
            _uow.Scores.Update(existingScore);
            await _uow.SaveChangesAsync();
            return _mapper.Map<ScoreDto>(existingScore);
        }

        var score = new Score
        {
            EventRegistrationId = dto.EventRegistrationId,
            RawPerformance = dto.RawPerformance,
            RecordedBy = recordedBy,
            RecordedAt = DateTime.UtcNow
        };

        await _uow.Scores.AddAsync(score);
        await _uow.SaveChangesAsync();
        return _mapper.Map<ScoreDto>(score);
    }

    public async Task CalculateFinalScoresAsync(int tournamentEventLevelId, string recordedBy)
    {
        var tel = await _uow.TournamentEventLevels.GetWithRegistrationsAsync(tournamentEventLevelId)
            ?? throw new KeyNotFoundException("Tournament event level not found.");

        var scoringConstant = await _uow.ScoringConstants.GetByEventAsync(tel.EventId);
        if (scoringConstant == null) return;

        bool isTrack = tel.Event.Category == Domain.Enums.EventCategory.Track;

        foreach (var reg in tel.Registrations)
        {
            var score = await _uow.Scores.GetByRegistrationAsync(reg.EventRegistrationId);
            if (score == null || score.RawPerformance == 0) continue;

            score.CalculatedPoints = CalculatePoints(
                score.RawPerformance,
                scoringConstant.A,
                scoringConstant.B,
                scoringConstant.C,
                isTrack);
            score.RecordedBy = recordedBy;
            score.RecordedAt = DateTime.UtcNow;
            _uow.Scores.Update(score);
        }
        await _uow.SaveChangesAsync();
    }

    public async Task CalculateTrackFinalResultsAsync(int tournamentEventLevelId, string recordedBy)
    {
        var tel = await _uow.TournamentEventLevels.GetWithRegistrationsAsync(tournamentEventLevelId)
            ?? throw new KeyNotFoundException("Tournament event level not found.");
        if (tel.Event.Category != Domain.Enums.EventCategory.Track)
            throw new InvalidOperationException("Final standings can only be calculated for track events.");

        var rounds = (await _uow.Rounds.GetByTournamentEventLevelAsync(tournamentEventLevelId)).ToList();
        var finalRound = rounds.FirstOrDefault(r => r.RoundType == Domain.Enums.RoundType.Final)
            ?? rounds.OrderByDescending(r => r.RoundOrder).FirstOrDefault()
            ?? throw new InvalidOperationException("This event has no rounds yet — seed heats and record times first.");

        // Finishers in the final = lanes with a recorded time (DNS/DNF/DQ excluded), fastest first.
        var finishers = finalRound.Heats
            .SelectMany(h => h.HeatLanes)
            .Where(l => l.Time.HasValue && !l.IsDNS && !l.IsDNF && !l.IsDQ)
            .OrderBy(l => l.Time)
            .ToList();
        if (finishers.Count == 0)
            throw new InvalidOperationException("The final round has no recorded times yet.");

        var constant = await _uow.ScoringConstants.GetByEventAsync(tel.EventId);
        var placementPoints = (await _uow.PlacementPointConfigs.GetAllAsync())
            .ToDictionary(p => p.Placement, p => p.Points);
        var finalistRegIds = finishers.Select(f => f.EventRegistrationId).ToHashSet();

        // Points come from the final only: drop scores for anyone not in the final.
        var existing = (await _uow.Scores.GetByTournamentEventLevelAsync(tournamentEventLevelId)).ToList();
        foreach (var sc in existing.Where(s => !finalistRegIds.Contains(s.EventRegistrationId)))
            _uow.Scores.Remove(sc);

        for (int i = 0; i < finishers.Count; i++)
        {
            var lane = finishers[i];
            int place = i + 1;
            int calc = constant != null
                ? CalculatePoints(lane.Time!.Value, constant.A, constant.B, constant.C, isTrack: true)
                : 0;

            var score = await _uow.Scores.GetByRegistrationAsync(lane.EventRegistrationId);
            var isNew = score == null;
            score ??= new Score { EventRegistrationId = lane.EventRegistrationId };

            score.RawPerformance = lane.Time!.Value;
            score.CalculatedPoints = calc;
            score.Placement = place;
            score.PlacementPoints = placementPoints.TryGetValue(place, out var pts) ? pts : 0;
            score.RecordedBy = recordedBy;
            score.RecordedAt = DateTime.UtcNow;

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

    public async Task CalculatePlacementsAsync(int tournamentEventLevelId)
    {
        var scores = (await _uow.Scores.GetByTournamentEventLevelAsync(tournamentEventLevelId))
            .Where(s => s.RawPerformance > 0)
            .OrderByDescending(s => s.CalculatedPoints)
            .ToList();

        var placementPoints = (await _uow.PlacementPointConfigs.GetAllAsync())
            .ToDictionary(p => p.Placement, p => p.Points);

        for (int i = 0; i < scores.Count; i++)
        {
            scores[i].Placement = i + 1;
            scores[i].PlacementPoints = placementPoints.TryGetValue(i + 1, out var pts) ? pts : 0;
            _uow.Scores.Update(scores[i]);
        }
        await _uow.SaveChangesAsync();
    }

    public async Task<IEnumerable<SchoolPointsSummaryDto>> GetSchoolStandingsAsync(int tournamentId)
    {
        var tels = await _uow.TournamentEventLevels.GetByTournamentAsync(tournamentId);
        var schoolPoints = new Dictionary<int, SchoolPointsSummaryDto>();

        foreach (var tel in tels)
        {
            var regs = await _uow.EventRegistrations.GetByTournamentEventLevelAsync(tel.TournamentEventLevelId);
            foreach (var reg in regs)
            {
                if (reg.Student == null || reg.Score == null) continue;
                var schoolId = reg.Student.SchoolId;

                if (!schoolPoints.ContainsKey(schoolId))
                {
                    schoolPoints[schoolId] = new SchoolPointsSummaryDto
                    {
                        SchoolId = schoolId,
                        SchoolName = reg.Student.School?.Name ?? string.Empty,
                        ShortName = reg.Student.School?.ShortName
                    };
                }

                var summary = schoolPoints[schoolId];
                summary.TotalPoints += reg.Score.PlacementPoints;
                if (reg.Score.Placement == 1) summary.FirstPlaceCount++;
                else if (reg.Score.Placement == 2) summary.SecondPlaceCount++;
                else if (reg.Score.Placement == 3) summary.ThirdPlaceCount++;
            }
        }

        return schoolPoints.Values
            .OrderByDescending(s => s.TotalPoints)
            .ThenByDescending(s => s.FirstPlaceCount)
            .ThenByDescending(s => s.SecondPlaceCount)
            .ThenByDescending(s => s.ThirdPlaceCount);
    }

    public async Task<IEnumerable<ScoringConstantDto>> GetScoringConstantsAsync()
    {
        var constants = await _uow.ScoringConstants.GetAllAsync();
        return _mapper.Map<IEnumerable<ScoringConstantDto>>(constants);
    }

    public async Task UpdateScoringConstantAsync(ScoringConstantUpdateDto dto)
    {
        var constant = await _uow.ScoringConstants.GetByIdAsync(dto.ScoringConstantId)
            ?? throw new KeyNotFoundException("Scoring constant not found.");
        constant.A = dto.A;
        constant.B = dto.B;
        constant.C = dto.C;
        _uow.ScoringConstants.Update(constant);
        await _uow.SaveChangesAsync();
    }

    public async Task<IEnumerable<PlacementPointConfigDto>> GetPlacementPointConfigsAsync()
    {
        var configs = await _uow.PlacementPointConfigs.GetAllAsync();
        return _mapper.Map<IEnumerable<PlacementPointConfigDto>>(configs);
    }

    public async Task UpdatePlacementPointConfigAsync(PlacementPointConfigDto dto)
    {
        var config = await _uow.PlacementPointConfigs.GetByIdAsync(dto.PlacementPointConfigId)
            ?? throw new KeyNotFoundException("Placement point config not found.");
        config.Placement = dto.Placement;
        config.Points = dto.Points;
        _uow.PlacementPointConfigs.Update(config);
        await _uow.SaveChangesAsync();
    }
}
