using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using Eis.Application.Administration;
using Eis.Infrastructure.Authentication;
using Eis.Infrastructure.Candidate;
using Eis.Infrastructure.Security;

namespace Eis.Infrastructure.Administration;

internal sealed partial class AdminAdmissionService
{
    private static readonly Regex AdmissionNoticePattern =
        new(@"AN-[A-F0-9]{24}", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);

    public async Task<AdminEndpointResult> GetAdmissionSchoolContextAsync(
        string sessionToken,
        CancellationToken cancellationToken)
    {
        var context = await ResolveAdmissionSchoolAsync(sessionToken, cancellationToken);
        if (context.Error is not null) return context.Error;
        var data = await LoadAsync(cancellationToken);
        var school = AdmissionSchool(data, context.User!);
        if (school is null) return Error(403, "招生学校账号未绑定有效学校");
        var plans = Records(data, "plan")
            .Where(item => item.SchoolId == school.Id && item.Status == "approved")
            .Select(item =>
            {
                var output = RecordJson(item);
                output["examName"] = Text(data.Operational.Exams.FirstOrDefault(exam => exam.Id == item.ExamId)?.Data["name"]);
                output["progress"] = PlanProgress(data, item);
                return (JsonNode)output;
            }).ToArray();
        var home = await publicQueries.GetHomeAsync(cancellationToken);
        var notifications = (home["notices"] as JsonArray ?? []).OfType<JsonObject>()
            .Where(item => Text(item["sourceType"]).Length > 0 &&
                           (Text(item["schoolId"]).Length == 0 || Text(item["schoolId"]) == school.Id))
            .Take(6)
            .Select(item =>
            {
                var output = item.DeepClone().AsObject();
                if (Text(output["noticeId"]).Length > 0) output["id"] = output["noticeId"]!.DeepClone();
                return (JsonNode)output;
            }).ToArray();
        var exams = data.Operational.Exams.Where(item =>
            item.Data["archivedAt"] is null &&
            Records(data, "setting", item.Id).Any(setting => Boolean(setting.Payload["enabled"])))
            .Select(item => item.Data.DeepClone()).ToArray();
        return Success(new JsonObject
        {
            ["ok"] = true,
            ["school"] = SchoolJson(school),
            ["plans"] = new JsonArray(plans),
            ["notifications"] = new JsonArray(notifications),
            ["exams"] = new JsonArray(exams)
        });
    }

    public async Task<AdminEndpointResult> GetAdmissionSchoolPlansAsync(
        string sessionToken,
        CancellationToken cancellationToken)
    {
        var context = await ResolveAdmissionSchoolAsync(sessionToken, cancellationToken);
        if (context.Error is not null) return context.Error;
        var data = await LoadAsync(cancellationToken);
        var school = AdmissionSchool(data, context.User!);
        if (school is null) return Error(403, "招生学校账号未绑定有效学校");
        var plans = Records(data, "plan").Where(item => item.SchoolId == school.Id).Select(item =>
        {
            var output = RecordJson(item);
            output["remainingCategories"] = RemainingPlanQuota(data, item);
            output["progress"] = PlanProgress(data, item);
            return (JsonNode)output;
        }).ToArray();
        return Success(new JsonObject
        {
            ["ok"] = true,
            ["school"] = SchoolJson(school),
            ["plans"] = new JsonArray(plans),
            ["exams"] = new JsonArray(data.Operational.Exams.Where(item => item.Data["archivedAt"] is null)
                .Select(item => item.Data.DeepClone()).ToArray()),
            ["sourceSchools"] = new JsonArray(data.Directory.Schools
                .Where(item => item.Active && item.IsSourceSchool)
                .Select(item => (JsonNode)SchoolJson(item)).ToArray())
        });
    }

