using System.ComponentModel.DataAnnotations;
using System.Data;
using System.Net;
using System.Text.RegularExpressions;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Auth;
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace Jiaowu.Api.Controllers;

[ApiController]
[Authorize]
[Route("api/notifications")]
public sealed class NotificationsController(
    AppDbContext db,
    ICurrentUserDataScope currentUserDataScope,
    ILogger<NotificationsController>? logger = null) : ControllerBase
{
    private const int MaximumSelectedRecipients = 500;
    private const int NotificationBatchSize = 300;
    private const string Senders =
        SystemRoles.SuperAdmin + "," +
        SystemRoles.AcademicAdmin + "," +
        SystemRoles.CollegeAdmin + "," +
        SystemRoles.Counselor + "," +
        SystemRoles.Teacher;

    [HttpGet]
    public async Task<ActionResult> GetNotifications(
        bool? unreadOnly,
        NotificationCategory? category,
        string? keyword,
        int page = 1,
        int pageSize = 20,
        CancellationToken cancellationToken = default)
    {
        page = Math.Max(1, page);
        pageSize = Math.Clamp(pageSize, 1, 100);
        var userId = currentUserDataScope.Current.UserId;
        var source = db.Notifications.AsNoTracking()
            .Where(x => x.UserId == userId);
        if (unreadOnly == true)
            source = source.Where(x => !x.IsRead);
        if (category.HasValue)
            source = source.Where(x => x.Category == category);
        if (!string.IsNullOrWhiteSpace(keyword))
        {
            var value = keyword.Trim();
            source = source.Where(x =>
                x.Title.Contains(value) ||
                x.Content.Contains(value));
        }

        var total = await source.CountAsync(cancellationToken);
        var items = await source
            .OrderByDescending(x => x.CreatedAt)
            .ThenByDescending(x => x.Id)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(x => new
            {
                x.Id,
                x.Title,
                x.Content,
                x.Category,
                x.IsRead,
                x.LinkUrl,
                SenderName = x.MessageDispatch == null
                    ? "教务系统"
                    : x.MessageDispatch.SenderName,
                AudienceName = x.MessageDispatch == null
                    ? null
                    : x.MessageDispatch.AudienceName,
                IsManual = x.MessageDispatchId != null,
                x.CreatedAt
            })
            .ToListAsync(cancellationToken);

        var unreadCount = await db.Notifications
            .CountAsync(x => x.UserId == userId && !x.IsRead, cancellationToken);
        var unreadByCategory = await db.Notifications.AsNoTracking()
            .Where(x => x.UserId == userId && !x.IsRead)
            .GroupBy(x => x.Category)
            .Select(x => new { Category = x.Key, Count = x.Count() })
            .ToListAsync(cancellationToken);

        return Ok(new
        {
            Items = items,
            Total = total,
            UnreadCount = unreadCount,
            UnreadByCategory = unreadByCategory
        });
    }

    [HttpGet("unread-count")]
    public async Task<ActionResult> GetUnreadCount(CancellationToken cancellationToken)
    {
        var userId = currentUserDataScope.Current.UserId;
        var count = await db.Notifications
            .CountAsync(x => x.UserId == userId && !x.IsRead, cancellationToken);
        return Ok(new { Count = count });
    }

    [HttpGet("composer")]
    [Authorize(Roles = Senders)]
    public async Task<ActionResult> GetComposer(CancellationToken cancellationToken)
    {
        var scope = currentUserDataScope.Current;
        if (IsSchoolAdministrator(scope))
        {
            return Ok(await BuildAdministratorComposerAsync(
                scope,
                null,
                MessageAudienceType.School,
                "全校已启用账号",
                cancellationToken));
        }

        if (scope.IsInRole(SystemRoles.CollegeAdmin) ||
            scope.IsInRole(SystemRoles.Counselor))
        {
            var collegeId = await ResolveSenderCollegeIdAsync(
                scope,
                cancellationToken);
            if (!collegeId.HasValue)
            {
                return ConflictProblem(
                    "当前账号未关联学院，暂时无法确定发信范围。");
            }

            var college = await db.Colleges.AsNoTracking()
                .Where(x => x.Id == collegeId.Value)
                .Select(x => x.Name)
                .FirstOrDefaultAsync(cancellationToken);
            if (college is null)
                return ConflictProblem("当前账号关联的学院不存在。");

            return Ok(await BuildAdministratorComposerAsync(
                scope,
                collegeId.Value,
                MessageAudienceType.College,
                $"{college}全院成员",
                cancellationToken));
        }

        if (scope.IsInRole(SystemRoles.Teacher))
        {
            var tasks = await db.TeachingTasks.AsNoTracking()
                .Where(task =>
                    task.Status != TeachingTaskStatus.Draft &&
                    task.Teachers.Any(item =>
                        item.Teacher!.UserId == scope.UserId))
                .OrderByDescending(task => task.AcademicTerm!.IsCurrent)
                .ThenByDescending(task => task.AcademicTerm!.StartDate)
                .ThenBy(task => task.TaskNumber)
                .Select(task => new
                {
                    task.Id,
                    task.TaskNumber,
                    CourseName = task.Course!.Name,
                    TermName = task.AcademicTerm!.Name,
                    RecipientCount = db.Students.Count(student =>
                        student.Status == StudentStatus.Active &&
                        student.UserId != null &&
                        (task.Classes.Any(item =>
                             item.AdministrativeClassId ==
                             student.AdministrativeClassId) ||
                         db.CourseEnrollments.Any(enrollment =>
                             enrollment.StudentId == student.Id &&
                             enrollment.Status ==
                             CourseEnrollmentStatus.Enrolled &&
                             enrollment.CourseSelectionOffering!
                                 .TeachingTaskId == task.Id)))
                })
                .ToListAsync(cancellationToken);
            return Ok(new
            {
                AudienceType = MessageAudienceType.TeachingTask,
                AudienceName = "所带教学班",
                RecipientCount = 0,
                TeachingTasks = tasks
            });
        }

        return Forbid();
    }

    [HttpGet("recipients")]
    [Authorize(Roles = Senders)]
    public async Task<ActionResult> GetRecipients(
        Guid? collegeId,
        string? role,
        Guid? administrativeClassId,
        Guid? teachingTaskId,
        string? keyword,
        int page = 1,
        int pageSize = 30,
        CancellationToken cancellationToken = default)
    {
        var scope = currentUserDataScope.Current;
        if (scope.IsInRole(SystemRoles.Teacher) &&
            !IsSchoolAdministrator(scope) &&
            !scope.IsInRole(SystemRoles.CollegeAdmin) &&
            !scope.IsInRole(SystemRoles.Counselor))
        {
            return Forbid();
        }

        var resolvedCollegeId = IsSchoolAdministrator(scope)
            ? null
            : await ResolveSenderCollegeIdAsync(scope, cancellationToken);
        if (!IsSchoolAdministrator(scope) && !resolvedCollegeId.HasValue)
            return ConflictProblem("当前账号未关联学院，暂时无法确定收件人范围。");

        var filter = new MessageRecipientFilter(
            collegeId,
            Normalize(role),
            administrativeClassId,
            teachingTaskId,
            Normalize(keyword));
        if (filter.Role is not null && !SystemRoles.All.Contains(filter.Role))
            return ValidationProblem("所选身份类型无效。");

        page = Math.Max(1, page);
        pageSize = Math.Clamp(pageSize, 10, 100);
        var source = ApplyRecipientFilter(
            ScopedRecipientQuery(scope, resolvedCollegeId),
            filter);
        var total = await source.CountAsync(cancellationToken);
        var users = await source
            .OrderBy(x => x.DisplayName)
            .ThenBy(x => x.UserName)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(user => new
            {
                user.Id,
                user.DisplayName,
                user.UserName,
                user.StaffNumber,
                CollegeName = db.Colleges
                    .Where(college => college.Id == user.CollegeId)
                    .Select(college => college.Name)
                    .FirstOrDefault(),
                StudentNumber = db.Students
                    .Where(student => student.UserId == user.Id)
                    .Select(student => student.StudentNumber)
                    .FirstOrDefault(),
                ClassName = db.Students
                    .Where(student => student.UserId == user.Id)
                    .Select(student => student.AdministrativeClass!.Name)
                    .FirstOrDefault(),
                TeacherNumber = db.Teachers
                    .Where(teacher => teacher.UserId == user.Id)
                    .Select(teacher => teacher.TeacherNumber)
                    .FirstOrDefault()
            })
            .ToListAsync(cancellationToken);
        var userIds = users.Select(x => x.Id).ToArray();
        var roleRows = await db.UserRoles.AsNoTracking()
            .Where(x => userIds.Contains(x.UserId))
            .Join(
                db.Roles.AsNoTracking(),
                userRole => userRole.RoleId,
                roleEntity => roleEntity.Id,
                (userRole, roleEntity) => new
                {
                    userRole.UserId,
                    RoleName = roleEntity.Name!
                })
            .ToListAsync(cancellationToken);
        var rolesByUser = roleRows
            .GroupBy(x => x.UserId)
            .ToDictionary(
                group => group.Key,
                group => group.Select(x => x.RoleName).Distinct().ToArray());

        return Ok(new
        {
            Items = users.Select(user => new
            {
                user.Id,
                user.DisplayName,
                user.UserName,
                Number = user.StudentNumber ?? user.TeacherNumber ?? user.StaffNumber,
                user.CollegeName,
                user.ClassName,
                Roles = rolesByUser.GetValueOrDefault(user.Id, [])
            }),
            Total = total,
            Page = page,
            PageSize = pageSize
        });
    }

    [HttpPost("send")]
    [Authorize(Roles = Senders)]
    public async Task<ActionResult> Send(
        SendMessageRequest request,
        CancellationToken cancellationToken)
    {
        var title = request.Title.Trim();
        var content = request.Content.Trim();
        if (title.Length == 0)
            return ValidationProblem("请填写消息标题。");
        if (PlainText(content).Length == 0)
            return ValidationProblem("请填写消息正文。");
        if (content.Length > 20000)
            return ValidationProblem("消息正文过长，请精简至 20000 个字符以内。");
        var scope = currentUserDataScope.Current;
        MessageAudienceType audienceType;
        Guid? audienceId = null;
        string audienceName;
        List<Guid> recipientIds;

        if (IsSchoolAdministrator(scope))
        {
            var result = await ResolveAdministratorRecipientsAsync(
                scope,
                null,
                request,
                "全校已启用账号",
                cancellationToken);
            if (result.Error is not null) return result.Error;
            audienceType = result.AudienceType;
            audienceName = result.AudienceName!;
            recipientIds = result.RecipientIds!;
        }
        else if (scope.IsInRole(SystemRoles.CollegeAdmin) ||
                 scope.IsInRole(SystemRoles.Counselor))
        {
            var collegeId = await ResolveSenderCollegeIdAsync(
                scope,
                cancellationToken);
            if (!collegeId.HasValue)
            {
                return ConflictProblem(
                    "当前账号未关联学院，暂时无法确定发信范围。");
            }

            var collegeName = await db.Colleges.AsNoTracking()
                .Where(x => x.Id == collegeId.Value)
                .Select(x => x.Name)
                .FirstOrDefaultAsync(cancellationToken);
            if (collegeName is null)
                return ConflictProblem("当前账号关联的学院不存在。");

            var result = await ResolveAdministratorRecipientsAsync(
                scope,
                collegeId.Value,
                request,
                $"{collegeName}全院成员",
                cancellationToken);
            if (result.Error is not null) return result.Error;
            audienceType = result.AudienceType;
            audienceId = audienceType == MessageAudienceType.College
                ? collegeId.Value
                : null;
            audienceName = result.AudienceName!;
            recipientIds = result.RecipientIds!;
        }
        else if (scope.IsInRole(SystemRoles.Teacher))
        {
            if (!request.TeachingTaskId.HasValue)
                return ValidationProblem("请选择接收消息的教学班。");

            var task = await db.TeachingTasks.AsNoTracking()
                .Where(x =>
                    x.Id == request.TeachingTaskId.Value &&
                    x.Status != TeachingTaskStatus.Draft &&
                    x.Teachers.Any(item =>
                        item.Teacher!.UserId == scope.UserId))
                .Select(x => new
                {
                    x.Id,
                    x.TaskNumber,
                    CourseName = x.Course!.Name
                })
                .FirstOrDefaultAsync(cancellationToken);
            if (task is null)
            {
                return ConflictProblem(
                    "只能向自己所带且已发布的教学班发信。");
            }

            audienceType = MessageAudienceType.TeachingTask;
            audienceId = task.Id;
            audienceName = $"{task.TaskNumber} · {task.CourseName}";
            recipientIds = await GetTeachingTaskRecipientIdsAsync(
                task.Id,
                cancellationToken);
        }
        else
        {
            return Forbid();
        }

        recipientIds = recipientIds.Distinct().ToList();
        if (recipientIds.Count == 0)
            return ConflictProblem("当前发信范围内没有可接收消息的已启用账号。");

        var dispatch = new MessageDispatch
        {
            SenderUserId = scope.UserId,
            SenderName = scope.DisplayName ?? "教务用户",
            Title = title,
            Content = content,
            Category = NotificationCategory.General,
            AudienceType = audienceType,
            AudienceId = audienceId,
            AudienceName = audienceName,
            RecipientCount = recipientIds.Count,
            LinkUrl = null
        };
        try
        {
            return await db.ExecuteInRetriableTransactionAsync<ActionResult>(
                async transaction =>
                {
                    db.MessageDispatches.Add(dispatch);
                    await db.SaveChangesAsync(cancellationToken);
                    db.ChangeTracker.Clear();

                    foreach (var batch in recipientIds.Chunk(NotificationBatchSize))
                    {
                        db.Notifications.AddRange(batch.Select(userId => new Notification
                        {
                            UserId = userId,
                            Title = title,
                            Content = content,
                            Category = NotificationCategory.General,
                            LinkUrl = null,
                            MessageDispatchId = dispatch.Id
                        }));
                        await db.SaveChangesAsync(cancellationToken);
                        db.ChangeTracker.Clear();
                    }

                    await transaction.CommitAsync(cancellationToken);
                    return Ok(new
                    {
                        dispatch.Id,
                        dispatch.AudienceName,
                        dispatch.RecipientCount,
                        dispatch.CreatedAt
                    });
                },
                cancellationToken,
                IsolationLevel.ReadCommitted);
        }
        catch (DbUpdateException exception)
        {
            logger?.LogError(
                exception,
                "Failed to send notification dispatch {DispatchId} to {RecipientCount} recipients.",
                dispatch.Id,
                recipientIds.Count);
            return Problem(
                title: "消息未能发送",
                detail: "消息数据保存失败。请确认数据库已完成最新升级后重试；本次消息没有发送。",
                statusCode: StatusCodes.Status503ServiceUnavailable);
        }
    }

    [HttpGet("sent")]
    [Authorize(Roles = Senders)]
    public async Task<ActionResult> GetSent(
        int page = 1,
        int pageSize = 20,
        CancellationToken cancellationToken = default)
    {
        page = Math.Max(1, page);
        pageSize = Math.Clamp(pageSize, 1, 100);
        var userId = currentUserDataScope.Current.UserId;
        var source = db.MessageDispatches.AsNoTracking()
            .Where(x => x.SenderUserId == userId);
        var total = await source.CountAsync(cancellationToken);
        var items = await source
            .OrderByDescending(x => x.CreatedAt)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(x => new
            {
                x.Id,
                x.Title,
                x.Content,
                x.Category,
                x.AudienceType,
                x.AudienceName,
                x.RecipientCount,
                x.CreatedAt
            })
            .ToListAsync(cancellationToken);
        return Ok(new { Items = items, Total = total });
    }

    [HttpPost("{id:guid}/read")]
    public async Task<ActionResult> MarkRead(
        Guid id,
        CancellationToken cancellationToken)
    {
        var userId = currentUserDataScope.Current.UserId;
        var notification = await db.Notifications
            .FirstOrDefaultAsync(
                x => x.Id == id && x.UserId == userId,
                cancellationToken);
        if (notification is null) return NotFound();
        notification.IsRead = true;
        await db.SaveChangesAsync(cancellationToken);
        return NoContent();
    }

    [HttpPost("read-all")]
    public async Task<ActionResult> MarkAllRead(
        CancellationToken cancellationToken)
    {
        var userId = currentUserDataScope.Current.UserId;
        await db.Notifications
            .Where(x => x.UserId == userId && !x.IsRead)
            .ExecuteUpdateAsync(
                updates => updates.SetProperty(x => x.IsRead, true),
                cancellationToken);
        return NoContent();
    }

    private async Task<List<Guid>> GetTeachingTaskRecipientIdsAsync(
        Guid teachingTaskId,
        CancellationToken cancellationToken)
    {
        return await db.Students.AsNoTracking()
            .Where(student =>
                student.Status == StudentStatus.Active &&
                student.UserId != null &&
                db.Users.Any(user =>
                    user.Id == student.UserId &&
                    user.IsEnabled) &&
                (db.TeachingTaskClasses.Any(item =>
                     item.TeachingTaskId == teachingTaskId &&
                     item.AdministrativeClassId ==
                     student.AdministrativeClassId) ||
                 db.CourseEnrollments.Any(enrollment =>
                     enrollment.StudentId == student.Id &&
                     enrollment.Status == CourseEnrollmentStatus.Enrolled &&
                     enrollment.CourseSelectionOffering!.TeachingTaskId ==
                     teachingTaskId)))
            .Select(x => x.UserId!.Value)
            .Distinct()
            .ToListAsync(cancellationToken);
    }

    private async Task<Guid?> ResolveSenderCollegeIdAsync(
        CurrentUserScope scope,
        CancellationToken cancellationToken)
    {
        if (scope.CollegeId.HasValue)
            return scope.CollegeId;
        if (!scope.IsInRole(SystemRoles.Counselor))
            return null;

        var collegeIds = await db.AdministrativeClasses.AsNoTracking()
            .Where(x => x.CounselorUserId == scope.UserId)
            .Select(x => x.Major!.CollegeId)
            .Distinct()
            .Take(2)
            .ToListAsync(cancellationToken);
        return collegeIds.Count == 1 ? collegeIds[0] : null;
    }

    private static bool IsSchoolAdministrator(CurrentUserScope scope) =>
        scope.IsInRole(SystemRoles.SuperAdmin) ||
        scope.IsInRole(SystemRoles.AcademicAdmin);

    private async Task<object> BuildAdministratorComposerAsync(
        CurrentUserScope scope,
        Guid? fixedCollegeId,
        MessageAudienceType audienceType,
        string audienceName,
        CancellationToken cancellationToken)
    {
        var colleges = await db.Colleges.AsNoTracking()
            .Where(x => !fixedCollegeId.HasValue || x.Id == fixedCollegeId.Value)
            .OrderBy(x => x.Code)
            .Select(x => new { x.Id, x.Code, x.Name })
            .ToListAsync(cancellationToken);
        var administrativeClasses = await db.AdministrativeClasses.AsNoTracking()
            .Where(x =>
                x.IsEnabled &&
                (!fixedCollegeId.HasValue ||
                 x.Major!.CollegeId == fixedCollegeId.Value))
            .OrderByDescending(x => x.Grade)
            .ThenBy(x => x.Code)
            .Select(x => new
            {
                x.Id,
                x.Code,
                x.Name,
                x.Grade,
                CollegeId = x.Major!.CollegeId
            })
            .ToListAsync(cancellationToken);
        var teachingTasks = await db.TeachingTasks.AsNoTracking()
            .Where(x =>
                x.Status != TeachingTaskStatus.Draft &&
                (!fixedCollegeId.HasValue ||
                 x.Course!.CollegeId == fixedCollegeId.Value))
            .OrderByDescending(x => x.AcademicTerm!.IsCurrent)
            .ThenByDescending(x => x.AcademicTerm!.StartDate)
            .ThenBy(x => x.TaskNumber)
            .Select(x => new
            {
                x.Id,
                x.TaskNumber,
                CourseName = x.Course!.Name,
                TermName = x.AcademicTerm!.Name,
                CollegeId = x.Course.CollegeId
            })
            .ToListAsync(cancellationToken);
        var recipientCount = await ScopedRecipientQuery(scope, fixedCollegeId)
            .CountAsync(cancellationToken);

        return new
        {
            AudienceType = audienceType,
            AudienceName = audienceName,
            RecipientCount = recipientCount,
            CanFilterRecipients = true,
            MaximumSelectedRecipients,
            Colleges = colleges,
            Roles = RoleOptions,
            AdministrativeClasses = administrativeClasses,
            TeachingTasks = teachingTasks
        };
    }

    private IQueryable<ApplicationUser> ScopedRecipientQuery(
        CurrentUserScope scope,
        Guid? fixedCollegeId)
    {
        var source = db.Users.AsNoTracking()
            .Where(x => x.IsEnabled && x.Id != scope.UserId);
        if (IsSchoolAdministrator(scope)) return source;
        if (!fixedCollegeId.HasValue) return source.Where(_ => false);
        var collegeId = fixedCollegeId.Value;
        return source.Where(user =>
            user.CollegeId == collegeId ||
            db.Students.Any(student =>
                student.UserId == user.Id &&
                student.AdministrativeClass!.Major!.CollegeId == collegeId) ||
            db.Teachers.Any(teacher =>
                teacher.UserId == user.Id &&
                teacher.CollegeId == collegeId));
    }

    private IQueryable<ApplicationUser> ApplyRecipientFilter(
        IQueryable<ApplicationUser> source,
        MessageRecipientFilter? filter)
    {
        if (filter is null) return source;
        if (filter.CollegeId.HasValue)
        {
            var collegeId = filter.CollegeId.Value;
            source = source.Where(user =>
                user.CollegeId == collegeId ||
                db.Students.Any(student =>
                    student.UserId == user.Id &&
                    student.AdministrativeClass!.Major!.CollegeId == collegeId) ||
                db.Teachers.Any(teacher =>
                    teacher.UserId == user.Id &&
                    teacher.CollegeId == collegeId));
        }
        if (!string.IsNullOrWhiteSpace(filter.Role))
        {
            var role = filter.Role;
            source = source.Where(user =>
                db.UserRoles.Any(userRole =>
                    userRole.UserId == user.Id &&
                    db.Roles.Any(roleEntity =>
                        roleEntity.Id == userRole.RoleId &&
                        roleEntity.Name == role)));
        }
        if (filter.AdministrativeClassId.HasValue)
        {
            var classId = filter.AdministrativeClassId.Value;
            source = source.Where(user =>
                db.Students.Any(student =>
                    student.UserId == user.Id &&
                    student.AdministrativeClassId == classId));
        }
        if (filter.TeachingTaskId.HasValue)
        {
            var taskId = filter.TeachingTaskId.Value;
            source = source.Where(user =>
                db.Students.Any(student =>
                    student.UserId == user.Id &&
                    (db.TeachingTaskClasses.Any(item =>
                         item.TeachingTaskId == taskId &&
                         item.AdministrativeClassId ==
                         student.AdministrativeClassId) ||
                     db.CourseEnrollments.Any(enrollment =>
                         enrollment.StudentId == student.Id &&
                         enrollment.Status == CourseEnrollmentStatus.Enrolled &&
                         enrollment.CourseSelectionOffering!.TeachingTaskId ==
                         taskId))));
        }
        if (!string.IsNullOrWhiteSpace(filter.Keyword))
        {
            var keyword = filter.Keyword;
            source = source.Where(user =>
                user.DisplayName.Contains(keyword) ||
                (user.UserName != null && user.UserName.Contains(keyword)) ||
                (user.StaffNumber != null && user.StaffNumber.Contains(keyword)) ||
                db.Students.Any(student =>
                    student.UserId == user.Id &&
                    (student.Name.Contains(keyword) ||
                     student.StudentNumber.Contains(keyword))) ||
                db.Teachers.Any(teacher =>
                    teacher.UserId == user.Id &&
                    (teacher.Name.Contains(keyword) ||
                     teacher.TeacherNumber.Contains(keyword))));
        }

        return source;
    }

    private async Task<RecipientResolution> ResolveAdministratorRecipientsAsync(
        CurrentUserScope scope,
        Guid? fixedCollegeId,
        SendMessageRequest request,
        string defaultAudienceName,
        CancellationToken cancellationToken)
    {
        var source = ScopedRecipientQuery(scope, fixedCollegeId);
        if (request.RecipientMode == MessageRecipientMode.Scope)
        {
            return new RecipientResolution(
                fixedCollegeId.HasValue
                    ? MessageAudienceType.College
                    : MessageAudienceType.School,
                defaultAudienceName,
                await source.Select(x => x.Id).ToListAsync(cancellationToken),
                null);
        }

        if (request.RecipientMode == MessageRecipientMode.Filtered)
        {
            if (request.RecipientFilter?.Role is not null &&
                !SystemRoles.All.Contains(request.RecipientFilter.Role))
            {
                return new RecipientResolution(
                    default,
                    null,
                    null,
                    ValidationProblem("所选身份类型无效。"));
            }
            var recipientIds = await ApplyRecipientFilter(
                    source,
                    request.RecipientFilter)
                .Select(x => x.Id)
                .ToListAsync(cancellationToken);
            return new RecipientResolution(
                MessageAudienceType.Custom,
                BuildFilteredAudienceName(request.RecipientFilter),
                recipientIds,
                null);
        }

        if (request.RecipientMode == MessageRecipientMode.Selected)
        {
            var requestedIds = request.RecipientUserIds?
                .Distinct()
                .ToArray() ?? [];
            if (requestedIds.Length == 0)
            {
                return new RecipientResolution(
                    default,
                    null,
                    null,
                    ValidationProblem("请至少选择一名收件人。"));
            }
            if (requestedIds.Length > MaximumSelectedRecipients)
            {
                return new RecipientResolution(
                    default,
                    null,
                    null,
                    ValidationProblem(
                        $"单次最多指定 {MaximumSelectedRecipients} 名收件人。"));
            }

            var authorizedIds = await source
                .Where(x => requestedIds.Contains(x.Id))
                .Select(x => x.Id)
                .ToListAsync(cancellationToken);
            if (authorizedIds.Count != requestedIds.Length)
            {
                return new RecipientResolution(
                    default,
                    null,
                    null,
                    ValidationProblem("所选收件人包含无权限访问或已停用的账号。"));
            }

            return new RecipientResolution(
                MessageAudienceType.Custom,
                $"指定收件人（{authorizedIds.Count} 人）",
                authorizedIds,
                null);
        }

        return new RecipientResolution(
            default,
            null,
            null,
            ValidationProblem("请选择有效的收件方式。"));
    }

    private static string BuildFilteredAudienceName(MessageRecipientFilter? filter)
    {
        if (filter is null) return "当前权限范围内全部账号";
        var parts = new List<string>();
        if (filter.CollegeId.HasValue) parts.Add("指定学院");
        if (filter.Role is not null)
            parts.Add(RoleOptions.FirstOrDefault(x => x.Value == filter.Role)?.Label ??
                      filter.Role);
        if (filter.AdministrativeClassId.HasValue) parts.Add("指定行政班");
        if (filter.TeachingTaskId.HasValue) parts.Add("指定教学班");
        if (filter.Keyword is not null) parts.Add($"关键词“{filter.Keyword}”");
        return parts.Count == 0
            ? "当前权限范围内全部账号"
            : string.Join(" · ", parts);
    }

    private static string PlainText(string html)
    {
        var withoutTags = Regex.Replace(html, "<[^>]+>", " ");
        return WebUtility.HtmlDecode(withoutTags).Trim();
    }

    private static string? Normalize(string? value) =>
        string.IsNullOrWhiteSpace(value) ? null : value.Trim();

    private static readonly RoleOption[] RoleOptions =
    [
        new(SystemRoles.Student, "学生"),
        new(SystemRoles.Teacher, "任课教师"),
        new(SystemRoles.Counselor, "辅导员"),
        new(SystemRoles.CollegeAdmin, "学院管理员"),
        new(SystemRoles.AcademicAdmin, "校级教务管理员"),
        new(SystemRoles.Leader, "校领导"),
        new(SystemRoles.SuperAdmin, "超级管理员")
    ];

    private ActionResult ConflictProblem(string detail) =>
        Conflict(new ProblemDetails
        {
            Title = "无法发送消息",
            Detail = detail,
            Status = StatusCodes.Status409Conflict
        });

    private ActionResult ValidationProblem(string detail) =>
        BadRequest(new ProblemDetails
        {
            Title = "消息内容不完整",
            Detail = detail,
            Status = StatusCodes.Status400BadRequest
        });
}

