using System.Globalization;
using System.Text.Json.Nodes;
using Eis.Application.Candidate;

namespace Eis.Infrastructure.Candidate;

internal sealed partial class CandidateService
{
    private static readonly IReadOnlyDictionary<string, (string Category, string Type)> LegacySpecialties =
        new Dictionary<string, (string Category, string Type)>(StringComparer.Ordinal)
        {
            ["田径"] = ("sports", "track_field"),
            ["篮球"] = ("sports", "basketball"),
            ["足球"] = ("sports", "football"),
            ["排球"] = ("sports", "volleyball"),
            ["乒乓球"] = ("sports", "table_tennis"),
            ["羽毛球"] = ("sports", "badminton"),
            ["游泳"] = ("sports", "swimming"),
            ["武术"] = ("sports", "martial_arts"),
            ["健美操与啦啦操"] = ("sports", "aerobics_cheer"),
            ["声乐"] = ("arts", "vocal_music"),
            ["器乐"] = ("arts", "instrumental_music"),
            ["舞蹈"] = ("arts", "dance"),
            ["美术"] = ("arts", "fine_arts"),
            ["书法"] = ("arts", "calligraphy"),
            ["戏剧与播音"] = ("arts", "drama_broadcasting")
        };

    public async Task<CandidateEndpointResult> GetAdmissionsAsync(
        string sessionToken,
        string verificationBaseUrl,
        CancellationToken cancellationToken)
    {
        var context = await ResolveAsync(sessionToken, profileRoute: false, cancellationToken);
        if (context.Error is not null)
        {
            return context.Error;
        }

        var user = context.User!;
        var profile = context.Profile!;
        var snapshot = await snapshotLoader.LoadAsync(user.Id, cancellationToken);
        var admission = await admissionRepository.LoadAsync(cancellationToken);
        var settings = new List<JsonObject>();
        foreach (var setting in Records(admission, "setting").Where(item => AdmissionBoolean(item.Payload["enabled"])))
        {
            var exam = snapshot.Exams.FirstOrDefault(item => item.Id == setting.ExamId);
            if (exam is null)
            {
                continue;
            }

            var round = Math.Max(1, AdmissionInteger(setting.Payload["round"], 1));
            var preference = ActivePreference(admission, setting.ExamId, user.Id, round);
            var qualification = Records(admission, "indicator_qualification", setting.ExamId)
                .FirstOrDefault(item => item.UserId == user.Id);
            var placement = Records(admission, "placement", setting.ExamId)
                .FirstOrDefault(item => item.UserId == user.Id && item.Status != "withdrawn");
            var blocking = setting.Status == "supplementary"
                ? Records(admission, "placement", setting.ExamId).FirstOrDefault(item =>
                    item.UserId == user.Id && BlockingPlacementStatuses.Contains(item.Status, StringComparer.Ordinal))
                : null;
            var supplementEligible = blocking is null;
            var supplementarySchools = SupplementarySchoolIds(admission, setting);
            var plans = supplementEligible
                ? BuildAvailablePlans(admission, profile, setting.ExamId, qualification, supplementarySchools)
                : [];
            var registration = snapshot.Registrations.FirstOrDefault(item => item.ExamId == setting.ExamId);
            var submissionCount = AdmissionInteger(preference?.Payload["submissionCount"]);
            var maxSubmissions = Math.Max(1, AdmissionInteger(setting.Payload["maxSubmissions"], 3));
            var school = placement?.SchoolId is not null
                ? admission.Schools.GetValueOrDefault(placement.SchoolId)
                : null;
            var templateRecord = placement?.SchoolId is not null
                ? Records(admission, "notification").FirstOrDefault(item => item.SchoolId == placement.SchoolId && item.Status == "template")
                : null;
            var verificationCode = placement?.Status == "final"
                ? documentCodes.AdmissionNoticeCode(
                    placement.Id,
                    placement.UserId ?? string.Empty,
                    placement.SchoolId ?? string.Empty,
                    exam.Id,
                    AdmissionText(placement.Payload["categoryCode"]),
                    AdmissionText(placement.Payload["noticeNumber"]),
                    placement.UpdatedAt)
                : string.Empty;

            var item = AdmissionRecordJson(setting);
            item["exam"] = PublicExamJson(exam);
            item["preference"] = preference is null ? null : PreferenceView(admission, preference);
            if (placement is not null)
            {
                item["placement"] = AdmissionRecordJson(placement);
            }
            item["placementSchool"] = school is null
                ? null
                : new JsonObject { ["id"] = school.Id, ["name"] = school.Name, ["code"] = school.Code };
            item["noticeTemplate"] = templateRecord?.Payload["template"]?.DeepClone();
            item["noticeVerificationCode"] = verificationCode;
            item["noticeVerificationQr"] = verificationCode.Length == 0
                ? string.Empty
                : CreateQrCodeDataUrl($"{verificationBaseUrl}/#verify/{Uri.EscapeDataString(verificationCode)}");
            item["noticeNumber"] = AdmissionText(placement?.Payload["noticeNumber"]);
            item["plans"] = new JsonArray(plans.Select(plan => (JsonNode)plan).ToArray());
            item["supplementEligible"] = supplementEligible;
            item["supplementIneligibilityReason"] = blocking?.Status switch
            {
                "forfeited" => "因本轮未按规定完成报到，不能再次参加补录。",
                not null => "你已被录取，本轮补录无需且不能再次填报。",
                _ => string.Empty
            };
            item["totalScore"] = JsonValue.Create(CandidateTotalScore(snapshot, setting.ExamId));
            item["featureScore"] = registration?.FeatureScore ?? 0;
            item["specialtyQualification"] = SpecialtyQualification(profile);
            item["indicatorQualification"] = qualification is null ? null : AdmissionRecordJson(qualification);
            item["submissionCount"] = submissionCount;
            item["maxSubmissions"] = maxSubmissions;
            item["remainingSubmissions"] = Math.Max(0, maxSubmissions - submissionCount);
            item["preferenceLocked"] = submissionCount >= maxSubmissions;
            settings.Add(item);
        }

        var notifications = Records(admission, "notification")
            .Where(item => item.UserId == user.Id)
            .OrderByDescending(item => ParseDate(item.CreatedAt))
            .Select(item =>
            {
                var placement = Records(admission, "placement", item.ExamId)
                    .FirstOrDefault(entry => entry.Id == AdmissionText(item.Payload["placementId"]));
                var schoolId = placement?.SchoolId ?? item.SchoolId;
                var school = schoolId is not null ? admission.Schools.GetValueOrDefault(schoolId) : null;
                var exam = snapshot.Exams.FirstOrDefault(entry => entry.Id == item.ExamId);
                var output = AdmissionRecordJson(item);
                output["examName"] = exam?.Name ?? string.Empty;
                output["schoolName"] = school?.Name ?? string.Empty;
                output["schoolCode"] = school?.Code ?? string.Empty;
                output["categoryName"] = AdmissionText(placement?.Payload["categoryName"]);
                output["noticeNumber"] = AdmissionText(placement?.Payload["noticeNumber"]);
                output["placementStatus"] = placement?.Status ?? string.Empty;
                return output;
            })
            .ToArray();

        return Success(new JsonObject
        {
            ["ok"] = true,
            ["admissions"] = new JsonArray(settings.Select(item => (JsonNode)item).ToArray()),
            ["notifications"] = new JsonArray(notifications.Select(item => (JsonNode)item).ToArray())
        });
    }