    public async Task<AdminEndpointResult> SaveAdmissionSchoolPlanAsync(
        string sessionToken,
        JsonObject body,
        CancellationToken cancellationToken)
    {
        var context = await ResolveAdmissionSchoolAsync(sessionToken, cancellationToken);
        if (context.Error is not null) return context.Error;
        var user = context.User!;
        var data = await LoadAsync(cancellationToken);
        var school = AdmissionSchool(data, user);
        if (school is null) return Error(403, "招生学校账号未绑定有效学校");
        var examId = Clean(Text(body["examId"]), 64);
        var exam = data.Operational.Exams.FirstOrDefault(item =>
            item.Id == examId && item.Data["archivedAt"] is null);
        if (exam is null) return Error(404, "考试不存在或已经归档");
        var categories = NormalizeCategories(body["categories"]);
        if (categories.Count == 0) return Error(400, "请至少填写一个有效招生类别和计划人数");
        if (categories.Select(item => Text(item["code"])).Distinct(StringComparer.Ordinal).Count() != categories.Count)
            return Error(400, "招生类别代码不能重复");
        if (categories.Any(item => !ValidSpecialty(Text(item["specialtyCategory"]), Text(item["specialtyType"]))))
            return Error(400, "特长生招生类别的大类与小类不对应");
        foreach (var category in categories)
        {
            var allocations = (category["indicatorAllocations"] as JsonArray ?? []).OfType<JsonObject>().ToArray();
            if (allocations.Select(item => Text(item["sourceSchoolId"])).Distinct(StringComparer.Ordinal).Count() != allocations.Length)
                return Error(400, "同一招生类别不能重复分配同一生源校指标");
            if (allocations.Sum(item => Number(item["quota"])) > Number(category["quota"]))
                return Error(400, "指标分配合计不能超过该类别计划人数");
            if (allocations.Any(item => !data.Directory.Schools.Any(schoolItem =>
                    schoolItem.Id == Text(item["sourceSchoolId"]) && schoolItem.Active && schoolItem.IsSourceSchool)))
                return Error(400, "指标分配中包含无效的生源学校");
        }
        var existing = Records(data, "plan", exam.Id).FirstOrDefault(item => item.SchoolId == school.Id);
        if (existing?.Status == "approved") return Error(409, "已审核通过的招生计划只能由超级管理员调整");
        var now = NowIso();
        var plan = new CandidateAdmissionRecord(
            existing?.Id ?? Uid("plan"),
            "plan",
            exam.Id,
            existing?.UserId ?? user.Id,
            school.Id,
            "pending",
            new JsonObject
            {
                ["categories"] = new JsonArray(categories.Select(item => (JsonNode)item).ToArray()),
                ["note"] = Clean(Text(body["note"]), 500),
                ["submittedBy"] = user.DisplayName,
                ["reviewNote"] = ""
            },
            existing?.CreatedAt ?? now,
            now);
        await repository.SaveRecordsAsync(
            [plan],
            Audit(user, "提交招生计划", $"{school.Name} · {Text(exam.Data["name"])}"),
            cancellationToken);
        return new AdminEndpointResult(existing is null ? 201 : 200,
            new JsonObject { ["ok"] = true, ["plan"] = RecordJson(plan) });
    }

    public async Task<AdminEndpointResult> SaveAdmissionSchoolReportingDraftAsync(
        string sessionToken,
        JsonObject body,
        CancellationToken cancellationToken)
    {
        var resolved = await ResolveReportingAsync(sessionToken, Text(body["examId"]), cancellationToken);
        if (resolved.Error is not null) return resolved.Error;
        var available = ReportingRows(resolved.Data!, resolved.Plan!, resolved.Record!)
            .Select(item => Text(item["placementId"])).ToHashSet(StringComparer.Ordinal);
        var now = NowIso();
        var updates = (body["rows"] as JsonArray ?? []).OfType<JsonObject>().Select(item => new JsonObject
        {
            ["placementId"] = Clean(Text(item["placementId"]), 64),
            ["status"] = Clean(Text(item["status"]), 30),
            ["note"] = Clean(Text(item["note"]), 300),
            ["updatedAt"] = now,
            ["source"] = "manual"
        }).ToArray();
        if (updates.Length == 0 || updates.Any(item =>
                !available.Contains(Text(item["placementId"])) ||
                Text(item["status"]) is not ("pending" or "reported" or "not_reported")))
            return Error(400, "报到暂存数据无效");
        var merged = ReportingRowMap(resolved.Record!);
        foreach (var update in updates) merged[Text(update["placementId"])] = update;
        var updated = UpdateReportingRecord(
            resolved.Record!, "draft", merged.Values, resolved.User!.DisplayName, now,
            ("savedAt", JsonValue.Create(now)), ("savedBy", JsonValue.Create(resolved.User.DisplayName)));
        await repository.SaveRecordsAsync(
            [updated],
            Audit(resolved.User, "暂存考生报到状态", $"{resolved.School!.Name} · {updates.Length} 人"),
            cancellationToken);
        var data = ReplaceRecord(resolved.Data!, updated);
        return Success(new JsonObject
        {
            ["ok"] = true,
            ["batch"] = ReportingBatch(data, resolved.Plan!, updated)
        });
    }

