using Microsoft.EntityFrameworkCore;
using ROLAC.API.Data;
using ROLAC.API.DTOs.MealAttendance;
using ROLAC.API.Entities;

namespace ROLAC.API.Services;

public class MealAttendanceService : IMealAttendanceService
{
    private readonly AppDbContext _db;

    public MealAttendanceService(AppDbContext db) => _db = db;

    // Server local time is assumed to match the church's local day.
    public DateOnly ServiceDay
    {
        get
        {
            var today = DateOnly.FromDateTime(DateTime.Now);
            return today.AddDays(-(int)today.DayOfWeek);
        }
    }

    public async Task<AttendanceCountsDto> GetOrCreateAsync(DateOnly date)
    {
        await EnsureRowAsync(date);
        return await ReadAsync(date);
    }

    public async Task<AttendanceCountsDto> IncrementAsync(DateOnly date, string category, int delta)
    {
        await EnsureRowAsync(date);

        var rows = _db.MealAttendances.Where(a => a.AttendanceDate == date);

        // Atomic, race-free server-side increment; never let a column go below zero.
        switch (category.Trim().ToLowerInvariant())
        {
            case "adult":
                await rows.ExecuteUpdateAsync(s => s.SetProperty(
                    a => a.AdultCount, a => a.AdultCount + delta < 0 ? 0 : a.AdultCount + delta));
                break;
            case "youth":
                await rows.ExecuteUpdateAsync(s => s.SetProperty(
                    a => a.YouthCount, a => a.YouthCount + delta < 0 ? 0 : a.YouthCount + delta));
                break;
            case "kid":
                await rows.ExecuteUpdateAsync(s => s.SetProperty(
                    a => a.KidCount, a => a.KidCount + delta < 0 ? 0 : a.KidCount + delta));
                break;
            default:
                throw new ArgumentException($"Unknown attendance category '{category}'.", nameof(category));
        }

        return await ReadAsync(date);
    }

    public async Task<AttendanceCountsDto> SetAsync(DateOnly date, string category, int value)
    {
        await EnsureRowAsync(date);

        // Counts can never be negative; clamp before writing.
        var clamped = value < 0 ? 0 : value;

        var rows = _db.MealAttendances.Where(a => a.AttendanceDate == date);

        switch (category.Trim().ToLowerInvariant())
        {
            case "adult":
                await rows.ExecuteUpdateAsync(s => s.SetProperty(a => a.AdultCount, clamped));
                break;
            case "youth":
                await rows.ExecuteUpdateAsync(s => s.SetProperty(a => a.YouthCount, clamped));
                break;
            case "kid":
                await rows.ExecuteUpdateAsync(s => s.SetProperty(a => a.KidCount, clamped));
                break;
            default:
                throw new ArgumentException($"Unknown attendance category '{category}'.", nameof(category));
        }

        return await ReadAsync(date);
    }

    public async Task<AttendanceCountsDto> SetCountsAsync(DateOnly date, int adult, int youth, int kid)
    {
        // Single-editor back-office path, so a tracked load + SaveChanges is fine here; no need for the
        // race-safe EnsureRowAsync + ExecuteUpdateAsync pattern, which the EF InMemory test provider can't run.
        var row = await _db.MealAttendances.FirstOrDefaultAsync(a => a.AttendanceDate == date);
        if (row is null)
        {
            row = new MealAttendance { AttendanceDate = date };
            _db.MealAttendances.Add(row);
        }

        // Counts can never be negative; clamp before writing.
        row.AdultCount = adult < 0 ? 0 : adult;
        row.YouthCount = youth < 0 ? 0 : youth;
        row.KidCount   = kid   < 0 ? 0 : kid;

        await _db.SaveChangesAsync();
        return ToDto(row);
    }

    public async Task<IReadOnlyList<AttendanceCountsDto>> GetRangeAsync(DateOnly from, DateOnly to)
    {
        var rows = await _db.MealAttendances.AsNoTracking()
            .Where(a => a.AttendanceDate >= from && a.AttendanceDate <= to)
            .OrderBy(a => a.AttendanceDate)
            .ToListAsync();

        return rows.Select(ToDto).ToList();
    }

    // ── helpers ──────────────────────────────────────────────────────────────

    // Insert a zeroed row if today's row doesn't exist yet. A concurrent insert
    // that wins the unique index throws DbUpdateException — harmless, the row exists.
    private async Task EnsureRowAsync(DateOnly date)
    {
        if (await _db.MealAttendances.AsNoTracking().AnyAsync(a => a.AttendanceDate == date))
            return;

        var row = new MealAttendance { AttendanceDate = date };
        _db.MealAttendances.Add(row);
        try
        {
            await _db.SaveChangesAsync();
        }
        catch (DbUpdateException)
        {
            _db.Entry(row).State = EntityState.Detached;
        }
    }

    private async Task<AttendanceCountsDto> ReadAsync(DateOnly date)
    {
        var row = await _db.MealAttendances.AsNoTracking()
            .FirstOrDefaultAsync(a => a.AttendanceDate == date);
        return row is null
            ? new AttendanceCountsDto { Date = date.ToString("yyyy-MM-dd") }
            : ToDto(row);
    }

    private static AttendanceCountsDto ToDto(MealAttendance row) => new()
    {
        Date  = row.AttendanceDate.ToString("yyyy-MM-dd"),
        Adult = row.AdultCount,
        Youth = row.YouthCount,
        Kid   = row.KidCount,
    };
}