public sealed record SendMessageRequest(
    [Required, StringLength(200)] string Title,
    [Required, StringLength(20000)] string Content,
    Guid? TeachingTaskId = null,
    MessageRecipientMode RecipientMode = MessageRecipientMode.Scope,
    MessageRecipientFilter? RecipientFilter = null,
    IReadOnlyCollection<Guid>? RecipientUserIds = null);

public sealed record MessageRecipientFilter(
    Guid? CollegeId = null,
    string? Role = null,
    Guid? AdministrativeClassId = null,
    Guid? TeachingTaskId = null,
    string? Keyword = null);

public enum MessageRecipientMode
{
    Scope = 1,
    Filtered = 2,
    Selected = 3
}

public sealed record RoleOption(string Value, string Label);

internal sealed record RecipientResolution(
    MessageAudienceType AudienceType,
    string? AudienceName,
    List<Guid>? RecipientIds,
    ActionResult? Error);

/// <summary>
/// Centralized helper to send notifications across the app.
/// </summary>
public static class NotificationService
{
    public static async Task SendAsync(
        AppDbContext db,
        Guid userId,
        string title,
        string content,
        string? linkUrl = null,
        CancellationToken cancellationToken = default,
        NotificationCategory category = NotificationCategory.General)
    {
        db.Notifications.Add(new Notification
        {
            UserId = userId,
            Title = title,
            Content = content,
            Category = category,
            LinkUrl = linkUrl
        });
        await db.SaveChangesAsync(cancellationToken);
    }