    public async Task<AdminEndpointResult> PreviewAdmissionSchoolReportingScanAsync(
        string sessionToken,
        JsonObject body,
        CancellationToken cancellationToken)
    {
        var target = await ResolveScanTargetAsync(sessionToken, Text(body["code"]), cancellationToken);
        if (target.Error is not null) return target.Error;
        if (Text(body["examId"]) is { Length: > 0 } examId && Clean(examId, 64) != target.Placement!.ExamId)
            return Error(400, "二维码不属于当前考试报到批次");
        var row = ReportingRows(target.Data!, target.Plan!, target.Record!)
            .First(item => Text(item["placementId"]) == target.Placement!.Id);
        return Success(new JsonObject
        {
            ["ok"] = true,
            ["code"] = target.Code,
            ["examId"] = target.Placement!.ExamId,
            ["row"] = row
        });
    }

    public async Task<AdminEndpointResult> SaveAdmissionSchoolReportingScanAsync(
        string sessionToken,
        JsonObject body,
        CancellationToken cancellationToken)
    {
        var target = await ResolveScanTargetAsync(sessionToken, Text(body["code"]), cancellationToken);
        if (target.Error is not null) return target.Error;
        if (Text(body["examId"]) is { Length: > 0 } examId && Clean(examId, 64) != target.Placement!.ExamId)
            return Error(400, "二维码不属于当前考试报到批次");
        var status = Clean(Text(body["status"]), 30);
        if (status is not ("reported" or "not_reported" or "pending"))
            return Error(400, "请选择有效的报到确认状态");
        var now = NowIso();
        var fallback = status switch
        {
            "reported" => "扫描录取通知书二维码确认报到",
            "not_reported" => "扫描录取通知书二维码确认未报到",
            _ => "扫描录取通知书二维码后暂待确认"
        };
        var merged = ReportingRowMap(target.Record!);
        merged[target.Placement!.Id] = new JsonObject
        {
            ["placementId"] = target.Placement.Id,
            ["status"] = status,
            ["note"] = Clean(Text(body["note"]), 300) is { Length: > 0 } note ? note : fallback,
            ["updatedAt"] = now,
            ["source"] = "qr_scan"
        };
        var updated = UpdateReportingRecord(
            target.Record!, "draft", merged.Values, target.User!.DisplayName, now,
            ("savedAt", JsonValue.Create(now)), ("savedBy", JsonValue.Create(target.User.DisplayName)));
        await repository.SaveRecordsAsync(
            [updated],
            Audit(target.User, "扫码确认并暂存考生报到",
                $"{target.School!.Name} · {(Text(target.Placement.Payload["noticeNumber"]) is { Length: > 0 } number ? number : target.Placement.Id)} · {ReportingCode(status)}"),
            cancellationToken);
        var data = ReplaceRecord(target.Data!, updated);
        return Success(new JsonObject
        {
            ["ok"] = true,
            ["row"] = ReportingRows(data, target.Plan!, updated)
                .First(item => Text(item["placementId"]) == target.Placement.Id),
            ["batch"] = ReportingBatch(data, target.Plan!, updated)
        });
    }

