using System.Globalization;
using System.Security.Cryptography;
using System.Text.Json.Nodes;
using Eis.Application.Administration;
using Eis.Infrastructure.Authentication;
using Eis.Infrastructure.Caching;

namespace Eis.Infrastructure.Administration;

internal sealed class AdminResultService(
    IAuthenticationStateStore authenticationState,
    AuthenticationRepository authenticationRepository,
    AdminOperationalSnapshotLoader operationalSnapshotLoader,
    AdminWorkflowResultSnapshotLoader resultSnapshotLoader,
    AdminAccountBatchSnapshotLoader directorySnapshotLoader,
    AdminResultRepository repository,
    IApplicationCache cache) : IAdminResultService
{
    public async Task<AdminEndpointResult> GetSummaryAsync(
        string sessionToken,
        string examId,
        CancellationToken cancellationToken)
    {
        var context = await ResolveAsync(sessionToken, superOnly: false, cancellationToken);
        if (context.Error is not null) return context.Error;
        if (examId.Length == 0)
        {
            return Success(new JsonObject
            {
                ["ok"] = true,
                ["selectedExamId"] = "",
                ["appeals"] = new JsonArray(),
                ["exams"] = new JsonArray(),
                ["resultCache"] = CacheStatus()
            });
        }

        var user = context.User!;
        var operational = await operationalSnapshotLoader.LoadAsync(cancellationToken);
        var selectedExam = operational.Exams.FirstOrDefault(item => item.Id == examId);
        if (selectedExam is null) return Error(404, "请选择有效的考试后再读取成绩");
        var directory = await directorySnapshotLoader.LoadAsync(cancellationToken);
        var examResults = await resultSnapshotLoader.LoadForExamAsync(selectedExam.Id, cancellationToken);
        var summaries = await resultSnapshotLoader.LoadSummariesForExamAsync(selectedExam.Id, cancellationToken);
        var lookups = ResultLookups.Create(operational, directory);
        var scoped = operational.Registrations.Where(item =>
            lookups.ProfileByUserId.TryGetValue(item.UserId, out var profile) && InScope(user, profile)).ToArray();
        var selectedScoped = scoped.Where(item => item.ExamId == selectedExam.Id).ToArray();
        if (Level(user) != "super" && selectedScoped.Length == 0)
            return Error(404, "该考试不在当前管理范围内");
        var approved = selectedScoped.Where(item => item.Status == "approved").ToArray();
        var selectedIds = selectedScoped.Select(item => item.Id).ToHashSet(StringComparer.Ordinal);
        var rawScopedResults = examResults.Where(item => selectedIds.Contains(item.RegistrationId)).ToArray();
        var appealedResultIds = directory.Instances
            .Where(item => item.BusinessType == "score_appeal")
            .Select(item => item.BusinessId)
            .ToHashSet(StringComparer.Ordinal);
        var resultById = rawScopedResults
            .Where(item => appealedResultIds.Contains(item.Id))
            .ToDictionary(
                item => item.Id,
                item => ResultJson(lookups, selectedExam, item),
                StringComparer.Ordinal);
        var appealViews = directory.Instances
            .Where(item => item.BusinessType == "score_appeal" && resultById.ContainsKey(item.BusinessId))
            .Select(item =>
            {
                var output = WorkflowJson(directory, item);
                output["result"] = resultById[item.BusinessId].DeepClone();
                output["reason"] = directory.Actions
                    .FirstOrDefault(action => action.InstanceId == item.Id && action.Action == "submit")?.Note ?? "";
                return output;
            })
            .ToArray();
        var summaryByRegistration = summaries.ToDictionary(item => item.RegistrationId, StringComparer.Ordinal);
        var exams = operational.Exams.Select(exam =>
        {
            var output = PublicExamJson(exam);
            if (exam.Id != selectedExam.Id) return output;
            var enrolled = approved.Sum(item => SubjectIds(item).Count);
            var scopedSummaries = approved
                .Select(item => summaryByRegistration.GetValueOrDefault(item.Id))
                .Where(item => item is not null)
                .Cast<AdminRegistrationResultSummary>()
                .ToArray();
            output["registrationCount"] = approved.Length;
            output["enrolledSubjects"] = enrolled;
            output["scored"] = rawScopedResults.Length;
            output["published"] = rawScopedResults.Count(item => item.Published);
            output["missing"] = Math.Max(0, enrolled - rawScopedResults.Length);
            output["complete"] = scopedSummaries.Count(item => item.Complete);
            output["qualified"] = scopedSummaries.Count(item => item.Complete && item.Qualified is true);
            output["unqualified"] = scopedSummaries.Count(item => item.Complete && item.Qualified is false);
            output["appeals"] = appealViews.Length;
            return output;
        }).ToArray();
        return Success(new JsonObject
        {
            ["ok"] = true,
            ["selectedExamId"] = selectedExam.Id,
            ["appeals"] = new JsonArray(appealViews),
            ["exams"] = new JsonArray(exams),
            ["resultCache"] = CacheStatus()
        });
    }

    public async Task<AdminEndpointResult> GetAsync(
        string sessionToken,
        string examId,
        int page,
        int pageSize,
        string query,
        string status,
        CancellationToken cancellationToken)
    {
        var context = await ResolveAsync(sessionToken, superOnly: false, cancellationToken);
        if (context.Error is not null) return context.Error;
        if (examId.Length == 0) return Error(400, "请选择考试后再读取成绩明细");
        var user = context.User!;
        var operational = await operationalSnapshotLoader.LoadAsync(cancellationToken);
        var exam = operational.Exams.FirstOrDefault(item => item.Id == examId);
        if (exam is null) return Error(404, "考试不存在");
        var directory = await directorySnapshotLoader.LoadAsync(cancellationToken);
        var lookups = ResultLookups.Create(operational, directory);
        var scopedRegistrationIds = operational.Registrations
            .Where(item => item.ExamId == exam.Id && item.Status == "approved" &&
                lookups.ProfileByUserId.TryGetValue(item.UserId, out var profile) && InScope(user, profile))
            .Select(item => item.Id)
            .ToHashSet(StringComparer.Ordinal);
        if (Level(user) != "super" && scopedRegistrationIds.Count == 0)
            return Error(404, "该考试不在当前管理范围内");
        var results = await resultSnapshotLoader.LoadForExamAsync(exam.Id, cancellationToken);
        var filtered = results.Where(item => scopedRegistrationIds.Contains(item.RegistrationId));
        status = status.Length == 0 ? "all" : status;
        filtered = status switch
        {
            "published" => filtered.Where(item => item.Published),
            "draft" => filtered.Where(item => !item.Published),
            "qualified" => filtered.Where(item => item.Qualified is true),
            "unqualified" => filtered.Where(item => item.Qualified is false),
            _ => filtered
        };
        var normalizedQuery = query.Trim();
        if (normalizedQuery.Length > 0)
        {
            filtered = filtered.Where(item => ResultSearchText(lookups, exam, item)
                .Contains(normalizedQuery, StringComparison.OrdinalIgnoreCase));
        }
        var ordered = filtered
            .OrderBy(item => lookups.AccountByRegistrationId.GetValueOrDefault(item.RegistrationId)?.CandidateNumber, StringComparer.Ordinal)
            .ThenBy(item => item.SubjectId, StringComparer.Ordinal)
            .ToArray();
        var pagination = Page(ordered.Length, page, pageSize, "resultTable");
        var items = ordered.Skip(pagination.Offset).Take(pagination.PageSize)
            .Select(item => ResultJson(lookups, exam, item))
            .ToArray();
        return Success(new JsonObject
        {
            ["ok"] = true,
            ["items"] = new JsonArray(items),
            ["pagination"] = pagination.Json
        });
    }

    public async Task<AdminEndpointResult> GetRosterAsync(
        string sessionToken,
        string examId,
        string subjectId,
        bool featureMode,
        int page,
        int pageSize,
        string query,
        string status,
        CancellationToken cancellationToken)
    {
        var context = await ResolveAsync(sessionToken, superOnly: true, cancellationToken);
        if (context.Error is not null) return context.Error;
        var operational = await operationalSnapshotLoader.LoadAsync(cancellationToken);
        var exam = operational.Exams.FirstOrDefault(item => item.Id == examId);
        if (exam is null) return Error(404, "考试不存在");
        if (!featureMode && exam.Subjects.All(item => Text(item["id"]) != subjectId))
            return Error(400, "请选择有效的考试科目");
        var directory = await directorySnapshotLoader.LoadAsync(cancellationToken);
        var lookups = ResultLookups.Create(operational, directory);
        var subjectResults = featureMode
            ? []
            : await resultSnapshotLoader.LoadForExamAsync(exam.Id, cancellationToken, subjectId);
        var resultByRegistration = subjectResults.ToDictionary(item => item.RegistrationId, StringComparer.Ordinal);
        var baseRegistrations = operational.Registrations
            .Where(item => item.ExamId == exam.Id && item.Status == "approved" &&
                (featureMode || SubjectIds(item).Contains(subjectId)))
            .ToArray();
        var registrations = baseRegistrations
            .Where(item =>
            {
                var result = resultByRegistration.GetValueOrDefault(item.Id);
                return status switch
                {
                    "missing" => result is null,
                    "draft" => result is { Published: false },
                    "published" => result is { Published: true },
                    "zero" => Number(item.Data["featureScore"]) == 0,
                    "modified" => Number(item.Data["featureScore"]) != 0,
                    "specialty" => Text(lookups.ProfileByUserId.GetValueOrDefault(item.UserId)?.Data["specialtyType"]).Length > 0,
                    _ => true
                };
            })
            .Where(item => query.Trim().Length == 0 || RegistrationSearchText(lookups, item)
                .Contains(query.Trim(), StringComparison.OrdinalIgnoreCase))
            .OrderBy(item => lookups.AccountByUserId.GetValueOrDefault(item.UserId)?.CandidateNumber, StringComparer.Ordinal)
            .ToArray();
        var pagination = Page(registrations.Length, page, pageSize, featureMode ? "featureScoreTable" : "resultEntryTable");
        var items = registrations.Skip(pagination.Offset).Take(pagination.PageSize).Select(item =>
        {
            var output = RegistrationJson(lookups, item);
            output["result"] = resultByRegistration.TryGetValue(item.Id, out var result)
                ? ResultJson(lookups, exam, result)
                : null;
            return output;
        }).ToArray();
        return Success(new JsonObject
        {
            ["ok"] = true,
            ["items"] = new JsonArray(items),
            ["pagination"] = pagination.Json,
            ["counts"] = new JsonObject
            {
                ["total"] = baseRegistrations.Length,
                ["recorded"] = baseRegistrations.Count(item => resultByRegistration.ContainsKey(item.Id)),
                ["published"] = baseRegistrations.Count(item => resultByRegistration.GetValueOrDefault(item.Id)?.Published is true),
                ["modified"] = baseRegistrations.Count(item => Number(item.Data["featureScore"]) != 0),
                ["specialty"] = baseRegistrations.Count(item =>
                    Text(lookups.ProfileByUserId.GetValueOrDefault(item.UserId)?.Data["specialtyType"]).Length > 0)
            }
        });
    }

    public async Task<AdminEndpointResult> SaveAsync(
        string sessionToken,
        JsonObject body,
        CancellationToken cancellationToken)
    {
        var context = await ResolveAsync(sessionToken, superOnly: true, cancellationToken);
        if (context.Error is not null) return context.Error;
        var user = context.User!;
        var operational = await operationalSnapshotLoader.LoadAsync(cancellationToken);
        var registration = operational.Registrations.FirstOrDefault(item =>
            item.Id == Text(body["registrationId"]) && item.Status == "approved");
        if (registration is null) return Error(404, "已通过的报名记录不存在");
        var exam = operational.Exams.FirstOrDefault(item => item.Id == registration.ExamId);
        if (exam?.Data["archivedAt"] is not null) return Error(409, "该考试已归档，成绩已永久锁定");
        var subjectId = Text(body["subjectId"]);
        var subject = exam?.Subjects.FirstOrDefault(item => Text(item["id"]) == subjectId);
        if (subject is null || !SubjectIds(registration).Contains(subjectId))
            return Error(400, "该考生未报名此科目");
        var score = Number(body["score"]);
        var fullScore = Number(subject["fullScore"]);
        if (!AdminResultStatistics.IsValidScore(score, fullScore))
            return Error(400, $"成绩必须在 0—{AdminResultStatistics.Format(fullScore)} 之间，且只能填写整数或 .5");
        var allResults = (await resultSnapshotLoader.LoadForExamAsync(exam!.Id, cancellationToken)).ToList();
        var existing = allResults.FirstOrDefault(item =>
            item.RegistrationId == registration.Id && item.SubjectId == subjectId);
        var published = JsBoolean(body["published"]);
        var now = NowIso();
        var candidate = new AdminWorkflowResult(
            existing?.Id ?? Uid("result"),
            registration.Id,
            subjectId,
            score,
            published ? "" : "待发布",
            published,
            now,
            published ? existing?.PublishedAt ?? now : null);
        Replace(allResults, candidate);
        var calculation = AdminResultStatistics.Calculate(
            exam,
            operational.Registrations,
            allResults,
            now);
        candidate = ApplyMetric(candidate, calculation);
        var profile = operational.Profiles.FirstOrDefault(item => item.UserId == registration.UserId);
        await repository.SaveAsync(
            new ResultWrite(
                candidate,
                existing is null,
                Audit(
                    user,
                    published ? "发布成绩" : "保存成绩",
                    $"{Text(profile?.Data["name"])} · {Text(subject["name"])} · {AdminResultStatistics.Format(score)}")),
            calculation,
            cancellationToken);
        await cache.InvalidateAsync("results");
        return Success(new JsonObject { ["ok"] = true, ["result"] = RawResultJson(candidate) });
    }

    public async Task<AdminEndpointResult> SaveBulkAsync(
        string sessionToken,
        JsonObject body,
        CancellationToken cancellationToken)
    {
        var context = await ResolveAsync(sessionToken, superOnly: true, cancellationToken);
        if (context.Error is not null) return context.Error;
        var user = context.User!;
        var operational = await operationalSnapshotLoader.LoadAsync(cancellationToken);
        var exam = operational.Exams.FirstOrDefault(item =>
            item.Id == Clean(Text(body["examId"]), 64) && item.Data["archivedAt"] is null);
        var subject = exam?.Subjects.FirstOrDefault(item => Text(item["id"]) == Clean(Text(body["subjectId"]), 64));
        if (exam is null || subject is null) return Error(400, "请选择有效且未归档的考试科目");
        var sourceRows = body["rows"] as JsonArray ?? [];
        if (sourceRows.Count == 0) return Error(400, "没有需要保存的成绩");
        var published = JsBoolean(body["published"]);
        var allResults = (await resultSnapshotLoader.LoadForExamAsync(exam.Id, cancellationToken)).ToList();
        var prepared = new List<PreparedResult>();
        var seen = new HashSet<string>(StringComparer.Ordinal);
        for (var index = 0; index < sourceRows.Count; index++)
        {
            var row = sourceRows[index] as JsonObject ?? new JsonObject();
            var registration = operational.Registrations.FirstOrDefault(item =>
                item.Id == Text(row["registrationId"]) &&
                item.ExamId == exam.Id &&
                item.Status == "approved" &&
                SubjectIds(item).Contains(Text(subject["id"])));
            if (registration is null || !seen.Add(registration.Id))
                return Error(400, $"第 {index + 1} 条考生成绩无效或重复");
            var score = Number(row["score"]);
            var fullScore = Number(subject["fullScore"]);
            if (!AdminResultStatistics.IsValidScore(score, fullScore))
                return Error(400, $"第 {index + 1} 条：成绩须在 0—{AdminResultStatistics.Format(fullScore)} 之间，且只能填写整数或 .5");
            var existing = allResults.FirstOrDefault(item =>
                item.RegistrationId == registration.Id && item.SubjectId == Text(subject["id"]));
            var now = NowIso();
            var result = new AdminWorkflowResult(
                existing?.Id ?? Uid("result"),
                registration.Id,
                Text(subject["id"]),
                score,
                published ? "" : "待发布",
                published,
                now,
                published ? existing?.PublishedAt ?? now : null);
            Replace(allResults, result);
            prepared.Add(new(registration, existing, result));
        }
        var calculation = AdminResultStatistics.Calculate(
            exam,
            operational.Registrations,
            allResults,
            NowIso());
        var writes = prepared.Select(item =>
        {
            var result = ApplyMetric(item.Result, calculation);
            var profile = operational.Profiles.FirstOrDefault(profile => profile.UserId == item.Registration.UserId);
            var account = operational.Users.FirstOrDefault(account => account.Id == item.Registration.UserId);
            return new ResultWrite(
                result,
                item.Existing is null,
                Audit(
                    user,
                    published ? "Excel 批量发布成绩" : "Excel 批量保存成绩",
                    $"{account?.CandidateNumber ?? Text(item.Registration.Data["registrationNumber"])} · {Text(exam.Data["name"])} · {Text(subject["name"])} · {AdminResultStatistics.Format(result.Score)}"));
        }).ToArray();
        await repository.SaveManyAsync(writes, [calculation], cancellationToken);
        await cache.InvalidateAsync("results");
        var create = prepared.Count(item => item.Existing is null);
        return Success(new JsonObject
        {
            ["ok"] = true,
            ["published"] = published,
            ["count"] = writes.Length,
            ["summary"] = new JsonObject
            {
                ["total"] = writes.Length,
                ["valid"] = writes.Length,
                ["invalid"] = 0,
                ["create"] = create,
                ["update"] = writes.Length - create,
                ["publish"] = published ? writes.Length : 0
            }
        });
    }

    public async Task<AdminEndpointResult> ImportAsync(
        string sessionToken,
        JsonObject body,
        CancellationToken cancellationToken)
    {
        var context = await ResolveAsync(sessionToken, superOnly: true, cancellationToken);
        if (context.Error is not null) return context.Error;
        var user = context.User!;
        var operational = await operationalSnapshotLoader.LoadAsync(cancellationToken);
        var allResults = (await resultSnapshotLoader.LoadAsync(cancellationToken)).ToList();
        var rows = body["rows"] as JsonArray ?? [];
        var prepared = new List<PreparedImport>();
        var seen = new HashSet<string>(StringComparer.Ordinal);
        for (var index = 0; index < rows.Count; index++)
        {
            var row = rows[index] as JsonObject ?? new JsonObject();
            var sourceRow = Integer(row["__row"] ?? row["sourceRow"], index + 3);
            var candidateNumber = Clean(Text(row["candidateNumber"]), 120);
            var examCode = Clean(Text(row["examCode"]), 60);
            var subjectName = Clean(Text(row["subjectName"]), 50);
            var account = operational.Users.FirstOrDefault(item => item.CandidateNumber == candidateNumber);
            var exam = operational.Exams.FirstOrDefault(item =>
                Text(item.Data["code"]).Equals(examCode, StringComparison.OrdinalIgnoreCase));
            var registration = operational.Registrations.FirstOrDefault(item =>
                item.UserId == account?.Id && item.ExamId == exam?.Id && item.Status == "approved");
            var subject = exam?.Subjects.FirstOrDefault(item =>
                Text(item["name"]).Equals(subjectName, StringComparison.OrdinalIgnoreCase));
            string? message = null;
            if (candidateNumber.Length == 0) message = "报名号不能为空";
            else if (account is null) message = "报名号不存在";
            else if (examCode.Length == 0) message = "考试代码不能为空";
            else if (exam is null) message = "考试代码不存在";
            else if (exam.Data["archivedAt"] is not null) message = "该考试已归档，成绩已永久锁定";
            else if (subjectName.Length == 0) message = "科目不能为空";
            else if (subject is null) message = "该考试中不存在此科目";
            else if (registration is null) message = "该考生没有已通过的本场考试报名";
            else if (!SubjectIds(registration).Contains(Text(subject["id"]))) message = "该考生未报考此科目";
            var score = Text(row["score"]).Length == 0 ? double.NaN : Number(row["score"]);
            if (message is null && !AdminResultStatistics.IsValidScore(score, Number(subject?["fullScore"])))
                message = $"成绩须在 0—{AdminResultStatistics.Format(Number(subject?["fullScore"]))} 之间，且只能填写整数或 .5";
            var publishText = Text(row["published"]);
            bool published;
            if (row["published"] is JsonValue publishedValue &&
                publishedValue.TryGetValue<bool>(out var publishedBoolean))
                published = publishedBoolean;
            else if (publishText is "" or "不发布")
                published = false;
            else if (publishText == "发布")
                published = true;
            else
            {
                published = false;
                message = "发布状态只能是“发布”或“不发布”";
            }
            var key = registration is null || subject is null
                ? $"{candidateNumber}|{examCode}|{subjectName}"
                : $"{registration.Id}|{Text(subject["id"])}";
            if (!seen.Add(key) && message is null) message = "同一考生、考试和科目在文件中重复";
            if (message is not null) return Error(400, $"第 {sourceRow} 行：{message}；请返回预览修正后重试");
            var existing = allResults.FirstOrDefault(item =>
                item.RegistrationId == registration!.Id && item.SubjectId == Text(subject!["id"]));
            var now = NowIso();
            var result = new AdminWorkflowResult(
                existing?.Id ?? Uid("result"), registration!.Id, Text(subject!["id"]), score,
                published ? "" : "待发布", published, now, published ? existing?.PublishedAt ?? now : null);
            Replace(allResults, result);
            prepared.Add(new(registration, exam!, subject!, existing, result, candidateNumber));
        }
        var calculations = prepared
            .Select(item => item.Exam)
            .DistinctBy(item => item.Id)
            .Select(exam => AdminResultStatistics.Calculate(
                exam,
                operational.Registrations,
                allResults,
                NowIso()))
            .ToArray();
        var metricByResultId = calculations
            .SelectMany(item => item.Metrics)
            .ToDictionary(item => item.ResultId, StringComparer.Ordinal);
        var writes = prepared.Select(item =>
        {
            var result = ApplyMetric(item.Result, metricByResultId.GetValueOrDefault(item.Result.Id));
            return new ResultWrite(
                result,
                item.Existing is null,
                Audit(user, result.Published ? "Excel 批量发布成绩" : "Excel 批量保存成绩",
                    $"{item.CandidateNumber} · {Text(item.Exam.Data["name"])} · {Text(item.Subject["name"])} · {AdminResultStatistics.Format(result.Score)}"));
        }).ToArray();
        await repository.SaveManyAsync(writes, calculations, cancellationToken);
        await cache.InvalidateAsync("results");
        var create = prepared.Count(item => item.Existing is null);
        return Success(new JsonObject
        {
            ["ok"] = true,
            ["count"] = writes.Length,
            ["summary"] = new JsonObject
            {
                ["total"] = writes.Length,
                ["valid"] = writes.Length,
                ["invalid"] = 0,
                ["create"] = create,
                ["update"] = writes.Length - create,
                ["publish"] = prepared.Count(item => item.Result.Published)
            }
        });
    }

    public async Task<AdminEndpointResult> PreviewImportAsync(
        string sessionToken,
        JsonArray rows,
        CancellationToken cancellationToken)
    {
        var context = await ResolveAsync(sessionToken, superOnly: true, cancellationToken);
        if (context.Error is not null) return context.Error;
        var operational = await operationalSnapshotLoader.LoadAsync(cancellationToken);
        var directory = await directorySnapshotLoader.LoadAsync(cancellationToken);
        var previewResults = (await resultSnapshotLoader.LoadAsync(cancellationToken)).ToList();
        var output = new List<JsonObject>();
        var seen = new HashSet<string>(StringComparer.Ordinal);
        for (var index = 0; index < rows.Count; index++)
        {
            var source = rows[index] as JsonObject ?? new JsonObject();
            var sourceRow = Integer(source["__row"] ?? source["sourceRow"], index + 3);
            var candidateNumber = Clean(Text(source["candidateNumber"]), 120);
            var examCode = Clean(Text(source["examCode"]), 60);
            var subjectName = Clean(Text(source["subjectName"]), 50);
            var account = operational.Users.FirstOrDefault(item => item.CandidateNumber == candidateNumber);
            var exam = operational.Exams.FirstOrDefault(item =>
                Text(item.Data["code"]).Equals(examCode, StringComparison.OrdinalIgnoreCase));
            var registration = operational.Registrations.FirstOrDefault(item =>
                item.UserId == account?.Id && item.ExamId == exam?.Id && item.Status == "approved");
            var subject = exam?.Subjects.FirstOrDefault(item =>
                Text(item["name"]).Equals(subjectName, StringComparison.OrdinalIgnoreCase));
            var errors = new JsonArray();
            void AddError(string message) => errors.Add(message);
            if (candidateNumber.Length == 0) AddError("报名号不能为空");
            else if (account is null) AddError("报名号不存在");
            if (examCode.Length == 0) AddError("考试代码不能为空");
            else if (exam is null) AddError("考试代码不存在");
            else if (exam.Data["archivedAt"] is not null) AddError("该考试已归档，成绩已永久锁定");
            if (subjectName.Length == 0) AddError("科目不能为空");
            else if (exam is not null && subject is null) AddError("该考试中不存在此科目");
            if (exam is not null && account is not null && registration is null)
                AddError("该考生没有已通过的本场考试报名");
            if (registration is not null && subject is not null &&
                !SubjectIds(registration).Contains(Text(subject["id"])))
                AddError("该考生未报考此科目");
            var score = Text(source["score"]).Length == 0 ? double.NaN : Number(source["score"]);
            if (!AdminResultStatistics.IsValidScore(score, Number(subject?["fullScore"])))
                AddError($"成绩须在 0—{AdminResultStatistics.Format(Number(subject?["fullScore"]))} 之间，且只能填写整数或 .5");
            var publishedText = Text(source["published"]);
            var published = source["published"] is JsonValue publishedValue &&
                            publishedValue.TryGetValue<bool>(out var publishedBoolean)
                ? publishedBoolean
                : publishedText == "发布";
            if (publishedText.Length > 0 && publishedText is not ("发布" or "不发布") &&
                !(source["published"] is JsonValue booleanValue && booleanValue.TryGetValue<bool>(out _)))
                AddError("发布状态只能是“发布”或“不发布”");
            var key = registration is not null && subject is not null
                ? $"{registration.Id}|{Text(subject["id"])}"
                : $"{candidateNumber}|{examCode}|{subjectName}";
            if (!seen.Add(key)) AddError("同一考生、考试和科目在文件中重复");
            var existing = registration is null || subject is null
                ? null
                : previewResults.FirstOrDefault(item =>
                    item.RegistrationId == registration.Id && item.SubjectId == Text(subject["id"]));
            var preview = new AdminWorkflowResult(
                existing?.Id ?? $"preview-{sourceRow}",
                registration?.Id ?? "",
                Text(subject?["id"]),
                double.IsFinite(score) ? score : 0,
                published ? "" : "待发布",
                published,
                existing?.UpdatedAt ?? NowIso(),
                published ? existing?.PublishedAt ?? NowIso() : null);
            if (errors.Count == 0) Replace(previewResults, preview);
            var profile = operational.Profiles.FirstOrDefault(item => item.UserId == account?.Id);
            var schoolClass = directory.Classes.FirstOrDefault(item => item.Id == profile?.ClassId);
            output.Add(new JsonObject
            {
                ["sourceRow"] = sourceRow,
                ["candidateNumber"] = candidateNumber,
                ["candidateName"] = Text(profile?.Data["name"]) is { Length: > 0 } name ? name : account?.DisplayName ?? "",
                ["schoolName"] = Text(profile?.Data["school"]),
                ["className"] = schoolClass?.Name ?? Text(profile?.Data["grade"]),
                ["examId"] = exam?.Id ?? "",
                ["examCode"] = examCode,
                ["examName"] = Text(exam?.Data["name"]),
                ["registrationId"] = registration?.Id ?? "",
                ["subjectId"] = Text(subject?["id"]),
                ["subjectName"] = subjectName,
                ["fullScore"] = subject is null ? null : Number(subject["fullScore"]),
                ["passRule"] = Text(subject?["passRule"]) is { Length: > 0 } rule ? rule : "fixed_score",
                ["passValue"] = subject is null ? null : Number(subject["passValue"] ?? subject["passScore"]),
                ["score"] = double.IsFinite(score) ? score : null,
                ["published"] = published,
                ["existingResultId"] = existing?.Id ?? "",
                ["mode"] = existing is null ? "create" : "update",
                ["errors"] = errors
            });
        }
        var previewCalculations = output
            .Where(item => (item["errors"] as JsonArray)?.Count == 0)
            .Select(item => Text(item["examId"]))
            .Distinct(StringComparer.Ordinal)
            .Select(examId => operational.Exams.First(item => item.Id == examId))
            .Select(exam => AdminResultStatistics.Calculate(
                exam,
                operational.Registrations,
                previewResults,
                NowIso()))
            .ToArray();
        var previewMetricByResultId = previewCalculations
            .SelectMany(item => item.Metrics)
            .ToDictionary(item => item.ResultId, StringComparer.Ordinal);
        foreach (var row in output.Where(item => (item["errors"] as JsonArray)?.Count == 0))
        {
            var result = previewResults.First(item =>
                item.RegistrationId == Text(row["registrationId"]) && item.SubjectId == Text(row["subjectId"]));
            var exam = operational.Exams.First(item => item.Id == Text(row["examId"]));
            var subject = exam.Subjects.First(item => Text(item["id"]) == result.SubjectId);
            var metric = previewMetricByResultId[result.Id];
            row["rank"] = metric.Rank;
            row["cohortSize"] = metric.CohortSize;
            row["rankPercent"] = metric.RankPercent;
            row["grade"] = metric.Grade;
            row["passScore"] = metric.PassScore;
            row["cutoffRank"] = metric.CutoffRank;
            row["qualified"] = JsonValue.Create(metric.Qualified);
            row["passText"] = AdminResultStatistics.PassText(subject);
        }
        var invalid = output.Count(item => (item["errors"] as JsonArray)?.Count > 0);
        return Success(new JsonObject
        {
            ["ok"] = true,
            ["rows"] = new JsonArray(output.Select(item => (JsonNode)item).ToArray()),
            ["errors"] = new JsonArray(output.SelectMany(item =>
                    (item["errors"] as JsonArray ?? []).Select(error => (JsonNode)new JsonObject
                    {
                        ["row"] = Integer(item["sourceRow"]),
                        ["message"] = Text(error)
                    })).ToArray()),
            ["summary"] = new JsonObject
            {
                ["total"] = output.Count,
                ["valid"] = output.Count - invalid,
                ["invalid"] = invalid,
                ["create"] = output.Count(item => Text(item["mode"]) == "create" && (item["errors"] as JsonArray)?.Count == 0),
                ["update"] = output.Count(item => Text(item["mode"]) == "update" && (item["errors"] as JsonArray)?.Count == 0),
                ["publish"] = output.Count(item =>
                    item["published"] is JsonValue value &&
                    value.TryGetValue<bool>(out var published) &&
                    published &&
                    (item["errors"] as JsonArray)?.Count == 0)
            }
        });
    }

    public async Task<AdminEndpointResult> SaveFeatureBulkAsync(
        string sessionToken,
        JsonObject body,
        CancellationToken cancellationToken)
    {
        var context = await ResolveAsync(sessionToken, superOnly: true, cancellationToken);
        if (context.Error is not null) return context.Error;
        var user = context.User!;
        var operational = await operationalSnapshotLoader.LoadAsync(cancellationToken);
        var exam = operational.Exams.FirstOrDefault(item =>
            item.Id == Clean(Text(body["examId"]), 64) && item.Data["archivedAt"] is null);
        if (exam is null) return Error(400, "请选择有效且未归档的考试");
        var rows = body["rows"] as JsonArray ?? [];
        if (rows.Count == 0) return Error(400, "没有需要保存的特征分");
        var seen = new HashSet<string>(StringComparer.Ordinal);
        var entries = new List<FeatureScoreWrite>();
        for (var index = 0; index < rows.Count; index++)
        {
            var row = rows[index] as JsonObject ?? new JsonObject();
            var registration = operational.Registrations.FirstOrDefault(item =>
                item.Id == Text(row["registrationId"]) && item.ExamId == exam.Id && item.Status == "approved");
            if (registration is null || !seen.Add(registration.Id))
                return Error(400, $"第 {index + 1} 条考生记录无效或重复");
            var score = Number(row["featureScore"]);
            if (!double.IsFinite(score) || score < 0 || score > 1000)
                return Error(400, $"第 {index + 1} 条特征分必须在 0—1000 之间");
            score = Math.Round(score, 2, MidpointRounding.AwayFromZero);
            var profile = operational.Profiles.FirstOrDefault(item => item.UserId == registration.UserId);
            entries.Add(new(
                registration.Id,
                score,
                Audit(user, "批量登记特征分", $"{Text(profile?.Data["name"])} · {Text(exam.Data["name"])} · {AdminResultStatistics.Format(score)}")));
        }
        await repository.SaveFeatureScoresAsync(entries, cancellationToken);
        await cache.InvalidateAsync("results");
        return Success(new JsonObject { ["ok"] = true, ["count"] = entries.Count });
    }

    public async Task<AdminEndpointResult> SaveFeatureAsync(
        string sessionToken,
        string registrationId,
        JsonObject body,
        CancellationToken cancellationToken)
    {
        var context = await ResolveAsync(sessionToken, superOnly: true, cancellationToken);
        if (context.Error is not null) return context.Error;
        var user = context.User!;
        var operational = await operationalSnapshotLoader.LoadAsync(cancellationToken);
        var registration = operational.Registrations.FirstOrDefault(item =>
            item.Id == registrationId && item.Status == "approved");
        if (registration is null) return Error(404, "已通过的报名记录不存在");
        var exam = operational.Exams.FirstOrDefault(item => item.Id == registration.ExamId);
        if (exam?.Data["archivedAt"] is not null) return Error(409, "该考试已归档，特征分已永久锁定");
        var score = Number(body["featureScore"]);
        if (!double.IsFinite(score) || score < 0 || score > 1000)
            return Error(400, "特征分必须在 0—1000 之间");
        score = Math.Round(score, 2, MidpointRounding.AwayFromZero);
        var profile = operational.Profiles.FirstOrDefault(item => item.UserId == registration.UserId);
        await repository.SaveFeatureScoresAsync(
            [new(
                registration.Id,
                score,
                Audit(user, "登记特征分", $"{Text(profile?.Data["name"])} · {Text(exam?.Data["name"])} · {AdminResultStatistics.Format(score)}"))],
            cancellationToken);
        await cache.InvalidateAsync("results");
        var output = registration.Data.DeepClone().AsObject();
        output["featureScore"] = score;
        return Success(new JsonObject { ["ok"] = true, ["registration"] = output });
    }

    public async Task<AdminEndpointResult> RefreshCacheAsync(
        string sessionToken,
        CancellationToken cancellationToken)
    {
        var context = await ResolveAsync(sessionToken, superOnly: true, cancellationToken);
        if (context.Error is not null) return context.Error;
        var refreshed = await cache.InvalidateAsync("results");
        return Success(new JsonObject
        {
            ["ok"] = true,
            ["refreshed"] = refreshed,
            ["cacheStatus"] = cache.Status,
            ["message"] = refreshed
                ? "成绩 Redis 缓存已刷新，后续查询将重新生成缓存"
                : "Redis 缓存当前未连接或刷新失败，成绩查询继续直接读取数据库"
        });
    }

    public async Task<AdminDocumentResult> ExportAsync(
        string sessionToken,
        string examId,
        CancellationToken cancellationToken)
    {
        var context = await ResolveAsync(sessionToken, superOnly: false, cancellationToken);
        if (context.Error is not null) return DocumentError(context.Error);
        var user = context.User!;
        var operational = await operationalSnapshotLoader.LoadAsync(cancellationToken);
        var directory = await directorySnapshotLoader.LoadAsync(cancellationToken);
        var allResults = examId.Length > 0
            ? await resultSnapshotLoader.LoadForExamAsync(examId, cancellationToken)
            : await resultSnapshotLoader.LoadAsync(cancellationToken);
        var registrations = operational.Registrations.Where(item =>
        {
            if (item.Status != "approved" || (examId.Length > 0 && item.ExamId != examId)) return false;
            var profile = operational.Profiles.FirstOrDefault(profile => profile.UserId == item.UserId);
            return profile is not null && InScope(user, profile);
        }).ToArray();
        var rows = new List<AdminResultExportRow>();
        foreach (var registration in registrations)
        {
            var exam = operational.Exams.FirstOrDefault(item => item.Id == registration.ExamId);
            var profile = operational.Profiles.FirstOrDefault(item => item.UserId == registration.UserId);
            var account = operational.Users.FirstOrDefault(item => item.Id == registration.UserId);
            var schoolClass = directory.Classes.FirstOrDefault(item => item.Id == profile?.ClassId);
            foreach (var subject in (exam?.Subjects ?? []).Where(item => SubjectIds(registration).Contains(Text(item["id"]))))
            {
                var result = allResults.FirstOrDefault(item =>
                    item.RegistrationId == registration.Id && item.SubjectId == Text(subject["id"]));
                var card = registration.Data["admitCard"] as JsonObject;
                rows.Add(new(
                    account?.CandidateNumber ?? Text(registration.Data["registrationNumber"]),
                    Text(card?["number"]),
                    Text(profile?.Data["name"]) is { Length: > 0 } name ? name : account?.DisplayName ?? "",
                    Text(profile?.Data["school"]),
                    schoolClass?.Name ?? Text(profile?.Data["grade"]),
                    Text(exam?.Data["code"]),
                    Text(exam?.Data["name"]),
                    Text(subject["name"]),
                    Number(subject["fullScore"]),
                    AdminResultStatistics.PassText(subject),
                    result?.PassScore is double passScore ? passScore : "",
                    result is null ? "" : result.Score,
                    result?.Rank is int rank ? rank : "",
                    result?.RankPercent is double percent ? percent : "",
                    result?.Qualified is null ? "" : result.Qualified.Value ? "达线" : "未达线",
                    result is null ? "" : result.Published ? result.Grade : "待发布",
                    result?.Published is true ? "发布" : "不发布",
                    result?.UpdatedAt ?? result?.PublishedAt ?? ""));
            }
        }
        var content = AdminResultExporter.Build(rows);
        return new(
            200,
            content,
            "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
            "考试成绩台账.xlsx",
            null);
    }

    public async Task<AdminEndpointResult> ReviewAppealAsync(
        string sessionToken,
        string resultId,
        JsonObject body,
        CancellationToken cancellationToken)
    {
        var context = await ResolveAsync(sessionToken, superOnly: false, cancellationToken);
        if (context.Error is not null) return context.Error;
        var user = context.User!;
        var requestedStatus = Text(body["status"]);
        if (requestedStatus is not ("approved" or "rejected"))
            return Error(400, "复议处理状态无效");
        var operational = await operationalSnapshotLoader.LoadAsync(cancellationToken);
        var directory = await directorySnapshotLoader.LoadAsync(cancellationToken);
        var allResults = (await resultSnapshotLoader.LoadAsync(cancellationToken)).ToList();
        var result = allResults.FirstOrDefault(item => item.Id == resultId && item.Published);
        var registration = operational.Registrations.FirstOrDefault(item => item.Id == result?.RegistrationId);
        var profile = operational.Profiles.FirstOrDefault(item => item.UserId == registration?.UserId);
        if (result is null || registration is null || profile is null)
            return Error(404, "待处理的成绩复议不存在");
        if (!InScope(user, profile)) return Error(403, "该成绩复议不在你的数据范围内");
        var instance = directory.Instances.FirstOrDefault(item =>
            item.BusinessType == "score_appeal" &&
            item.BusinessId == result.Id &&
            item.Status == "pending");
        var workflow = directory.Workflows.FirstOrDefault(item => item.Id == instance?.WorkflowId);
        var step = workflow?.Steps.FirstOrDefault(item => item.Position == instance?.CurrentStep);
        if (instance is null || workflow is null || step is null)
            return Error(409, "成绩复议流程状态异常");
        if (Level(user) != "super" &&
            (instance.AssigneeId != user.Id || step.AdminLevel != Level(user)))
            return Error(403, "该流程当前未分配给你，可由当前处理人转交");
        var exam = operational.Exams.FirstOrDefault(item => item.Id == registration.ExamId);
        if (exam?.Data["archivedAt"] is not null)
            return Error(409, "该考试已归档，成绩及复议流程已永久锁定");
        var subject = exam?.Subjects.FirstOrDefault(item => Text(item["id"]) == result.SubjectId);
        var finalStep = instance.CurrentStep >= workflow.Steps.Count;
        var reviewedScore = double.NaN;
        if (requestedStatus == "approved" && finalStep)
        {
            reviewedScore = Number(body["reviewedScore"]);
            var fullScore = Number(subject?["fullScore"]);
            if (!AdminResultStatistics.IsValidScore(reviewedScore, fullScore))
                return Error(400, $"最终审批必须填写 0—{AdminResultStatistics.Format(fullScore)} 之间的整数或 .5 分数");
        }

        var now = NowIso();
        var note = Clean(Text(body["reviewNote"]), 300);
        string actionName;
        string? nextAssigneeId = null;
        AccountWorkflowInstance updatedInstance;
        AdminWorkflowResult? updatedResult = null;
        AdminResultCalculation? calculation = null;
        if (requestedStatus == "rejected")
        {
            actionName = "reject";
            updatedInstance = instance with { Status = "rejected", CompletedAt = now, AssigneeId = null };
        }
        else if (!finalStep)
        {
            actionName = "approve";
            var nextStep = workflow.Steps.FirstOrDefault(item => item.Position == instance.CurrentStep + 1);
            var nextAssignee = nextStep is null
                ? null
                : SelectAssignee(directory, nextStep.AdminLevel, profile);
            if (nextAssignee is null)
                return Error(409, $"没有可承接“{nextStep?.Name ?? "下一审批步骤"}”的管理员");
            nextAssigneeId = nextAssignee.Id;
            updatedInstance = instance with
            {
                CurrentStep = instance.CurrentStep + 1,
                AssigneeId = nextAssignee.Id
            };
        }
        else
        {
            actionName = "approve";
            updatedInstance = instance with { Status = "approved", CompletedAt = now, AssigneeId = null };
            var originalScore = result.Score;
            updatedResult = result with { Score = reviewedScore, UpdatedAt = now };
            Replace(allResults, updatedResult);
            calculation = AdminResultStatistics.Calculate(
                exam!,
                operational.Registrations,
                allResults,
                now);
            updatedResult = ApplyMetric(updatedResult, calculation);
            Replace(allResults, updatedResult);
            var conclusion = updatedResult.Qualified is null
                ? "本科不判定单科达线"
                : updatedResult.Qualified.Value
                    ? $"达到{PassConclusion(subject, updatedResult)}"
                    : $"未达到{PassConclusion(subject, updatedResult)}";
            note = string.Join(
                '；',
                new[]
                {
                    $"原分 {AdminResultStatistics.Format(originalScore)} → 复核后 {AdminResultStatistics.Format(reviewedScore)}",
                    conclusion,
                    note
                }.Where(item => item.Length > 0));
        }
        var action = new AccountWorkflowAction(
            Uid("flow_action"),
            instance.Id,
            user.Id,
            actionName,
            note,
            instance.AssigneeId,
            nextAssigneeId,
            now);
        var auditAction = requestedStatus == "rejected"
            ? "退回成绩复议"
            : finalStep ? "复议终审并更新成绩" : "处理成绩复议";
        var detail = $"{Text(profile.Data["name"])} · {Text(exam?.Data["name"])} · {Text(subject?["name"])}" +
                     (requestedStatus == "approved" && finalStep ? $" · {AdminResultStatistics.Format(reviewedScore)} 分" : "");
        await repository.ReviewAppealAsync(
            updatedInstance,
            action,
            updatedResult,
            calculation,
            Audit(user, auditAction, detail),
            cancellationToken);
        if (updatedResult is not null)
        {
            await cache.InvalidateAsync("results");
        }
        var responseResult = updatedResult ?? result;
        var resultOutput = RawResultJson(responseResult);
        resultOutput["rank"] = JsonValue.Create(responseResult.Rank);
        resultOutput["cohortSize"] = responseResult.CohortSize;
        resultOutput["rankPercent"] = JsonValue.Create(responseResult.RankPercent);
        resultOutput["passScore"] = JsonValue.Create(responseResult.PassScore);
        resultOutput["cutoffRank"] = JsonValue.Create(responseResult.CutoffRank);
        resultOutput["qualified"] = JsonValue.Create(responseResult.Qualified);
        return Success(new JsonObject
        {
            ["ok"] = true,
            ["workflow"] = WorkflowJson(directory, updatedInstance, action),
            ["result"] = resultOutput
        });
    }

    private static JsonObject ResultJson(
        ResultLookups lookups,
        OperationalExam exam,
        AdminWorkflowResult result)
    {
        var registration = lookups.RegistrationById.GetValueOrDefault(result.RegistrationId);
        var profile = registration is null ? null : lookups.ProfileByUserId.GetValueOrDefault(registration.UserId);
        var account = registration is null ? null : lookups.AccountByUserId.GetValueOrDefault(registration.UserId);
        var schoolClass = profile?.ClassId is null ? null : lookups.ClassById.GetValueOrDefault(profile.ClassId);
        var subject = exam.Subjects.FirstOrDefault(item => Text(item["id"]) == result.SubjectId);
        var output = RawResultJson(result);
        output["rank"] = JsonValue.Create(result.Rank);
        output["cohortSize"] = result.CohortSize;
        output["rankPercent"] = JsonValue.Create(result.RankPercent);
        output["candidateName"] = Text(profile?.Data["name"]);
        output["candidateNumber"] = account?.CandidateNumber ?? Text(registration?.Data["registrationNumber"]);
        output["schoolName"] = Text(profile?.Data["school"]);
        output["className"] = schoolClass?.Name ?? Text(profile?.Data["grade"]);
        output["examId"] = exam.Id;
        output["examCode"] = Text(exam.Data["code"]);
        output["examName"] = Text(exam.Data["name"]);
        output["subjectName"] = Text(subject?["name"]);
        output["fullScore"] = subject?["fullScore"]?.DeepClone();
        output["passRule"] = Text(subject?["passRule"]) is { Length: > 0 } rule ? rule : "fixed_score";
        output["passValue"] = (subject?["passValue"] ?? subject?["passScore"])?.DeepClone();
        output["passScore"] = JsonValue.Create(result.PassScore);
        output["cutoffRank"] = JsonValue.Create(result.CutoffRank);
        output["passText"] = AdminResultStatistics.PassText(subject);
        output["qualified"] = JsonValue.Create(result.Qualified);
        return output;
    }

    private static JsonObject RegistrationJson(
        ResultLookups lookups,
        OperationalRegistration registration)
    {
        var output = registration.Data.DeepClone().AsObject();
        var exam = lookups.Operational.Exams.FirstOrDefault(item => item.Id == registration.ExamId);
        var subjects = (exam?.Subjects ?? [])
            .Where(item => SubjectIds(registration).Contains(Text(item["id"])))
            .Select(item => item.DeepClone())
            .ToArray();
        output["exam"] = exam?.Data.DeepClone();
        output["subjects"] = new JsonArray(subjects);
        output["amountDue"] = Math.Round(subjects.Sum(item => Number(item?["fee"])), 2, MidpointRounding.AwayFromZero);
        output["paidByName"] = lookups.Operational.Users.FirstOrDefault(item => item.Id == registration.PaidBy)?.DisplayName ?? "";
        var instance = lookups.Directory.Instances.FirstOrDefault(item =>
            item.BusinessType == "registration_review" && item.BusinessId == registration.Id && item.Status == "pending") ??
            lookups.Directory.Instances.FirstOrDefault(item =>
                item.BusinessType == "registration_review" && item.BusinessId == registration.Id);
        output["workflow"] = instance is null ? null : WorkflowJson(lookups.Directory, instance);
        var profile = lookups.ProfileByUserId.GetValueOrDefault(registration.UserId);
        var account = lookups.AccountByUserId.GetValueOrDefault(registration.UserId);
        var schoolClass = profile?.ClassId is null ? null : lookups.ClassById.GetValueOrDefault(profile.ClassId);
        output["candidateName"] = Text(profile?.Data["name"]) is { Length: > 0 } name ? name : account?.DisplayName ?? "";
        output["candidateNumber"] = account?.CandidateNumber ?? Text(registration.Data["registrationNumber"]);
        output["schoolName"] = Text(profile?.Data["school"]);
        output["className"] = schoolClass?.Name ?? Text(profile?.Data["grade"]);
        var category = Text(profile?.Data["specialtyCategory"]);
        var type = Text(profile?.Data["specialtyType"]);
        output["specialtyCategory"] = category;
        output["specialtyType"] = type;
        output["specialtyLabel"] = SpecialtyLabel(category, type);
        return output;
    }

    private static JsonObject WorkflowJson(
        AdminAccountBatchSnapshot snapshot,
        AccountWorkflowInstance instance,
        AccountWorkflowAction? extraAction = null)
    {
        var workflow = snapshot.Workflows.FirstOrDefault(item => item.Id == instance.WorkflowId);
        var assignee = snapshot.Users.FirstOrDefault(item => item.Id == instance.AssigneeId);
        var actions = snapshot.Actions.Where(item => item.InstanceId == instance.Id)
            .Concat(extraAction is null ? [] : [extraAction])
            .Select(item => ActionJson(snapshot, item))
            .ToArray();
        return new JsonObject
        {
            ["id"] = instance.Id,
            ["workflowId"] = instance.WorkflowId,
            ["businessType"] = instance.BusinessType,
            ["businessId"] = instance.BusinessId,
            ["status"] = instance.Status,
            ["currentStep"] = instance.CurrentStep,
            ["assigneeId"] = JsonValue.Create(instance.AssigneeId),
            ["createdAt"] = instance.CreatedAt,
            ["completedAt"] = JsonValue.Create(instance.CompletedAt),
            ["workflowName"] = workflow?.Name ?? "未命名流程",
            ["steps"] = new JsonArray((workflow?.Steps ?? []).Select(StepJson).ToArray()),
            ["currentStepDetail"] = workflow?.Steps.FirstOrDefault(item => item.Position == instance.CurrentStep) is { } step ? StepJson(step) : null,
            ["assignee"] = assignee is null ? null : SafeUser(assignee),
            ["actions"] = new JsonArray(actions)
        };
    }

    private async Task<ResolvedAdmin> ResolveAsync(
        string token,
        bool superOnly,
        CancellationToken cancellationToken)
    {
        if (token.Length == 0) return ResolvedAdmin.Failed(Error(401, "请先登录"));
        var userId = await authenticationState.GetSessionUserIdAsync(token);
        if (userId is null) return ResolvedAdmin.Failed(Error(401, "请先登录"));
        var user = await authenticationRepository.FindUserByIdAsync(userId, cancellationToken);
        if (user is not { Active: true, ArchivedAt: null })
            return ResolvedAdmin.Failed(Error(401, "请先登录"));
        if (user.Role != "admin") return ResolvedAdmin.Failed(Error(403, "当前账号无权执行此操作"));
        if (superOnly && Level(user) != "super")
            return ResolvedAdmin.Failed(Error(403, "当前账号无权执行此操作"));
        return new(user, null);
    }

    private static bool InScope(AuthenticationUser user, OperationalProfile profile) => Level(user) switch
    {
        "super" => true,
        "school" => user.SchoolId is not null && profile.SchoolId == user.SchoolId,
        _ => user.ClassId is not null && profile.ClassId == user.ClassId
    };

    private static HashSet<string> SubjectIds(OperationalRegistration registration) =>
        registration.Data["subjectIds"] is JsonArray items
            ? items.Select(Text).Where(item => item.Length > 0).ToHashSet(StringComparer.Ordinal)
            : [];

    private static string ResultSearchText(ResultLookups lookups, OperationalExam exam, AdminWorkflowResult result)
    {
        var registration = lookups.RegistrationById.GetValueOrDefault(result.RegistrationId);
        var profile = registration is null ? null : lookups.ProfileByUserId.GetValueOrDefault(registration.UserId);
        var account = registration is null ? null : lookups.AccountByUserId.GetValueOrDefault(registration.UserId);
        var schoolClass = profile?.ClassId is null ? null : lookups.ClassById.GetValueOrDefault(profile.ClassId);
        var subject = exam.Subjects.FirstOrDefault(item => Text(item["id"]) == result.SubjectId);
        return string.Join(' ',
            account?.CandidateNumber,
            account?.DisplayName,
            Text(profile?.Data["name"]),
            Text(profile?.Data["school"]),
            schoolClass?.Name,
            Text(subject?["name"]),
            Text(exam.Data["code"]),
            Text(exam.Data["name"]));
    }

    private static string RegistrationSearchText(ResultLookups lookups, OperationalRegistration registration)
    {
        var profile = lookups.ProfileByUserId.GetValueOrDefault(registration.UserId);
        var account = lookups.AccountByUserId.GetValueOrDefault(registration.UserId);
        var schoolClass = profile?.ClassId is null ? null : lookups.ClassById.GetValueOrDefault(profile.ClassId);
        return string.Join(' ',
            account?.CandidateNumber,
            account?.DisplayName,
            Text(profile?.Data["name"]),
            Text(profile?.Data["school"]),
            schoolClass?.Name,
            Text(profile?.Data["specialtyCategory"]),
            Text(profile?.Data["specialtyType"]));
    }

    private static PageResult Page(int total, int requestedPage, int requestedPageSize, string key)
    {
        var pageSize = requestedPageSize is 20 or 50 or 100 ? requestedPageSize : 50;
        var totalPages = Math.Max(1, (int)Math.Ceiling(total / (double)pageSize));
        var page = Math.Min(Math.Max(1, requestedPage), totalPages);
        return new(
            page,
            pageSize,
            total,
            totalPages,
            (page - 1) * pageSize,
            new JsonObject
            {
                ["page"] = page,
                ["pageSize"] = pageSize,
                ["total"] = total,
                ["totalPages"] = totalPages,
                ["key"] = key
            });
    }

    private static JsonObject RawResultJson(AdminWorkflowResult result) => new()
    {
        ["id"] = result.Id,
        ["registrationId"] = result.RegistrationId,
        ["subjectId"] = result.SubjectId,
        ["score"] = result.Score,
        ["grade"] = result.Grade,
        ["published"] = result.Published,
        ["updatedAt"] = JsonValue.Create(result.UpdatedAt),
        ["publishedAt"] = JsonValue.Create(result.PublishedAt)
    };

    private static JsonObject PublicExamJson(OperationalExam exam)
    {
        var output = exam.Data.DeepClone().AsObject();
        output["totalScore"] = exam.Subjects.Sum(item => Number(item["fullScore"]));
        var now = DateTimeOffset.UtcNow;
        var starts = DateTimeOffset.TryParse(
            Text(exam.Data["registrationStart"]),
            CultureInfo.InvariantCulture,
            DateTimeStyles.AssumeUniversal,
            out var start)
            ? start
            : DateTimeOffset.MinValue;
        var ends = DateTimeOffset.TryParse(
            Text(exam.Data["registrationEnd"]),
            CultureInfo.InvariantCulture,
            DateTimeStyles.AssumeUniversal,
            out var end)
            ? end
            : DateTimeOffset.MinValue;
        output["registrationState"] = exam.Data["archivedAt"] is not null
            ? "archived"
            : now < starts ? "upcoming" : now > ends ? "closed" : "open";
        return output;
    }

    private JsonObject CacheStatus() => new()
    {
        ["enabled"] = cache.Enabled,
        ["status"] = cache.Status
    };

    private static AdminWorkflowResult ApplyMetric(
        AdminWorkflowResult result,
        AdminResultCalculation calculation) =>
        ApplyMetric(
            result,
            calculation.Metrics.FirstOrDefault(item => item.ResultId == result.Id));

    private static AdminWorkflowResult ApplyMetric(
        AdminWorkflowResult result,
        AdminResultMetric? metric) => metric is null
        ? result
        : result with
        {
            Grade = metric.Grade,
            Rank = metric.Rank,
            CohortSize = metric.CohortSize,
            RankPercent = metric.RankPercent,
            Qualified = metric.Qualified,
            PassScore = metric.PassScore,
            CutoffRank = metric.CutoffRank
        };

    private static void Replace(List<AdminWorkflowResult> results, AdminWorkflowResult result)
    {
        var index = results.FindIndex(item => item.Id == result.Id);
        if (index >= 0) results[index] = result;
        else results.Add(result);
    }

    private static string SpecialtyLabel(string category, string type)
    {
        var categoryName = category switch { "sports" => "体育", "arts" => "艺术", _ => "" };
        var typeName = type switch
        {
            "track_field" => "田径",
            "basketball" => "篮球",
            "football" => "足球",
            "volleyball" => "排球",
            "table_tennis" => "乒乓球",
            "badminton" => "羽毛球",
            "swimming" => "游泳",
            "martial_arts" => "武术",
            "vocal_music" => "声乐",
            "instrumental_music" => "器乐",
            "dance" => "舞蹈",
            "fine_arts" => "美术",
            "calligraphy" => "书法",
            "broadcasting" => "播音主持",
            "drama" => "戏剧表演",
            _ => ""
        };
        return categoryName.Length > 0 && typeName.Length > 0 ? $"{categoryName}·{typeName}" : "普通生";
    }

    private static AdminUser? SelectAssignee(
        AdminAccountBatchSnapshot snapshot,
        string level,
        OperationalProfile profile)
    {
        var eligible = snapshot.Users.Where(item =>
            item.Role == "admin" &&
            item.Active &&
            item.AdminLevel == level &&
            level switch
            {
                "super" => true,
                "school" => profile.SchoolId is not null && item.SchoolId == profile.SchoolId,
                "class" => profile.SchoolId is not null && profile.ClassId is not null &&
                           item.SchoolId == profile.SchoolId && item.ClassId == profile.ClassId,
                _ => false
            });
        var pending = snapshot.Instances
            .Where(item => item.Status == "pending" && item.AssigneeId is not null)
            .GroupBy(item => item.AssigneeId!)
            .ToDictionary(group => group.Key, group => group.Count(), StringComparer.Ordinal);
        var assigned = snapshot.Actions
            .Where(item => item.ToAssigneeId is not null)
            .GroupBy(item => item.ToAssigneeId!)
            .ToDictionary(group => group.Key, group => group.Count(), StringComparer.Ordinal);
        return eligible
            .OrderBy(item => pending.GetValueOrDefault(item.Id))
            .ThenBy(item => assigned.GetValueOrDefault(item.Id))
            .ThenBy(item => item.CreatedAt, StringComparer.Ordinal)
            .ThenBy(item => item.Id, StringComparer.Ordinal)
            .FirstOrDefault();
    }

    private static string PassConclusion(JsonObject? subject, AdminWorkflowResult result)
    {
        if (Text(subject?["passRule"]) == "rank_percent")
            return $"排名前 {AdminResultStatistics.Format(Number(subject?["passValue"]))}%（当前第 {result.Rank} 名，截止第 {result.CutoffRank} 名）";
        return $"固定及格线 {AdminResultStatistics.Format(result.PassScore ?? 0)} 分";
    }

    private static JsonObject StepJson(AccountWorkflowStep item) => new()
    {
        ["id"] = item.Id,
        ["name"] = item.Name,
        ["adminLevel"] = item.AdminLevel,
        ["position"] = item.Position
    };

    private static JsonObject ActionJson(AdminAccountBatchSnapshot snapshot, AccountWorkflowAction item) => new()
    {
        ["id"] = item.Id,
        ["instanceId"] = item.InstanceId,
        ["actorId"] = JsonValue.Create(item.ActorId),
        ["action"] = item.Action,
        ["note"] = item.Note,
        ["fromAssigneeId"] = JsonValue.Create(item.FromAssigneeId),
        ["toAssigneeId"] = JsonValue.Create(item.ToAssigneeId),
        ["createdAt"] = item.CreatedAt,
        ["actorName"] = snapshot.Users.FirstOrDefault(user => user.Id == item.ActorId)?.DisplayName ?? "系统",
        ["fromAssigneeName"] = snapshot.Users.FirstOrDefault(user => user.Id == item.FromAssigneeId)?.DisplayName ?? "",
        ["toAssigneeName"] = snapshot.Users.FirstOrDefault(user => user.Id == item.ToAssigneeId)?.DisplayName ?? ""
    };

    private static JsonObject SafeUser(AdminUser item) => new()
    {
        ["id"] = item.Id,
        ["username"] = item.Username,
        ["role"] = item.Role,
        ["adminLevel"] = item.Role == "admin" ? item.AdminLevel ?? "super" : null,
        ["schoolId"] = JsonValue.Create(item.SchoolId),
        ["classId"] = JsonValue.Create(item.ClassId),
        ["displayName"] = item.DisplayName,
        ["candidateNumber"] = JsonValue.Create(item.CandidateNumber),
        ["mustChangePassword"] = item.MustChangePassword,
        ["totpEnabled"] = item.TotpEnabled,
        ["archived"] = item.ArchivedAt is not null
    };

    private static string Level(AuthenticationUser user) => user.AdminLevel ?? "super";
    private static bool JsBoolean(JsonNode? node)
    {
        if (node is null) return false;
        if (node is JsonValue value && value.TryGetValue<bool>(out var boolean)) return boolean;
        if (node is JsonValue text && text.TryGetValue<string>(out var stringValue)) return stringValue.Length > 0;
        if (node is JsonValue number && number.TryGetValue<double>(out var numberValue)) return numberValue != 0 && !double.IsNaN(numberValue);
        return true;
    }
    private static double Number(JsonNode? node) =>
        double.TryParse(Text(node), NumberStyles.Float, CultureInfo.InvariantCulture, out var value)
            ? value
            : double.NaN;
    private static int Integer(JsonNode? node, int fallback = 0) =>
        int.TryParse(Text(node), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) ? value : fallback;
    private static string Text(JsonNode? node) =>
        node is JsonValue value && value.TryGetValue<string>(out var text) ? text : node?.ToString() ?? "";
    private static string Clean(string value, int maximum)
    {
        var cleaned = value.Trim();
        return cleaned[..Math.Min(cleaned.Length, maximum)];
    }
    private static string NowIso() =>
        DateTimeOffset.UtcNow.ToString("yyyy-MM-dd'T'HH:mm:ss.fff'Z'", CultureInfo.InvariantCulture);
    private static string Uid(string prefix) =>
        $"{prefix}_{ToBase36(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds())}_{Convert.ToHexStringLower(RandomNumberGenerator.GetBytes(4))}";
    private static string ToBase36(long value)
    {
        const string alphabet = "0123456789abcdefghijklmnopqrstuvwxyz";
        Span<char> buffer = stackalloc char[16];
        var position = buffer.Length;
        do
        {
            buffer[--position] = alphabet[(int)(value % 36)];
            value /= 36;
        }
        while (value > 0);
        return new(buffer[position..]);
    }
    private static AdminAuditEntry Audit(AuthenticationUser user, string action, string detail) =>
        new(Uid("log"), user.Id, action, detail, NowIso());
    private static AdminEndpointResult Success(JsonObject body) => new(200, body);
    private static AdminEndpointResult Error(int status, string message) =>
        new(status, new JsonObject { ["ok"] = false, ["message"] = message });
    private static AdminDocumentResult DocumentError(AdminEndpointResult error) =>
        new(error.StatusCode, null, null, null, error.Body);

    private sealed record PreparedResult(
        OperationalRegistration Registration,
        AdminWorkflowResult? Existing,
        AdminWorkflowResult Result);
    private sealed record PreparedImport(
        OperationalRegistration Registration,
        OperationalExam Exam,
        JsonObject Subject,
        AdminWorkflowResult? Existing,
        AdminWorkflowResult Result,
        string CandidateNumber);
    private sealed record PageResult(
        int Page,
        int PageSize,
        int Total,
        int TotalPages,
        int Offset,
        JsonObject Json);
    private sealed record ResultLookups(
        AdminOperationalSnapshot Operational,
        AdminAccountBatchSnapshot Directory,
        IReadOnlyDictionary<string, OperationalRegistration> RegistrationById,
        IReadOnlyDictionary<string, OperationalProfile> ProfileByUserId,
        IReadOnlyDictionary<string, OperationalUser> AccountByUserId,
        IReadOnlyDictionary<string, OperationalUser> AccountByRegistrationId,
        IReadOnlyDictionary<string, AdminClass> ClassById)
    {
        public static ResultLookups Create(
            AdminOperationalSnapshot operational,
            AdminAccountBatchSnapshot directory)
        {
            var registrations = operational.Registrations.ToDictionary(item => item.Id, StringComparer.Ordinal);
            var accounts = operational.Users.ToDictionary(item => item.Id, StringComparer.Ordinal);
            return new(
                operational,
                directory,
                registrations,
                operational.Profiles.ToDictionary(item => item.UserId, StringComparer.Ordinal),
                accounts,
                registrations.Values
                    .Where(item => accounts.ContainsKey(item.UserId))
                    .ToDictionary(item => item.Id, item => accounts[item.UserId], StringComparer.Ordinal),
                directory.Classes.ToDictionary(item => item.Id, StringComparer.Ordinal));
        }
    }
    private sealed record ResolvedAdmin(AuthenticationUser? User, AdminEndpointResult? Error)
    {
        public static ResolvedAdmin Failed(AdminEndpointResult error) => new(null, error);
    }
}
