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

namespace Eis.Infrastructure.Administration;

internal sealed class AdminArrangementService(
    IAuthenticationStateStore authenticationState,
    AuthenticationRepository authenticationRepository,
    AdminOperationalSnapshotLoader operationalSnapshotLoader,
    AdminCenterSnapshotLoader centerSnapshotLoader,
    AdminArrangementSnapshotLoader arrangementSnapshotLoader,
    AdminAccountBatchSnapshotLoader directorySnapshotLoader,
    AdminArrangementRepository repository) : IAdminArrangementService
{
    public async Task<AdminEndpointResult> GetAsync(
        string sessionToken,
        CancellationToken cancellationToken)
    {
        var context = await ResolveAsync(sessionToken, superOnly: false, cancellationToken);
        if (context.Error is not null) return context.Error;
        var user = context.User!;
        var operational = await operationalSnapshotLoader.LoadAsync(cancellationToken);
        var centers = await centerSnapshotLoader.LoadAsync(cancellationToken);
        var arrangements = await arrangementSnapshotLoader.LoadAsync(cancellationToken);
        var directory = await directorySnapshotLoader.LoadAsync(cancellationToken);
        var scopedRegistrations = operational.Registrations
            .Where(item => item.Status == "approved")
            .Where(item =>
            {
                var profile = operational.Profiles.FirstOrDefault(profile => profile.UserId == item.UserId);
                return profile is not null && InScope(user, profile);
            })
            .ToArray();
        var registrations = scopedRegistrations.Select(registration =>
        {
            var output = RegistrationJson(directory, operational, registration);
            var profile = operational.Profiles.First(item => item.UserId == registration.UserId);
            var candidate = profile.Data.DeepClone().AsObject();
            candidate["idNumber"] = MaskId(Text(candidate, "idNumber"));
            output["candidate"] = candidate;
            return output;
        }).ToArray();
        var plans = arrangements.Plans.Select(plan => PlanJson(arrangements, operational, plan)).ToArray();
        var visibleExams = Level(user) == "super"
            ? operational.Exams
            : operational.Exams.Where(exam => scopedRegistrations.Any(item => item.ExamId == exam.Id)).ToArray();
        var exams = visibleExams.Select(exam =>
        {
            var output = PublicExamJson(exam);
            output["approvedCount"] = scopedRegistrations.Count(item => item.ExamId == exam.Id);
            output["arrangedCount"] = scopedRegistrations.Count(item =>
                item.ExamId == exam.Id && item.Data["admitCard"] is not null);
            var plan = arrangements.Plans.FirstOrDefault(item => item.ExamId == exam.Id);
            output["plan"] = plan is null ? null : PlanJson(arrangements, operational, plan);
            return output;
        }).ToArray();
        var centerScope = Level(user) switch
        {
            "super" => centers.Centers,
            "school" => centers.Centers.Where(item => item.SchoolId == user.SchoolId).ToArray(),
            _ => []
        };
        var centerOutput = centerScope
            .Where(item => item.Status == "active")
            .Select(center => CenterJson(centers, center))
            .ToArray();
        return Success(new JsonObject
        {
            ["ok"] = true,
            ["canArrange"] = Level(user) == "super",
            ["canExportCenterMaterials"] = Level(user) == "school",
            ["scopeLabel"] = ScopeLabel(user, directory),
            ["exams"] = new JsonArray(exams),
            ["registrations"] = new JsonArray(registrations),
            ["plans"] = Level(user) == "super" ? new JsonArray(plans) : [],
            ["rules"] = Level(user) == "super"
                ? new JsonArray(arrangements.Rules.Where(item => item.Active).Select(RuleJson).ToArray())
                : [],
            ["mixingScopes"] = Level(user) == "super" ? MixingScopesJson() : [],
            ["centers"] = new JsonArray(centerOutput)
        });
    }

    public Task<AdminEndpointResult> PreviewAsync(
        string sessionToken,
        string examId,
        JsonObject body,
        CancellationToken cancellationToken) =>
        ArrangeAsync(sessionToken, examId, body, preview: true, cancellationToken);

    public Task<AdminEndpointResult> ApplyAsync(
        string sessionToken,
        string examId,
        JsonObject body,
        CancellationToken cancellationToken) =>
        ArrangeAsync(sessionToken, examId, body, preview: false, cancellationToken);

    public async Task<AdminEndpointResult> RetiredSingleAsync(
        string sessionToken,
        CancellationToken cancellationToken)
    {
        var context = await ResolveAsync(sessionToken, superOnly: false, cancellationToken);
        return context.Error ?? Error(
            410,
            "单人生成已停用，请在“准考证编排”中按整场考试预检并批量生成");
    }

    public async Task<AdminDocumentResult> ExportAsync(
        string sessionToken,
        string type,
        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 centers = await centerSnapshotLoader.LoadAsync(cancellationToken);
        var arrangements = await arrangementSnapshotLoader.LoadAsync(cancellationToken);
        var directory = await directorySnapshotLoader.LoadAsync(cancellationToken);
        var exam = operational.Exams.FirstOrDefault(item => item.Id == examId);
        if (exam is null) return DocumentError(Error(404, "请选择有效考试"));
        var scopeLabel = ScopeLabel(user, directory);
        if (type == "center-materials")
        {
            if (Level(user) != "school")
                return DocumentError(Error(403, "考点桌贴、门贴和签名单只能由维护该考点的校级管理员导出"));
            var centerIds = centers.Centers
                .Where(item => item.SchoolId == user.SchoolId)
                .Select(item => item.Id)
                .ToHashSet(StringComparer.Ordinal);
            var registrations = operational.Registrations
                .Where(item => item.ExamId == exam.Id && item.Data["admitCard"] is JsonObject card &&
                               centerIds.Contains(Text(card, "centerId")))
                .ToArray();
            var rows = BuildExportRows(operational, centers, directory, registrations);
            if (rows.Count == 0)
                return DocumentError(Error(404, "本校维护考点暂无该考试的已编排考生"));
            var content = AdminArrangementExporter.CenterMaterials(rows, $"{Text(exam.Data, "name")}｜{scopeLabel}");
            return Document(
                content,
                "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
                $"{Text(exam.Data, "name")}-{scopeLabel}-考点桌贴门贴签名单.xlsx");
        }

        var scoped = operational.Registrations.Where(item =>
        {
            if (item.ExamId != exam.Id || item.Data["admitCard"] is null) return false;
            var profile = operational.Profiles.FirstOrDefault(profile => profile.UserId == item.UserId);
            return profile is not null && InScope(user, profile);
        }).ToArray();
        if (scoped.Length == 0) return DocumentError(Error(404, "当前范围暂无已生成的准考证"));
        if (type == "admit-cards")
        {
            var html = RenderAdmitCards(
                operational,
                centers,
                arrangements.OrganizationName,
                scoped,
                $"{Text(exam.Data, "name")}-{scopeLabel}-准考证");
            return Document(
                Encoding.UTF8.GetBytes(html),
                "text/html; charset=utf-8",
                $"{Text(exam.Data, "name")}-{scopeLabel}-准考证批量打印.html");
        }
        if (type != "info") return DocumentError(Error(404, "不支持的准考证导出类型"));
        var exportRows = BuildExportRows(operational, centers, directory, scoped);
        var workbook = AdminArrangementExporter.AdmitCardInformation(
            exportRows,
            $"{Text(exam.Data, "name")}｜{scopeLabel}");
        return Document(
            workbook,
            "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
            $"{Text(exam.Data, "name")}-{scopeLabel}-准考证信息.xlsx");
    }

    private async Task<AdminEndpointResult> ArrangeAsync(
        string sessionToken,
        string examId,
        JsonObject body,
        bool preview,
        CancellationToken cancellationToken)
    {
        var context = await ResolveAsync(sessionToken, superOnly: true, cancellationToken);
        if (context.Error is not null) return context.Error;
        var user = context.User!;
        var generatedAt = NowIso();
        var operational = await operationalSnapshotLoader.LoadAsync(cancellationToken);
        var exam = operational.Exams.FirstOrDefault(item => item.Id == examId);
        if (exam?.Data["archivedAt"] is not null)
            return Error(409, "该考试已归档，不能重新编排准考证");
        var centers = await centerSnapshotLoader.LoadAsync(cancellationToken);
        var arrangements = await arrangementSnapshotLoader.LoadAsync(cancellationToken);
        var directory = await directorySnapshotLoader.LoadAsync(cancellationToken);
        ArrangementBuildResult result;
        try
        {
            result = AdminArrangementPlanner.Build(
                operational,
                centers,
                arrangements,
                directory,
                examId,
                Clean(Text(body, "mixingScope"), 20),
                Clean(Text(body, "numberRuleId"), 64),
                Clean(Text(body, "seed"), 80),
                generatedAt);
        }
        catch (AdminArrangementException exception)
        {
            return Error(exception.StatusCode, exception.Message);
        }

        if (preview)
        {
            return Success(new JsonObject
            {
                ["ok"] = true,
                ["preview"] = true,
                ["summary"] = SummaryJson(result.Summary),
                ["warnings"] = StringArray(result.Warnings),
                ["samples"] = new JsonArray(result.Cards.Take(5).Select(CardJson).ToArray())
            });
        }

        var plan = new StoredArrangementPlan(
            Uid("arrangement"),
            result.Exam.Id,
            result.Rule.Id,
            result.MixingScope,
            result.Seed,
            result.Summary.CandidateCount,
            result.Summary.CenterCount,
            result.Summary.SubjectAssignmentCount,
            result.Summary.SubjectCombinationCount,
            result.Summary.SameSchoolCenterRate,
            result.Warnings,
            user.Id,
            generatedAt);
        var replacing = arrangements.Plans.Any(item => item.ExamId == result.Exam.Id);
        await repository.SaveAsync(
            plan,
            result.Cards,
            Audit(
                user,
                replacing ? "重新编排准考证" : "批量编排准考证",
                $"{Text(result.Exam.Data, "name")} · {plan.CandidateCount} 人 · {plan.CenterCount} 个考点 · {result.Rule.Name}"),
            cancellationToken);
        var planOutput = PlanJson(plan);
        AddSummaryFields(planOutput, result.Summary);
        return Success(new JsonObject
        {
            ["ok"] = true,
            ["plan"] = planOutput,
            ["summary"] = SummaryJson(result.Summary),
            ["warnings"] = StringArray(result.Warnings),
            ["cards"] = new JsonArray(result.Cards.Select(CardJson).ToArray())
        });
    }

    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 IReadOnlyList<ArrangementExportRow> BuildExportRows(
        AdminOperationalSnapshot operational,
        AdminCenterSnapshot centers,
        AdminAccountBatchSnapshot directory,
        IReadOnlyList<OperationalRegistration> registrations)
    {
        var rows = new List<ArrangementExportRow>();
        foreach (var registration in registrations)
        {
            var profile = operational.Profiles.FirstOrDefault(item => item.UserId == registration.UserId);
            var account = operational.Users.FirstOrDefault(item => item.Id == registration.UserId);
            var exam = operational.Exams.FirstOrDefault(item => item.Id == registration.ExamId);
            var card = registration.Data["admitCard"] as JsonObject;
            if (profile is null || exam is null || card is null) continue;
            var school = directory.Schools.FirstOrDefault(item => item.Id == profile.SchoolId);
            var schoolClass = directory.Classes.FirstOrDefault(item => item.Id == profile.ClassId);
            var center = centers.Centers.FirstOrDefault(item => item.Id == Text(card, "centerId"));
            foreach (var assignment in (card["assignments"] as JsonArray ?? []).OfType<JsonObject>())
            {
                var subject = exam.Subjects.FirstOrDefault(item => Text(item, "id") == Text(assignment, "subjectId"));
                var room = centers.Rooms.FirstOrDefault(item => item.Id == Text(assignment, "roomId"));
                rows.Add(new ArrangementExportRow(
                    school?.Name ?? Text(profile.Data, "school"),
                    schoolClass?.Name ?? Text(profile.Data, "grade"),
                    First(account?.CandidateNumber ?? "", Text(registration.Data, "registrationNumber")),
                    First(Text(profile.Data, "name"), account?.DisplayName ?? ""),
                    Text(profile.Data, "idNumber"),
                    Text(exam.Data, "code"),
                    Text(exam.Data, "name"),
                    Text(card, "number"),
                    First(Text(card, "centerCode"), center?.Code ?? ""),
                    First(Text(card, "testCenter"), center?.Name ?? ""),
                    First(
                        Text(card, "centerAddress"),
                        center is null
                            ? ""
                            : string.Join(' ', new[]
                            {
                                center.ProvinceName,
                                center.CityName,
                                center.DistrictName,
                                center.Address
                            }.Where(value => value.Length > 0))),
                    subject is null ? "" : Text(subject, "name"),
                    subject is null ? "" : Text(subject, "date"),
                    subject is null
                        ? ""
                        : string.Join('—', new[] { Text(subject, "start"), Text(subject, "end") }.Where(value => value.Length > 0)),
                    Text(assignment, "examRoomCode"),
                    First(Text(assignment, "roomName"), Text(assignment, "room"), room?.Name ?? ""),
                    First(Text(assignment, "roomCode"), room?.Code ?? ""),
                    First(Text(assignment, "building"), room?.Building ?? ""),
                    First(Text(assignment, "floor"), room?.Floor ?? ""),
                    Text(assignment, "seat")));
            }
        }
        return rows;
    }

    private static string RenderAdmitCards(
        AdminOperationalSnapshot operational,
        AdminCenterSnapshot centers,
        string organizationName,
        IReadOnlyList<OperationalRegistration> registrations,
        string title)
    {
        var sections = new StringBuilder();
        foreach (var registration in registrations)
        {
            var user = operational.Users.FirstOrDefault(item => item.Id == registration.UserId);
            var profile = operational.Profiles.FirstOrDefault(item => item.UserId == registration.UserId);
            var exam = operational.Exams.FirstOrDefault(item => item.Id == registration.ExamId);
            var card = registration.Data["admitCard"] as JsonObject;
            if (profile is null || exam is null || card is null) continue;
            var assignments = (card["assignments"] as JsonArray ?? [])
                .OfType<JsonObject>()
                .ToDictionary(item => Text(item, "subjectId"), StringComparer.Ordinal);
            var subjectIds = (registration.Data["subjectIds"] as JsonArray ?? [])
                .Select(item => item?.GetValue<string>() ?? "")
                .ToHashSet(StringComparer.Ordinal);
            var rows = new StringBuilder();
            foreach (var subject in exam.Subjects.Where(item => subjectIds.Contains(Text(item, "id"))))
            {
                assignments.TryGetValue(Text(subject, "id"), out var assignment);
                var room = assignment is null
                    ? null
                    : centers.Rooms.FirstOrDefault(item => item.Id == Text(assignment, "roomId"));
                rows.Append("<tr><td>").Append(E(Text(subject, "name")))
                    .Append("</td><td>").Append(E(Text(subject, "date")))
                    .Append("</td><td>").Append(E(Text(subject, "start"))).Append('—').Append(E(Text(subject, "end")))
                    .Append("</td><td class=\"room-no\">").Append(E(First(Text(assignment, "examRoomCode"), "待定")))
                    .Append("</td><td><strong>").Append(E(First(Text(assignment, "roomName"), Text(assignment, "room"), room?.Name ?? "", "待定")))
                    .Append("</strong><small>场地代码：").Append(E(First(Text(assignment, "roomCode"), room?.Code ?? "", "—")))
                    .Append("</small></td><td>").Append(E(First(Text(assignment, "building"), room?.Building ?? "", "—")))
                    .Append("<small>").Append(E(First(Text(assignment, "floor"), room?.Floor ?? "", "楼层待定")))
                    .Append("</small></td><td>").Append(E(First(Text(assignment, "seat"), "—"))).Append("</td></tr>");
            }
            var center = centers.Centers.FirstOrDefault(item => item.Id == Text(card, "centerId"));
            var centerCode = First(Text(card, "centerCode"), center?.Code ?? "");
            var centerAddress = First(
                Text(card, "centerAddress"),
                center is null
                    ? ""
                    : string.Join(' ', new[]
                    {
                        center.ProvinceName,
                        center.CityName,
                        center.DistrictName,
                        center.Address
                    }.Where(value => value.Length > 0)));
            var generatedAt = DateTimeOffset.TryParse(
                Text(card, "generatedAt"),
                CultureInfo.InvariantCulture,
                DateTimeStyles.AssumeUniversal,
                out var parsed)
                ? parsed.ToLocalTime().ToString("yyyy/M/d HH:mm:ss", CultureInfo.GetCultureInfo("zh-CN"))
                : Text(card, "generatedAt");
            sections.Append("<main class=\"card\"><header class=\"head\"><div><div class=\"brand\">衡准 · 准考证</div><p>")
                .Append(E(Text(exam.Data, "name")))
                .Append("</p></div><div><small>准考证号</small><div class=\"code\">").Append(E(Text(card, "number")))
                .Append("</div></div></header><section class=\"meta\"><div><span>姓名</span><strong>")
                .Append(E(First(Text(profile.Data, "name"), user?.DisplayName ?? "")))
                .Append("</strong></div><div><span>证件号码</span><strong>").Append(E(MaskId(Text(profile.Data, "idNumber"))))
                .Append("</strong></div><div><span>报名号</span><strong>")
                .Append(E(First(user?.CandidateNumber ?? "", Text(registration.Data, "registrationNumber"), "—")))
                .Append("</strong></div></section><section class=\"center\"><div><span>固定考点</span><strong>")
                .Append(E(Text(card, "testCenter"))).Append("</strong><small>考点代码：").Append(E(First(centerCode, "—")))
                .Append("</small></div><div><span>考点详细地址</span><strong>").Append(E(First(centerAddress, "地址待公布")))
                .Append("</strong></div></section><table><thead><tr><th>科目</th><th>日期</th><th>时间</th><th>考试考场序号</th><th>考场通用名称 / 场地代码</th><th>楼栋 / 楼层</th><th>座位号</th></tr></thead><tbody>")
                .Append(rows)
                .Append("</tbody></table><div class=\"explain\"><strong>编号说明</strong>“考试考场序号”是本次考试编排编号；“考场通用名称 / 场地代码”是考点内的物理场地档案，两者不是同一概念。</div><div class=\"notice\">所有科目均安排在同一考点，但不同科目可能对应不同考试考场序号、物理场地和座位。请逐科核对，并携带本人有效身份证件及本准考证至少提前 40 分钟到达考点。</div><footer class=\"footer\"><span>")
                .Append(E(organizationName)).Append("</span><span>生成时间：").Append(E(generatedAt))
                .Append("</span></footer></main>");
        }
        const string styles = "@page{size:A4 landscape;margin:10mm}*{box-sizing:border-box}body{font-family:\"Microsoft YaHei\",sans-serif;color:#182552;margin:0;background:#f2f4f8}.card{max-width:1120px;min-height:720px;margin:24px auto;background:#fff;border:2px solid #182552;padding:26px;box-shadow:0 12px 35px #18255218;break-after:page}.card:last-child{break-after:auto}.head{display:flex;justify-content:space-between;align-items:flex-start;border-bottom:3px double #182552;padding-bottom:14px}.head>div:last-child{text-align:right}.head small{color:#64748b}.brand{font-size:27px;font-weight:800}.head p{margin:6px 0}.code{font:700 22px Consolas,monospace;margin-top:5px}.meta{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin:16px 0}.meta div,.center>div{border:1px solid #d9e0e7;padding:9px 12px}.meta span,.center span{display:block;color:#64748b;font-size:11px;margin-bottom:4px}.center{display:grid;grid-template-columns:1fr 1.5fr;gap:10px;margin-bottom:16px}.center small,td small{display:block;color:#64748b;margin-top:4px}table{width:100%;border-collapse:collapse;font-size:12px}th,td{border:1px solid #b9c5d2;padding:9px;text-align:left;vertical-align:middle}th{background:#eaf1f5}.room-no{font:700 15px Consolas,monospace;text-align:center}.explain,.notice{margin-top:12px;padding:10px 12px;font-size:12px}.explain{background:#eaf4f6;color:#245365}.explain strong{margin-right:8px}.notice{background:#fff7e6;color:#704b12}.footer{display:flex;justify-content:space-between;margin-top:14px;color:#64748b;font-size:11px}@media print{body{background:#fff}.card{margin:0;box-shadow:none}}";
        return $"<!doctype html><html lang=\"zh-CN\"><head><meta charset=\"utf-8\"><title>{E(title)}</title><style>{styles}</style></head><body>{sections}</body></html>";
    }

    private static JsonObject RegistrationJson(
        AdminAccountBatchSnapshot directory,
        AdminOperationalSnapshot operational,
        OperationalRegistration registration)
    {
        var output = registration.Data.DeepClone().AsObject();
        var exam = operational.Exams.FirstOrDefault(item => item.Id == registration.ExamId);
        var subjectIds = (output["subjectIds"] as JsonArray ?? [])
            .Select(item => item?.GetValue<string>() ?? "")
            .ToHashSet(StringComparer.Ordinal);
        output["exam"] = exam?.Data.DeepClone();
        output["subjects"] = new JsonArray((exam?.Subjects ?? [])
            .Where(item => subjectIds.Contains(Text(item, "id")))
            .Select(item => item.DeepClone())
            .ToArray());
        output["amountDue"] = Math.Round(
            (output["subjects"] as JsonArray ?? []).OfType<JsonObject>().Sum(item => Number(item, "fee")),
            2,
            MidpointRounding.AwayFromZero);
        output["paidByName"] = operational.Users.FirstOrDefault(item => item.Id == registration.PaidBy)?.DisplayName ?? "";
        var instance = directory.Instances.FirstOrDefault(item =>
                           item.BusinessType == "registration_review" &&
                           item.BusinessId == registration.Id &&
                           item.Status == "pending") ??
                       directory.Instances.FirstOrDefault(item =>
                           item.BusinessType == "registration_review" && item.BusinessId == registration.Id);
        output["workflow"] = instance is null ? null : WorkflowJson(directory, instance);
        return output;
    }

    private static JsonObject WorkflowJson(
        AdminAccountBatchSnapshot snapshot,
        AccountWorkflowInstance instance)
    {
        var workflow = snapshot.Workflows.FirstOrDefault(item => item.Id == instance.WorkflowId);
        var assignee = snapshot.Users.FirstOrDefault(item => item.Id == instance.AssigneeId);
        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(snapshot.Actions
                .Where(item => item.InstanceId == instance.Id)
                .Select(item => ActionJson(snapshot, item))
                .ToArray())
        };
    }

    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 state = output["archivedAt"] is not null
            ? "archived"
            : TryDate(Text(output, "registrationStart"), out var start) && now < start
                ? "upcoming"
                : TryDate(Text(output, "registrationEnd"), out var end) && now > end
                    ? "closed"
                    : "open";
        output["registrationState"] = state;
        return output;
    }

    private static JsonObject PlanJson(
        AdminArrangementSnapshot arrangements,
        AdminOperationalSnapshot operational,
        StoredArrangementPlan plan)
    {
        var output = PlanJson(plan);
        output["examName"] = operational.Exams.FirstOrDefault(item => item.Id == plan.ExamId) is { } exam
            ? Text(exam.Data, "name")
            : "";
        output["ruleName"] = arrangements.Rules.FirstOrDefault(item => item.Id == plan.NumberRuleId)?.Name ?? "";
        output["mixingScopeName"] = MixingScopeName(plan.MixingScope);
        return output;
    }

    private static JsonObject PlanJson(StoredArrangementPlan plan) => new()
    {
        ["id"] = plan.Id,
        ["examId"] = plan.ExamId,
        ["numberRuleId"] = plan.NumberRuleId,
        ["mixingScope"] = plan.MixingScope,
        ["randomSeed"] = plan.RandomSeed,
        ["candidateCount"] = plan.CandidateCount,
        ["centerCount"] = plan.CenterCount,
        ["subjectAssignmentCount"] = plan.SubjectAssignmentCount,
        ["subjectCombinationCount"] = plan.SubjectCombinationCount,
        ["sameSchoolCenterRate"] = plan.SameSchoolCenterRate,
        ["warnings"] = StringArray(plan.Warnings),
        ["generatedBy"] = JsonValue.Create(plan.GeneratedBy),
        ["generatedAt"] = plan.GeneratedAt
    };

    private static JsonObject CenterJson(AdminCenterSnapshot snapshot, AdminCenter center) => new()
    {
        ["id"] = center.Id,
        ["schoolId"] = center.SchoolId,
        ["code"] = center.Code,
        ["name"] = center.Name,
        ["provinceCode"] = center.ProvinceCode,
        ["provinceName"] = center.ProvinceName,
        ["cityCode"] = center.CityCode,
        ["cityName"] = center.CityName,
        ["districtCode"] = center.DistrictCode,
        ["districtName"] = center.DistrictName,
        ["address"] = center.Address,
        ["contact"] = center.Contact,
        ["managerName"] = center.ManagerName,
        ["managerPhone"] = center.ManagerPhone,
        ["emergencyPhone"] = center.EmergencyPhone,
        ["gateOpenTime"] = center.GateOpenTime,
        ["transport"] = center.Transport,
        ["status"] = center.Status,
        ["notes"] = center.Notes,
        ["rooms"] = center.Rooms,
        ["updatedAt"] = center.UpdatedAt,
        ["roomCount"] = snapshot.Rooms.Count(item => item.CenterId == center.Id && item.Status == "active"),
        ["capacity"] = snapshot.Rooms
            .Where(item => item.CenterId == center.Id && item.Status == "active" && item.RoomType != "spare")
            .Sum(item => item.Capacity)
    };

    private static JsonObject RuleJson(AdmissionNumberRule rule) => new()
    {
        ["id"] = rule.Id,
        ["code"] = rule.Code,
        ["name"] = rule.Name,
        ["description"] = rule.Description,
        ["separator"] = rule.Separator,
        ["segments"] = new JsonArray(rule.Segments.Select(segment =>
        {
            var output = new JsonObject
            {
                ["source"] = segment.Source,
                ["label"] = segment.Label
            };
            if (segment.Width is not null) output["width"] = segment.Width;
            return output;
        }).ToArray()),
        ["example"] = rule.Example,
        ["active"] = rule.Active,
        ["createdAt"] = rule.CreatedAt
    };

    private static JsonArray MixingScopesJson() => new(
        new JsonObject
        {
            ["code"] = "class",
            ["name"] = "班内混编",
            ["description"] = "以班级为边界，同班考生按科目组合穿插编排。"
        },
        new JsonObject
        {
            ["code"] = "school",
            ["name"] = "校内混编",
            ["description"] = "同校跨班混编，优先安排在本校考点。"
        },
        new JsonObject
        {
            ["code"] = "district",
            ["name"] = "县区内混编",
            ["description"] = "同县区跨学校混编，优先使用本县区考点。"
        },
        new JsonObject
        {
            ["code"] = "city",
            ["name"] = "市内混编",
            ["description"] = "同市跨县区混编，优先使用本市考点。"
        },
        new JsonObject
        {
            ["code"] = "province",
            ["name"] = "省内混编",
            ["description"] = "全省范围混编，按容量和科目组合选择考点。"
        });

    private static JsonObject SummaryJson(ArrangementSummary summary) => new()
    {
        ["candidateCount"] = summary.CandidateCount,
        ["centerCount"] = summary.CenterCount,
        ["subjectAssignmentCount"] = summary.SubjectAssignmentCount,
        ["subjectCombinationCount"] = summary.SubjectCombinationCount,
        ["sameSchoolCenterCount"] = summary.SameSchoolCenterCount,
        ["sameSchoolCenterRate"] = summary.SameSchoolCenterRate,
        ["reservedSpareRooms"] = summary.ReservedSpareRooms
    };

    private static void AddSummaryFields(JsonObject output, ArrangementSummary summary)
    {
        output["sameSchoolCenterCount"] = summary.SameSchoolCenterCount;
        output["reservedSpareRooms"] = summary.ReservedSpareRooms;
    }

    private static JsonObject CardJson(ArrangementCard card) => new()
    {
        ["registrationId"] = card.RegistrationId,
        ["number"] = card.Number,
        ["centerId"] = card.CenterId,
        ["testCenter"] = card.TestCenter,
        ["centerCode"] = card.CenterCode,
        ["centerAddress"] = card.CenterAddress,
        ["generatedAt"] = card.GeneratedAt,
        ["assignments"] = new JsonArray(card.Assignments.Select(AssignmentJson).ToArray())
    };

    private static JsonObject AssignmentJson(ArrangementAssignment item) => new()
    {
        ["registrationId"] = item.RegistrationId,
        ["subjectId"] = item.SubjectId,
        ["centerId"] = item.CenterId,
        ["centerName"] = item.CenterName,
        ["roomId"] = item.RoomId,
        ["roomName"] = item.RoomName,
        ["roomCode"] = item.RoomCode,
        ["building"] = item.Building,
        ["floor"] = item.Floor,
        ["examRoomCode"] = item.ExamRoomCode,
        ["seat"] = item.Seat,
        ["subjectSignature"] = item.SubjectSignature
    };

    private static JsonArray StringArray(IEnumerable<string> values) =>
        new(values.Select(value => (JsonNode?)JsonValue.Create(value)).ToArray());

    private static string ScopeLabel(AuthenticationUser user, AdminAccountBatchSnapshot directory)
    {
        if (Level(user) == "super") return "全部学校与班级";
        var school = directory.Schools.FirstOrDefault(item => item.Id == user.SchoolId)?.Name ?? "未绑定学校";
        if (Level(user) == "school") return school;
        var schoolClass = directory.Classes.FirstOrDefault(item => item.Id == user.ClassId)?.Name ?? "未绑定班级";
        return $"{school} · {schoolClass}";
    }

    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 JsonObject StepJson(AccountWorkflowStep item) => new()
    {
        ["id"] = item.Id,
        ["name"] = item.Name,
        ["adminLevel"] = item.AdminLevel,
        ["position"] = item.Position
    };

    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 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 string MixingScopeName(string value) => value switch
    {
        "class" => "班内混编",
        "school" => "校内混编",
        "district" => "县区内混编",
        "city" => "市内混编",
        "province" => "省内混编",
        _ => value
    };

    private static bool TryDate(string value, out DateTimeOffset result) =>
        DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out result);

    private static double Number(JsonObject value, string property) =>
        double.TryParse(Text(value, property), NumberStyles.Float, CultureInfo.InvariantCulture, out var number)
            ? number
            : 0;

    private static string Text(JsonObject? value, string property) =>
        value?[property]?.ToString() ?? "";

    private static string First(params string[] values) =>
        values.FirstOrDefault(value => value.Length > 0) ?? "";

    private static string Clean(string value, int maximum)
    {
        var output = value.Trim();
        return output[..Math.Min(output.Length, maximum)];
    }

    private static string MaskId(string value) =>
        value.Length > 8 ? $"{value[..4]}********{value[^4..]}" : value;

    private static string E(string? value) => WebUtility.HtmlEncode(value ?? "");

    private static string Level(AuthenticationUser user) => user.AdminLevel ?? "super";

    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 Document(byte[] content, string contentType, string fileName) =>
        new(200, content, contentType, fileName, null);

    private static AdminDocumentResult DocumentError(AdminEndpointResult error) =>
        new(error.StatusCode, null, null, null, error.Body);

    private sealed record ResolvedAdmin(AuthenticationUser? User, AdminEndpointResult? Error)
    {
        public static ResolvedAdmin Failed(AdminEndpointResult error) => new(null, error);
    }
}

internal sealed record ArrangementExportRow(
    string SchoolName,
    string ClassName,
    string CandidateNumber,
    string CandidateName,
    string IdNumber,
    string ExamCode,
    string ExamName,
    string CardNumber,
    string CenterCode,
    string CenterName,
    string CenterAddress,
    string SubjectName,
    string SubjectDate,
    string SubjectTime,
    string ExamRoomCode,
    string RoomName,
    string RoomCode,
    string Building,
    string Floor,
    string Seat);