    public async Task<AdminEndpointResult> SubmitAdmissionSchoolReportingAsync(
        string sessionToken,
        JsonObject body,
        CancellationToken cancellationToken)
    {
        var resolved = await ResolveReportingAsync(sessionToken, Text(body["examId"]), cancellationToken);
        if (resolved.Error is not null) return resolved.Error;
        var rows = ReportingRows(resolved.Data!, resolved.Plan!, resolved.Record!);
        var pending = rows.Count(item => Text(item["status"]) == "pending");
        if (pending > 0) return Error(409, $"仍有 {pending} 名考生待确认，请全部标记后提交");
        var now = NowIso();
        var payload = resolved.Record!.Payload.DeepClone().AsObject();
        payload["submittedAt"] = now;
        payload["submittedBy"] = resolved.User!.DisplayName;
        var updated = resolved.Record with { Status = "submitted", UpdatedAt = now, Payload = payload };
        await repository.SaveRecordsAsync(
            [updated],
            Audit(resolved.User, "提交考生报到情况", $"{resolved.School!.Name} · {rows.Length} 人"),
            cancellationToken);
        var data = ReplaceRecord(resolved.Data!, updated);
        return Success(new JsonObject
        {
            ["ok"] = true,
            ["batch"] = ReportingBatch(data, resolved.Plan!, updated)
        });
    }

    public async Task<AdminEndpointResult> SaveAdmissionSchoolReportingDecisionAsync(
        string sessionToken,
        JsonObject body,
        CancellationToken cancellationToken)
    {
        var resolved = await ResolveReportingAsync(
            sessionToken, Text(body["examId"]), cancellationToken, requiredStatus: "submitted");
        if (resolved.Error is not null) return resolved.Error;
        var progress = PlanProgress(resolved.Data!, resolved.Plan!);
        var gap = Integer(progress["reportingGap"]);
        var supplement = ExactlyTrue(body["supplement"]) && gap > 0;
        var note = Clean(Text(body["decisionNote"]), 500);
        if (supplement && note.Length < 4)
            return Error(400, "申请补录时请填写至少 4 个字的补录说明");
        var now = NowIso();
        var payload = resolved.Record!.Payload.DeepClone().AsObject();
        payload["supplementDecision"] = supplement ? "supplement" : "no_supplement";
        payload["decisionNote"] = note.Length > 0 ? note :
            gap > 0 ? "经学校研究决定，本轮不进行补录。" : "本校招生计划已完成。";
        payload["decisionSubmittedAt"] = now;
        payload["decisionSubmittedBy"] = resolved.User!.DisplayName;
        payload["statistics"] = progress.DeepClone();
        var updated = resolved.Record with { Status = "pending_approval", UpdatedAt = now, Payload = payload };
        await repository.SaveRecordsAsync(
            [updated],
            Audit(resolved.User, supplement ? "提交补录申请" : "提交不补录决定",
                $"{resolved.School!.Name} · 缺额 {gap} 人"),
            cancellationToken);
        var data = ReplaceRecord(resolved.Data!, updated);
        return Success(new JsonObject
        {
            ["ok"] = true,
            ["batch"] = ReportingBatch(data, resolved.Plan!, updated)
        });
    }

    public async Task<AdminEndpointResult> ReviewAdmissionSchoolPlacementsAsync(
        string sessionToken,
        JsonObject body,
        CancellationToken cancellationToken)
    {
        var context = await ResolveAdmissionSchoolAsync(sessionToken, cancellationToken);
        if (context.Error is not null) return context.Error;
        var user = context.User!;
        var data = await LoadAsync(cancellationToken);
        var school = AdmissionSchool(data, user);
        if (school is null) return Error(403, "招生学校账号未绑定有效学校");
        var ids = (body["ids"] as JsonArray ?? []).Select(Text).Select(item => Clean(item, 64))
            .Where(item => item.Length > 0).Distinct(StringComparer.Ordinal).ToArray();
        var decision = Clean(Text(body["decision"]), 30);
        var note = Clean(Text(body["note"]), 500);
        if (ids.Length == 0) return Error(400, "请至少选择一名待审核考生");
        if (decision is not ("accept" or "withdraw")) return Error(400, "请选择接收或申请退档");
        if (decision == "withdraw" && note.Length < 8)
            return Error(400, "批量申请退档必须填写至少 8 个字的特殊理由");
        var placements = Records(data, "placement").Where(item =>
            ids.Contains(item.Id, StringComparer.Ordinal) && item.SchoolId == school.Id && item.Status == "school_review").ToArray();
        if (placements.Length != ids.Length)
            return Error(409, "所选记录中包含已处理或不属于本校的投档记录，请刷新后重试");
        var now = NowIso();
        var updated = placements.Select(item => ReviewedPlacement(item, decision, note, now)).ToArray();
        await repository.SaveRecordsAsync(
            updated,
            Audit(user, decision == "accept" ? "批量接收投档考生" : "批量申请退档",
                $"{school.Name} · {updated.Length} 人"),
            cancellationToken);
        return Success(new JsonObject { ["ok"] = true, ["count"] = updated.Length, ["decision"] = decision });
    }

