using Jiaowu.Api.Controllers;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;

namespace Jiaowu.Api.Infrastructure.Scheduling;

public sealed class WarningCheckWorker(IServiceScopeFactory scopeFactory, ILogger<WarningCheckWorker> logger) : BackgroundService
{
    private static readonly TimeZoneInfo ChinaTimeZone = ResolveChinaTimeZone();

    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            try
            {
                await Task.Delay(TimeSpan.FromMinutes(1), ct);
                await using var scope = scopeFactory.CreateAsyncScope();
                var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
                await RunAsync(db, ct);
            }
            catch (OperationCanceledException) { break; }
            catch (Exception ex) { logger.LogError(ex, "Warning check error"); }
        }
    }

    private static async Task RunAsync(AppDbContext db, CancellationToken ct)
    {
        var nowUtc = DateTime.UtcNow;
        var chinaNow = TimeZoneInfo.ConvertTime(
            new DateTimeOffset(nowUtc),
            ChinaTimeZone);
        var dow = (int)chinaNow.DayOfWeek == 0 ? 7 : (int)chinaNow.DayOfWeek;
        var currentMinuteUtc = new DateTime(
            nowUtc.Year,
            nowUtc.Month,
            nowUtc.Day,
            nowUtc.Hour,
            nowUtc.Minute,
            0,
            DateTimeKind.Utc);

        var rules = await db.WarningRules
            .Where(r => r.IsEnabled && r.AutoCheckEnabled &&
                        r.CheckHour == chinaNow.Hour && r.CheckMinute == chinaNow.Minute &&
                        (r.CheckDayOfWeek == null || r.CheckDayOfWeek == dow))
            .ToListAsync(ct);

        foreach (var rule in rules)
        {
            if (rule.LastCheckAt.HasValue && rule.LastCheckAt.Value >= currentMinuteUtc)
                continue;

            rule.LastCheckAt = nowUtc;
            await db.SaveChangesAsync(ct);

            var studentIds = await db.Students.Where(x => x.Status == StudentStatus.Active).Select(x => x.Id).ToListAsync(ct);
            var generated = new List<WarningRecord>();

            foreach (var sid in studentIds)
            {
                if (await db.WarningRecords.AnyAsync(x => x.StudentId == sid && x.AcademicTermId == rule.AcademicTermId && x.Type == rule.Type, ct))
                    continue;
                var w = rule.Type switch
                {
                    WarningType.FailedCredits => await CountWarning(db, rule, sid, rule.AcademicTermId,
                        sid => db.GradeRecords.CountAsync(x => x.StudentId == sid && x.GradeSheet!.TeachingTask!.AcademicTermId == rule.AcademicTermId && x.GradeSheet.Status == GradeSheetStatus.Published && x.TotalScore < 60, ct),
                        n => $"不及格课程 {n} 门", ct),
                    WarningType.LowGPA => await GPACheck(db, rule, sid, rule.AcademicTermId, ct),
                    WarningType.Absenteeism => await CountWarning(db, rule, sid, rule.AcademicTermId,
                        sid => db.AttendanceRecords.CountAsync(x => x.StudentId == sid && x.AttendanceSheet!.Status == AttendanceSheetStatus.Submitted && x.AttendanceSheet.TeachingTask!.AcademicTermId == rule.AcademicTermId && (x.Status == AttendanceStatus.Absent || x.Status == AttendanceStatus.Late), ct),
                        n => $"缺勤/迟到 {n} 次", ct),
                    WarningType.GraduationDelay => await CountWarning(db, rule, sid, rule.AcademicTermId,
                        sid => db.GradeRecords.CountAsync(x => x.StudentId == sid && x.GradeSheet!.Status == GradeSheetStatus.Published && x.TotalScore < 60, ct),
                        n => $"累计不及格 {n} 门", ct),
                    _ => null
                };
                if (w is not null) generated.Add(w);
            }

            if (generated.Count > 0)
            {
                db.WarningRecords.AddRange(generated);
                await db.SaveChangesAsync(ct);
                foreach (var w in generated)
                {
                    var si = await db.Students.Where(x => x.Id == w.StudentId).Select(x => new { x.UserId, x.Name, x.AdministrativeClassId }).FirstAsync(ct);
                    if (rule.NotifyStudent && si.UserId.HasValue)
                        await NotificationService.SendAsync(db, si.UserId.Value, "学业预警", w.Detail, "/warnings", ct, NotificationCategory.Warning);
                    if (rule.NotifyCounselor)
                    {
                        var cid = await db.AdministrativeClasses.Where(c => c.Id == si.AdministrativeClassId && c.CounselorUserId != null).Select(c => c.CounselorUserId!.Value).FirstOrDefaultAsync(ct);
                        if (cid != default) await NotificationService.SendAsync(db, cid, "学生学业预警", $"{si.Name}：{w.Detail}", "/warnings", ct, NotificationCategory.Warning);
                    }
                }
            }
        }
    }

    private static async Task<WarningRecord?> CountWarning(AppDbContext db, WarningRule rule, Guid sid, Guid termId,
        Func<Guid, Task<int>> counter, Func<int, string> msg, CancellationToken ct)
    {
        var n = await counter(sid);
        return n >= rule.Threshold ? new WarningRecord { StudentId = sid, Type = rule.Type, TriggerValue = n, Detail = $"{msg(n)}，达到预警阈值 {rule.Threshold}。", AcademicTermId = termId } : null;
    }

    private static async Task<WarningRecord?> GPACheck(AppDbContext db, WarningRule rule, Guid sid, Guid termId, CancellationToken ct)
    {
        var grades = await db.GradeRecords.Where(x => x.StudentId == sid && x.GradeSheet!.TeachingTask!.AcademicTermId == termId && x.GradeSheet.Status == GradeSheetStatus.Published && x.GradePoint != null).ToListAsync(ct);
        if (grades.Count == 0) return null;
        var gpa = grades.Average(x => x.GradePoint!.Value);
        return gpa < rule.Threshold ? new WarningRecord { StudentId = sid, Type = WarningType.LowGPA, TriggerValue = Math.Round(gpa, 2), Detail = $"平均绩点 {gpa:F2}，低于预警阈值 {rule.Threshold}。", AcademicTermId = termId } : null;
    }

    private static TimeZoneInfo ResolveChinaTimeZone()
    {
        foreach (var id in new[] { "Asia/Shanghai", "China Standard Time" })
        {
            try
            {
                return TimeZoneInfo.FindSystemTimeZoneById(id);
            }
            catch (TimeZoneNotFoundException)
            {
            }
            catch (InvalidTimeZoneException)
            {
            }
        }

        throw new InvalidOperationException("服务器未提供中国标准时间时区定义。");
    }
}