    public static async Task SendToRoleAsync(
        AppDbContext db,
        string roleName,
        string title,
        string content,
        Guid? collegeId = null,
        string? linkUrl = null,
        CancellationToken cancellationToken = default,
        NotificationCategory category = NotificationCategory.General)
    {
        var query = db.Users
            .Join(
                db.UserRoles,
                user => user.Id,
                userRole => userRole.UserId,
                (user, userRole) => new
                {
                    user.Id,
                    user.IsEnabled,
                    user.CollegeId,
                    userRole.RoleId
                })
            .Join(
                db.Roles,
                item => item.RoleId,
                role => role.Id,
                (item, role) => new
                {
                    item.Id,
                    item.IsEnabled,
                    item.CollegeId,
                    RoleName = role.Name!
                });

        var userIds = query.Where(x =>
            x.RoleName == roleName &&
            x.IsEnabled);

        if (collegeId.HasValue)
        {
            userIds = userIds.Where(x => x.CollegeId == collegeId.Value);
        }

        var ids = await userIds
            .Select(x => x.Id)
            .Distinct()
            .ToListAsync(cancellationToken);
        foreach (var id in ids)
        {
            db.Notifications.Add(new Notification
            {
                UserId = id,
                Title = title,
                Content = content,
                Category = category,
                LinkUrl = linkUrl
            });
        }
        await db.SaveChangesAsync(cancellationToken);
    }

    public static async Task SendToUserIdsAsync(
        AppDbContext db,
        IEnumerable<Guid> userIds,
        string title,
        string content,
        string? linkUrl = null,
        CancellationToken cancellationToken = default,
        NotificationCategory category = NotificationCategory.General)
    {
        foreach (var userId in userIds.Distinct())
        {
            db.Notifications.Add(new Notification
            {
                UserId = userId,
                Title = title,
                Content = content,
                Category = category,
                LinkUrl = linkUrl
            });
        }
        await db.SaveChangesAsync(cancellationToken);
    }
}