    public async Task<AdminEndpointResult> ReviewAdmissionSchoolPlacementAsync(
        string sessionToken,
        string placementId,
        JsonObject body,
        CancellationToken cancellationToken)
    {
        var context = await ResolveAdmissionSchoolAsync(sessionToken, cancellationToken);
        if (context.Error is not null) return context.Error;
        var user = context.User!;
        var data = await LoadAsync(cancellationToken);
        var school = AdmissionSchool(data, user);
        if (school is null) return Error(403, "招生学校账号未绑定有效学校");
        var placement = Records(data, "placement").FirstOrDefault(item =>
            item.Id == placementId && item.SchoolId == school.Id && item.Status == "school_review");
        if (placement is null) return Error(404, "待审核投档记录不存在");
        var decision = Clean(Text(body["decision"]), 30);
        var note = Clean(Text(body["note"]), 500);
        if (decision is not ("accept" or "withdraw")) return Error(400, "请选择接收或申请退档");
        if (decision == "withdraw" && note.Length < 8)
            return Error(400, "申请退档必须填写至少 8 个字的特殊理由");
        var updated = ReviewedPlacement(placement, decision, note, NowIso());
        await repository.SaveRecordsAsync(
            [updated],
            Audit(user, decision == "accept" ? "接收投档考生" : "申请退档", $"{school.Name} · {placement.Id}"),
            cancellationToken);
        return Success(new JsonObject { ["ok"] = true, ["placement"] = RecordJson(updated) });
    }

    private async Task<ResolvedReporting> ResolveReportingAsync(
        string sessionToken,
        string examId,
        CancellationToken cancellationToken,
        string? requiredStatus = null)
    {
        var context = await ResolveAdmissionSchoolAsync(sessionToken, cancellationToken);
        if (context.Error is not null) return ResolvedReporting.Failed(context.Error);
        var data = await LoadAsync(cancellationToken);
        var school = AdmissionSchool(data, context.User!);
        if (school is null) return ResolvedReporting.Failed(Error(403, "招生学校账号未绑定有效学校"));
        var cleanExamId = Clean(examId, 64);
        var plan = Records(data, "plan", cleanExamId)
            .FirstOrDefault(item => item.SchoolId == school.Id && item.Status == "approved");
        var record = plan is null ? null : EditableReportingRecord(data, cleanExamId, school.Id);
        var valid = requiredStatus is null
            ? record?.Status is "draft" or "rejected"
            : record?.Status == requiredStatus;
        if (plan is null || record is null || !valid)
        {
            var message = requiredStatus == "submitted"
                ? "请先提交本轮考生报到情况"
                : "当前报到批次不能修改暂存状态";
            return ResolvedReporting.Failed(Error(409, message));
        }
        return new(context.User, school, data, plan, record, null);
    }