    public async Task<CandidateEndpointResult> UpdateAdmissionPreferencesAsync(
        string sessionToken,
        string examId,
        JsonObject body,
        CancellationToken cancellationToken)
    {
        var context = await ResolveAsync(sessionToken, profileRoute: false, cancellationToken);
        if (context.Error is not null)
        {
            return context.Error;
        }

        var user = context.User!;
        var profile = context.Profile!;
        var snapshot = await snapshotLoader.LoadAsync(user.Id, cancellationToken);
        var admission = await admissionRepository.LoadAsync(cancellationToken);
        var setting = Records(admission, "setting", examId).FirstOrDefault();
        if (setting is null || !AdmissionBoolean(setting.Payload["enabled"]))
        {
            return Error(404, "该考试未开放志愿填报");
        }

        if (setting.Status is not ("filling" or "supplementary"))
        {
            return Error(409, "当前不在志愿填报阶段");
        }

        if (setting.Status == "supplementary")
        {
            var blocking = Records(admission, "placement", setting.ExamId).FirstOrDefault(item =>
                item.UserId == user.Id && BlockingPlacementStatuses.Contains(item.Status, StringComparer.Ordinal));
            if (blocking?.Status == "forfeited")
            {
                return Error(403, "因未按规定完成报到，本轮不能再次参加补录");
            }

            if (blocking is not null)
            {
                return Error(403, "你已被录取，本轮补录不能再次填报");
            }
        }

        var now = DateTimeOffset.UtcNow;
        if (TryAdmissionDate(AdmissionText(setting.Payload["preferenceStart"]), out var start) && now < start)
        {
            return Error(409, "志愿填报尚未开始");
        }

        if (TryAdmissionDate(AdmissionText(setting.Payload["preferenceEnd"]), out var end) && now > end)
        {
            return Error(409, "志愿填报已经截止");
        }

        if (CandidateTotalScore(snapshot, setting.ExamId) is null)
        {
            return Error(403, "本场考试成绩全部发布后才能填报志愿");
        }

        var maxChoices = Math.Max(1, AdmissionInteger(setting.Payload["maxChoices"], 5));
        var round = Math.Max(1, AdmissionInteger(setting.Payload["round"], 1));
        var current = ActivePreference(admission, setting.ExamId, user.Id, round);
        var maxSubmissions = Math.Max(1, AdmissionInteger(setting.Payload["maxSubmissions"], 3));
        var submissionCount = AdmissionInteger(current?.Payload["submissionCount"]);
        if (submissionCount >= maxSubmissions)
        {
            return Error(409, $"志愿已达到 {maxSubmissions} 次提交上限，现已自动锁定");
        }

        var choices = body["choices"] is JsonArray requested
            ? requested.Take(maxChoices + 1).OfType<JsonObject>().Select(item => new AdmissionChoice(
                Clean(item, "schoolId", 64),
                Clean(item, "categoryCode", 40),
                Text(item, "preferenceType") == "indicator" ? "indicator" : "general")).ToArray()
            : [];
        if (choices.Length == 0)
        {
            return Error(400, "请至少选择一个志愿");
        }

        var indicatorChoices = choices.Count(item => item.PreferenceType == "indicator");
        var generalChoices = choices.Count(item => item.PreferenceType == "general");
        if (indicatorChoices > 1 || generalChoices > maxChoices)
        {
            return Error(400, $"本轮最多填报 1 个指标分配志愿和 {maxChoices} 个普通志愿");
        }

        if (indicatorChoices > 0 && choices[0].PreferenceType != "indicator")
        {
            return Error(400, "指标分配志愿必须位于专用第一栏");
        }

        if (choices.Select(item => $"{item.PreferenceType}|{item.SchoolId}|{item.CategoryCode}")
            .Distinct(StringComparer.Ordinal).Count() != choices.Length)
        {
            return Error(400, "同类志愿中同一学校和招生类别不能重复填报");
        }

        var supplementarySchools = SupplementarySchoolIds(admission, setting);
        var plans = Records(admission, "plan", setting.ExamId)
            .Where(item => item.Status == "approved" &&
                (supplementarySchools is null || item.SchoolId is not null && supplementarySchools.Contains(item.SchoolId)))
            .ToArray();
        var qualification = Records(admission, "indicator_qualification", setting.ExamId)
            .FirstOrDefault(item => item.UserId == user.Id);
        if (choices.Any(choice => !ValidAdmissionChoice(admission, profile, plans, qualification, choice)))
        {
            return Error(400, "志愿中包含未审核通过、无剩余对应计划或与本人资格不符的招生类别");
        }

        var nowValue = NowIso();
        var storedChoices = choices.Select(choice =>
        {
            var school = admission.Schools.GetValueOrDefault(choice.SchoolId);
            var category = PlanCategories(plans.First(item => item.SchoolId == choice.SchoolId))
                .First(item => AdmissionText(item["code"]) == choice.CategoryCode);
            return new JsonObject
            {
                ["schoolId"] = choice.SchoolId,
                ["categoryCode"] = choice.CategoryCode,
                ["preferenceType"] = choice.PreferenceType,
                ["schoolCode"] = school?.Code ?? string.Empty,
                ["schoolName"] = school?.Name ?? string.Empty,
                ["categoryName"] = AdmissionText(category["name"])
            };
        }).ToArray();
        var preference = new CandidateAdmissionRecord(
            current?.Id ?? Uid("preference"),
            "preference",
            setting.ExamId,
            user.Id,
            null,
            "submitted",
            new JsonObject
            {
                ["round"] = round,
                ["choices"] = new JsonArray(storedChoices.Select(item => (JsonNode)item).ToArray()),
                ["submittedAt"] = nowValue,
                ["submissionCount"] = submissionCount + 1
            },
            current?.CreatedAt ?? nowValue,
            nowValue);
        await admissionRepository.SavePreferenceAsync(preference, cancellationToken);

        var locked = submissionCount + 1 >= maxSubmissions;
        return Success(new JsonObject
        {
            ["ok"] = true,
            ["preference"] = AdmissionRecordJson(preference),
            ["remainingSubmissions"] = Math.Max(0, maxSubmissions - submissionCount - 1),
            ["locked"] = locked,
            ["message"] = locked ? "志愿已保存并达到提交上限，现已自动锁定" : "志愿已由本人保存"
        });
    }

    private static readonly string[] BlockingPlacementStatuses =
        ["school_review", "admitted", "final", "withdrawal_pending", "forfeited"];

    private static IEnumerable<CandidateAdmissionRecord> Records(
        CandidateAdmissionSnapshot snapshot,
        string kind,
        string? examId = null) => snapshot.Records.Where(item =>
            item.Kind == kind && (examId is null || item.ExamId == examId));

    private static CandidateAdmissionRecord? ActivePreference(
        CandidateAdmissionSnapshot snapshot,
        string examId,
        string userId,
        int round) => Records(snapshot, "preference", examId).FirstOrDefault(item =>
            item.UserId == userId && AdmissionInteger(item.Payload["round"], 1) == round);

    private static JsonObject PreferenceView(
        CandidateAdmissionSnapshot snapshot,
        CandidateAdmissionRecord preference)
    {
        var output = AdmissionRecordJson(preference);
        var payload = preference.Payload.DeepClone().AsObject();
        var choices = preference.Payload["choices"]?.AsArray().OfType<JsonObject>().Select(choice =>
        {
            var item = choice.DeepClone().AsObject();
            var schoolId = AdmissionText(choice["schoolId"]);
            var categoryCode = AdmissionText(choice["categoryCode"]);
            var school = snapshot.Schools.GetValueOrDefault(schoolId);
            var categories = Records(snapshot, "plan", preference.ExamId)
                .Where(plan => plan.SchoolId == schoolId)
                .SelectMany(PlanCategories)
                .ToArray();
            var category = categories.FirstOrDefault(entry => AdmissionText(entry["code"]) == categoryCode)
                ?? (categoryCode == "general" ? categories.FirstOrDefault(entry =>
                    AdmissionText(entry["specialtyCategory"]).Length == 0 && AdmissionText(entry["specialtyType"]).Length == 0) : null)
                ?? (categoryCode is "sport" or "sports" ? categories.FirstOrDefault(entry => AdmissionText(entry["specialtyCategory"]) == "sports") : null)
                ?? (categoryCode is "art" or "arts" ? categories.FirstOrDefault(entry => AdmissionText(entry["specialtyCategory"]) == "arts") : null);
            item["schoolCode"] = AdmissionText(choice["schoolCode"]).Length > 0 ? AdmissionText(choice["schoolCode"]) : school?.Code ?? string.Empty;
            item["schoolName"] = AdmissionText(choice["schoolName"]).Length > 0 ? AdmissionText(choice["schoolName"]) : school?.Name ?? string.Empty;
            item["categoryName"] = AdmissionText(choice["categoryName"]).Length > 0 ? AdmissionText(choice["categoryName"]) : AdmissionText(category?["name"]);
            return item;
        }).ToArray() ?? [];
        payload["choices"] = new JsonArray(choices.Select(item => (JsonNode)item).ToArray());
        output["payload"] = payload;
        return output;
    }