    private async Task<ResolvedScan> ResolveScanTargetAsync(
        string sessionToken,
        string rawCode,
        CancellationToken cancellationToken)
    {
        var context = await ResolveAdmissionSchoolAsync(sessionToken, cancellationToken);
        if (context.Error is not null) return ResolvedScan.Failed(context.Error);
        var data = await LoadAsync(cancellationToken);
        var school = AdmissionSchool(data, context.User!);
        if (school is null) return ResolvedScan.Failed(Error(403, "招生学校账号未绑定有效学校"));
        var match = AdmissionNoticePattern.Match(rawCode.ToUpperInvariant());
        if (!match.Success) return ResolvedScan.Failed(Error(400, "未识别到有效的录取通知书防伪码"));
        var code = match.Value.ToUpperInvariant();
        var placement = Records(data, "placement").FirstOrDefault(item =>
            item.SchoolId == school.Id && item.Status == "final" &&
            DocumentVerificationCodeService.SafeEquals(code, NoticeCode(item)));
        if (placement is null) return ResolvedScan.Failed(Error(404, "该二维码不属于本校有效录取通知书"));
        var plan = Records(data, "plan", placement.ExamId)
            .FirstOrDefault(item => item.SchoolId == school.Id && item.Status == "approved");
        var record = plan is null ? null : EditableReportingRecord(data, placement.ExamId, school.Id);
        if (plan is null || record is null || record.Status is not ("draft" or "rejected") ||
            !(record.Payload["rows"] as JsonArray ?? []).Any(item => Text(item?["placementId"]) == placement.Id))
            return ResolvedScan.Failed(Error(409, "该考生不在当前可维护的报到批次"));
        return new(context.User, school, data, plan, record, placement, code, null);
    }

    private string NoticeCode(CandidateAdmissionRecord placement) =>
        documentCodes.AdmissionNoticeCode(
            placement.Id,
            placement.UserId ?? "",
            placement.SchoolId ?? "",
            placement.ExamId,
            Text(placement.Payload["categoryCode"]),
            Text(placement.Payload["noticeNumber"]),
            placement.UpdatedAt);

    private static Dictionary<string, JsonObject> ReportingRowMap(CandidateAdmissionRecord record) =>
        (record.Payload["rows"] as JsonArray ?? []).OfType<JsonObject>()
        .ToDictionary(item => Text(item["placementId"]), item => item.DeepClone().AsObject(), StringComparer.Ordinal);

    private static CandidateAdmissionRecord UpdateReportingRecord(
        CandidateAdmissionRecord record,
        string status,
        IEnumerable<JsonObject> rows,
        string actor,
        string now,
        params (string Key, JsonNode? Value)[] additions)
    {
        var payload = record.Payload.DeepClone().AsObject();
        payload["rows"] = new JsonArray(rows.Select(item => (JsonNode)item).ToArray());
        foreach (var addition in additions) payload[addition.Key] = addition.Value;
        return record with { Status = status, UpdatedAt = now, Payload = payload };
    }

    private static CandidateAdmissionRecord ReviewedPlacement(
        CandidateAdmissionRecord placement,
        string decision,
        string note,
        string now)
    {
        var payload = placement.Payload.DeepClone().AsObject();
        payload["schoolDecisionNote"] = note;
        if (decision == "withdraw") payload["withdrawalReason"] = note;
        return placement with
        {
            Status = decision == "accept" ? "admitted" : "withdrawal_pending",
            Payload = payload,
            UpdatedAt = now
        };
    }

    private static string ReportingCode(string status) =>
        status switch { "reported" => "Y", "not_reported" => "N", _ => "P" };

    private sealed record ResolvedReporting(
        AuthenticationUser? User,
        AdminSchool? School,
        AdmissionData? Data,
        CandidateAdmissionRecord? Plan,
        CandidateAdmissionRecord? Record,
        AdminEndpointResult? Error)
    {
        public static ResolvedReporting Failed(AdminEndpointResult error) =>
            new(null, null, null, null, null, error);
    }

    private sealed record ResolvedScan(
        AuthenticationUser? User,
        AdminSchool? School,
        AdmissionData? Data,
        CandidateAdmissionRecord? Plan,
        CandidateAdmissionRecord? Record,
        CandidateAdmissionRecord? Placement,
        string Code,
        AdminEndpointResult? Error)
    {
        public static ResolvedScan Failed(AdminEndpointResult error) =>
            new(null, null, null, null, null, null, "", error);
    }
}