    private static IReadOnlyList<JsonObject> BuildAvailablePlans(
        CandidateAdmissionSnapshot snapshot,
        JsonObject profile,
        string examId,
        CandidateAdmissionRecord? qualification,
        IReadOnlySet<string>? supplementarySchools)
    {
        var output = new List<JsonObject>();
        foreach (var plan in Records(snapshot, "plan", examId).Where(item => item.Status == "approved" &&
                     (supplementarySchools is null || item.SchoolId is not null && supplementarySchools.Contains(item.SchoolId))))
        {
            var school = plan.SchoolId is not null ? snapshot.Schools.GetValueOrDefault(plan.SchoolId) : null;
            var placements = Records(snapshot, "placement", examId).Where(item =>
                item.SchoolId == plan.SchoolId && item.Status is not ("withdrawn" or "forfeited")).ToArray();
            var categories = new List<JsonObject>();
            foreach (var source in PlanCategories(plan))
            {
                if (!CandidateEligibleForCategory(profile, source))
                {
                    continue;
                }

                var category = source.DeepClone().AsObject();
                var categoryCode = AdmissionText(source["code"]);
                var quota = AdmissionNumber(source["quota"]);
                var allocations = source["indicatorAllocations"]?.AsArray().OfType<JsonObject>().ToArray() ?? [];
                var used = placements.Count(item => AdmissionText(item.Payload["categoryCode"]) == categoryCode);
                category["used"] = used;
                category["remaining"] = Math.Max(0, quota - used);
                var allocation = allocations.FirstOrDefault(item => AdmissionText(item["sourceSchoolId"]) == Text(profile, "schoolId"));
                var indicatorUsed = placements.Count(item => AdmissionText(item.Payload["categoryCode"]) == categoryCode &&
                    AdmissionText(item.Payload["quotaBucket"]) == $"indicator:{Text(profile, "schoolId")}");
                var generalQuota = Math.Max(0, quota - allocations.Sum(item => AdmissionNumber(item["quota"])));
                var generalUsed = placements.Count(item => AdmissionText(item.Payload["categoryCode"]) == categoryCode &&
                    AdmissionText(item.Payload["quotaBucket"]) == "general");
                var indicatorRemaining = Math.Max(0, AdmissionNumber(allocation?["quota"]) - indicatorUsed);
                var generalRemaining = Math.Max(0, generalQuota - generalUsed);
                var preferenceTypes = new List<JsonNode>();
                if (generalRemaining > 0) preferenceTypes.Add(JsonValue.Create("general")!);
                if (AdmissionBoolean(qualification?.Payload["eligible"]) && indicatorRemaining > 0) preferenceTypes.Add(JsonValue.Create("indicator")!);
                category["generalRemaining"] = generalRemaining;
                category["indicatorRemaining"] = indicatorRemaining;
                category["preferenceTypes"] = new JsonArray(preferenceTypes.ToArray());
                if (preferenceTypes.Count > 0)
                {
                    categories.Add(category);
                }
            }

            if (categories.Count > 0)
            {
                output.Add(new JsonObject
                {
                    ["id"] = plan.Id,
                    ["schoolId"] = plan.SchoolId,
                    ["schoolCode"] = school?.Code ?? string.Empty,
                    ["schoolName"] = school?.Name ?? string.Empty,
                    ["categories"] = new JsonArray(categories.Select(item => (JsonNode)item).ToArray())
                });
            }
        }

        return output;
    }

    private static bool ValidAdmissionChoice(
        CandidateAdmissionSnapshot snapshot,
        JsonObject profile,
        IReadOnlyList<CandidateAdmissionRecord> plans,
        CandidateAdmissionRecord? qualification,
        AdmissionChoice choice)
    {
        var plan = plans.FirstOrDefault(item => item.SchoolId == choice.SchoolId);
        var category = plan is null
            ? null
            : PlanCategories(plan).FirstOrDefault(item => AdmissionText(item["code"]) == choice.CategoryCode);
        if (plan is null || category is null || !CandidateEligibleForCategory(profile, category))
        {
            return false;
        }

        var placements = Records(snapshot, "placement", plan.ExamId).Where(item =>
            item.SchoolId == plan.SchoolId && AdmissionText(item.Payload["categoryCode"]) == choice.CategoryCode &&
            item.Status is not ("withdrawn" or "forfeited")).ToArray();
        var allocations = category["indicatorAllocations"]?.AsArray().OfType<JsonObject>().ToArray() ?? [];
        if (choice.PreferenceType == "indicator")
        {
            var schoolId = Text(profile, "schoolId");
            var allocation = allocations.FirstOrDefault(item => AdmissionText(item["sourceSchoolId"]) == schoolId);
            var used = placements.Count(item => AdmissionText(item.Payload["quotaBucket"]) == $"indicator:{schoolId}");
            return AdmissionBoolean(qualification?.Payload["eligible"]) && AdmissionNumber(allocation?["quota"]) > used;
        }

        var quota = AdmissionNumber(category["quota"]) - allocations.Sum(item => AdmissionNumber(item["quota"]));
        return quota > placements.Count(item => AdmissionText(item.Payload["quotaBucket"]) == "general");
    }

    private static IReadOnlySet<string>? SupplementarySchoolIds(
        CandidateAdmissionSnapshot snapshot,
        CandidateAdmissionRecord setting)
    {
        if (setting.Status != "supplementary")
        {
            return null;
        }

        var sourceRound = Math.Max(1, AdmissionInteger(setting.Payload["round"], 1) - 1);
        var ids = Records(snapshot, "notification", setting.ExamId)
            .Where(item => item.UserId is null && item.Status == "approved" &&
                AdmissionText(item.Payload["type"]) == "admission_reporting" &&
                AdmissionInteger(item.Payload["round"], 1) == sourceRound &&
                AdmissionText(item.Payload["supplementDecision"]) == "supplement" && item.SchoolId is not null)
            .Select(item => item.SchoolId!)
            .ToHashSet(StringComparer.Ordinal);
        return ids.Count == 0 ? null : ids;
    }

    private static double? CandidateTotalScore(CandidateReadSnapshot snapshot, string examId)
    {
        var registration = snapshot.Registrations.FirstOrDefault(item => item.ExamId == examId && item.Status == "approved");
        if (registration is null || registration.SubjectIds.Count == 0)
        {
            return null;
        }

        var summary = snapshot.ResultSummaries.FirstOrDefault(item => item.RegistrationId == registration.Id);
        return summary is { Complete: true } ? summary.TotalScore : null;
    }

    private static bool CandidateEligibleForCategory(JsonObject profile, JsonObject category)
    {
        var requiredCategory = AdmissionText(category["specialtyCategory"]);
        var requiredType = AdmissionText(category["specialtyType"]);
        if (requiredCategory.Length == 0 && LegacySpecialties.TryGetValue(requiredType, out var legacy))
        {
            (requiredCategory, requiredType) = legacy;
        }

        if (requiredCategory.Length == 0)
        {
            return true;
        }

        var qualification = ResolveSpecialty(profile);
        return qualification.Category == requiredCategory &&
            (requiredType.Length == 0 || qualification.Type == requiredType);
    }

    private static JsonObject SpecialtyQualification(JsonObject profile)
    {
        var specialty = ResolveSpecialty(profile);
        return new JsonObject { ["category"] = specialty.Category, ["type"] = specialty.Type };
    }

    private static (string Category, string Type) ResolveSpecialty(JsonObject profile)
    {
        var category = Text(profile, "specialtyCategory");
        var type = Text(profile, "specialtyType");
        if (ValidSpecialty(category, type) && category.Length > 0)
        {
            return (category, type);
        }

        foreach (var value in profile["specialtyTypes"]?.AsArray() ?? [])
        {
            if (LegacySpecialties.TryGetValue(value?.ToString() ?? string.Empty, out var legacy))
            {
                return legacy;
            }
        }

        return (string.Empty, string.Empty);
    }

    private static JsonObject PublicExamJson(CandidateExam exam)
    {
        var output = ExamJson(exam);
        output["totalScore"] = exam.Subjects.Sum(item => item.FullScore);
        output["registrationState"] = exam.ArchivedAt is not null ? "archived" : RegistrationState(exam);
        return output;
    }

    private static JsonObject AdmissionRecordJson(CandidateAdmissionRecord item) => new()
    {
        ["id"] = item.Id,
        ["kind"] = item.Kind,
        ["examId"] = item.ExamId,
        ["userId"] = JsonValue.Create(item.UserId),
        ["schoolId"] = JsonValue.Create(item.SchoolId),
        ["status"] = item.Status,
        ["payload"] = item.Payload.DeepClone(),
        ["createdAt"] = item.CreatedAt,
        ["updatedAt"] = item.UpdatedAt
    };

    private static IReadOnlyList<JsonObject> PlanCategories(CandidateAdmissionRecord plan) =>
        plan.Payload["categories"]?.AsArray().OfType<JsonObject>().ToArray() ?? [];

    private static string AdmissionText(JsonNode? value) => value?.ToString() ?? string.Empty;

    private static double AdmissionNumber(JsonNode? value, double fallback = 0) =>
        double.TryParse(value?.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed) ? parsed : fallback;

    private static int AdmissionInteger(JsonNode? value, int fallback = 0) =>
        double.TryParse(value?.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed)
            ? (int)Math.Truncate(parsed)
            : fallback;

    private static bool AdmissionBoolean(JsonNode? value) =>
        bool.TryParse(value?.ToString(), out var parsed) && parsed;

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

    private sealed record AdmissionChoice(string SchoolId, string CategoryCode, string PreferenceType);
}
