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

namespace Eis.Infrastructure.Administration;

internal sealed class AdminExamManagementService(
    IAuthenticationStateStore authenticationState,
    AuthenticationRepository authenticationRepository,
    AdminOperationalSnapshotLoader operationalSnapshotLoader,
    AdminAccountBatchSnapshotLoader workflowSnapshotLoader,
    AdminWorkflowResultSnapshotLoader resultSnapshotLoader,
    AdminExamRepository repository) : IAdminExamManagementService
{
    private static readonly HashSet<string> PassPolicies = ["fixed_score", "rank_percent", "subject_scores", "none"];
    private static readonly HashSet<string> SubjectPassRules = ["fixed_score", "rank_percent", "none"];

    public async Task<AdminEndpointResult> CreateAsync(
        string sessionToken,
        JsonObject body,
        CancellationToken cancellationToken)
    {
        var context = await ResolveSuperAsync(sessionToken, cancellationToken);
        if (context.Error is not null) return context.Error;
        var user = context.User!;
        var name = Clean(Text(body["name"]), 100);
        if (name.Length == 0 || !JsBoolean(body["registrationStart"]) || !JsBoolean(body["registrationEnd"]) ||
            !JsBoolean(body["examStart"]) || !JsBoolean(body["examEnd"]))
            return Error(400, "请完整填写考试名称和关键日期");
        var examStart = Text(body["examStart"]);
        var subjects = NormalizeSubjects(body["subjects"], examStart);
        var requestedPolicy = Text(body["passPolicy"]);
        if (requestedPolicy == "score_ratio") requestedPolicy = "rank_percent";
        var passPolicy = PassPolicies.Contains(requestedPolicy) ? requestedPolicy : "rank_percent";
        var passValue = passPolicy is "subject_scores" or "none" ? 0 : NumberOr(body["passValue"], 60);
        var scoringError = ValidateScoring(subjects, passPolicy, passValue);
        if (scoringError.Length > 0) return Error(400, scoringError);

        var operational = await operationalSnapshotLoader.LoadAsync(cancellationToken);
        var code = Clean(Text(body["code"]), 30);
        if (code.Length == 0) code = $"EX-{DateTime.Now.Year}-{operational.Exams.Count + 1:00}";
        var exam = new ManagedExam(
            Uid("exam"),
            code,
            name,
            Clean(Text(body["description"]), 500),
            Text(body["registrationStart"]),
            Text(body["registrationEnd"]),
            examStart,
            Text(body["examEnd"]),
            JsBoolean(body["admitDownloadStart"]) ? Text(body["admitDownloadStart"]) : Text(body["registrationEnd"]),
            JsBoolean(body["admitDownloadEnd"]) ? Text(body["admitDownloadEnd"]) : examStart,
            Clean(Text(body["location"]), 100),
            passPolicy,
            passValue,
            Text(body["status"]) == "published" ? "published" : "draft",
            null,
            null,
            NowIso(),
            subjects);
        await repository.CreateAsync(
            exam,
            Audit(user, "创建考试", $"{exam.Name} · {subjects.Count} 个科目"),
            cancellationToken);
        var responseExam = ExamJson(exam);
        responseExam.Remove("archivedAt");
        responseExam.Remove("archivedBy");
        return Result(201, new JsonObject { ["ok"] = true, ["exam"] = responseExam });
    }

    public async Task<AdminEndpointResult> UpdateAsync(
        string sessionToken,
        string examId,
        JsonObject body,
        CancellationToken cancellationToken)
    {
        var context = await ResolveSuperAsync(sessionToken, cancellationToken);
        if (context.Error is not null) return context.Error;
        var user = context.User!;
        var operational = await operationalSnapshotLoader.LoadAsync(cancellationToken);
        var source = operational.Exams.FirstOrDefault(item => item.Id == examId);
        if (source is null) return Error(404, "考试不存在");
        var exam = FromOperational(source);
        if (exam.ArchivedAt is not null) return Error(409, "该考试已归档，所有配置和成绩均已锁定");

        string[] detailFields =
        [
            "code", "name", "description", "location", "registrationStart", "registrationEnd",
            "examStart", "examEnd", "admitDownloadStart", "admitDownloadEnd"
        ];
        var editingDetails = detailFields.Any(body.ContainsKey) ||
                             body.ContainsKey("subjects") || body.ContainsKey("passPolicy") || body.ContainsKey("passValue");
        if (editingDetails && exam.Status != "draft") return Error(409, "请先将考试撤回为草稿后再编辑");
        var requestedStatus = Text(body["status"]);
        if (requestedStatus is "draft" or "published" or "closed") exam = exam with { Status = requestedStatus };
        foreach (var field in detailFields.Where(body.ContainsKey))
        {
            var value = Clean(Text(body[field]), field == "description" ? 500 : 100);
            exam = field switch
            {
                "code" => exam with { Code = value },
                "name" => exam with { Name = value },
                "description" => exam with { Description = value },
                "location" => exam with { Location = value },
                "registrationStart" => exam with { RegistrationStart = value },
                "registrationEnd" => exam with { RegistrationEnd = value },
                "examStart" => exam with { ExamStart = value },
                "examEnd" => exam with { ExamEnd = value },
                "admitDownloadStart" => exam with { AdmitDownloadStart = value },
                "admitDownloadEnd" => exam with { AdmitDownloadEnd = value },
                _ => exam
            };
        }
        if (body.ContainsKey("passPolicy"))
        {
            var policy = Text(body["passPolicy"]);
            if (policy == "score_ratio") policy = "rank_percent";
            if (PassPolicies.Contains(policy)) exam = exam with { PassPolicy = policy };
        }
        if (body.ContainsKey("passValue"))
            exam = exam with { PassValue = body["passValue"] is null ? 0 : Number(body["passValue"]) };
        var replaceSubjects = false;
        if (body.ContainsKey("subjects"))
        {
            if (operational.Registrations.Any(item => item.ExamId == exam.Id))
                return Error(409, "已有报名记录，不能修改考试科目");
            exam = exam with { Subjects = NormalizeSubjects(body["subjects"], exam.ExamStart) };
            replaceSubjects = true;
        }
        var scoringError = ValidateScoring(exam.Subjects, exam.PassPolicy, exam.PassValue);
        if (scoringError.Length > 0) return Error(400, scoringError);
        if (exam.Name.Length == 0 || exam.RegistrationStart.Length == 0 || exam.RegistrationEnd.Length == 0 ||
            exam.ExamStart.Length == 0 || exam.ExamEnd.Length == 0)
            return Error(400, "请完整填写考试名称和关键日期");
        if (exam.Status == "published" && exam.Subjects.Count == 0) return Error(400, "请先配置考试科目再发布");

        await repository.UpdateAsync(
            exam,
            replaceSubjects,
            Audit(user, "更新考试", $"{exam.Name} · 状态 {exam.Status}"),
            cancellationToken);
        return Success(new JsonObject { ["ok"] = true, ["exam"] = ExamJson(exam) });
    }

    public async Task<AdminEndpointResult> ArchiveAsync(
        string sessionToken,
        string examId,
        CancellationToken cancellationToken)
    {
        var context = await ResolveSuperAsync(sessionToken, cancellationToken);
        if (context.Error is not null) return context.Error;
        var user = context.User!;
        var operational = await operationalSnapshotLoader.LoadAsync(cancellationToken);
        var source = operational.Exams.FirstOrDefault(item => item.Id == examId);
        if (source is null) return Error(404, "考试不存在");
        var exam = FromOperational(source);
        if (exam.ArchivedAt is not null) return Error(409, "该考试已经归档，归档操作不可撤销");
        var resultIds = (await resultSnapshotLoader.LoadAsync(cancellationToken))
            .Where(result => operational.Registrations.FirstOrDefault(item => item.Id == result.RegistrationId)?.ExamId == exam.Id)
            .Select(result => result.Id)
            .ToHashSet(StringComparer.Ordinal);
        var workflow = await workflowSnapshotLoader.LoadAsync(cancellationToken);
        var pendingAppeals = workflow.Instances.Count(item =>
            item.BusinessType == "score_appeal" && item.Status == "pending" && resultIds.Contains(item.BusinessId));
        if (pendingAppeals > 0) return Error(409, $"本场还有 {pendingAppeals} 项成绩复议待处理，请先办结后再归档");
        var archived = exam with { ArchivedAt = NowIso(), ArchivedBy = user.Id, Status = "closed" };
        await repository.ArchiveAsync(
            archived,
            Audit(user, "归档考试并永久锁定成绩", $"{archived.Name} · {archived.Code}"),
            cancellationToken);
        var output = ExamJson(archived);
        output["totalScore"] = archived.Subjects.Sum(item => item.FullScore);
        output["registrationState"] = "archived";
        return Success(new JsonObject
        {
            ["ok"] = true,
            ["exam"] = output,
            ["message"] = "考试已归档，全部成绩已永久锁定"
        });
    }

    private static IReadOnlyList<ManagedSubject> NormalizeSubjects(JsonNode? input, string examStart)
    {
        IEnumerable<JsonNode?> source = input switch
        {
            JsonArray array => array,
            _ => Text(input).Split([',', '，'], StringSplitOptions.None).Select(value => (JsonNode?)JsonValue.Create(value))
        };
        var output = new List<ManagedSubject>();
        var index = 0;
        foreach (var item in source)
        {
            var structured = item is JsonObject;
            var objectItem = item as JsonObject;
            var name = Clean(Text(structured ? objectItem!["name"] : item), 50);
            if (name.Length == 0) continue;
            var fullScore = structured
                ? objectItem!.ContainsKey("fullScore")
                    ? objectItem["fullScore"] is null ? 0 : Number(objectItem["fullScore"])
                    : double.NaN
                : 150;
            var requestedRule = Text(objectItem?["passRule"]);
            if (requestedRule == "score_ratio") requestedRule = "rank_percent";
            var passRule = SubjectPassRules.Contains(requestedRule) ? requestedRule : "fixed_score";
            var rawPassValue = objectItem?["passValue"] ?? objectItem?["passScore"];
            var passValue = passRule == "none" ? 0 : rawPassValue is null ? fullScore * 0.6 : Number(rawPassValue);
            double? passScore = passRule == "fixed_score"
                ? Math.Round(passValue, 2, MidpointRounding.AwayFromZero)
                : null;
            output.Add(new ManagedSubject(
                Uid("sub"),
                name,
                Clean(Text(objectItem?["date"]), 10) is { Length: > 0 } date ? date : examStart[..Math.Min(10, examStart.Length)],
                Clean(Text(objectItem?["start"]), 5) is { Length: > 0 } start ? start : "09:00",
                Clean(Text(objectItem?["end"]), 5) is { Length: > 0 } end ? end : "11:00",
                objectItem?["fee"] is null ? 0 : Number(objectItem["fee"]),
                fullScore,
                passRule,
                passValue,
                passScore,
                ++index));
        }
        return output;
    }

    private static string ValidateScoring(IReadOnlyList<ManagedSubject> subjects, string passPolicy, double passValue)
    {
        if (subjects.Count == 0) return "请至少添加一个考试科目";
        if (subjects.Any(item => !double.IsFinite(item.FullScore) || item.FullScore <= 0 || item.FullScore > 1000))
            return "科目满分必须大于 0 且不超过 1000";
        if (subjects.Any(item => !SubjectPassRules.Contains(item.PassRule))) return "请选择有效的单科合格线计算方式";
        if (subjects.Any(item => item.PassRule == "fixed_score" && (!double.IsFinite(item.PassValue) || item.PassValue < 0 || item.PassValue > item.FullScore)))
            return "固定单科合格分必须在 0 与该科满分之间";
        if (subjects.Any(item => item.PassRule == "rank_percent" && (!double.IsFinite(item.PassValue) || item.PassValue <= 0 || item.PassValue > 100)))
            return "单科排名比例必须大于 0 且不超过 100%";
        if (subjects.Any(item => !double.IsFinite(item.Fee) || item.Fee < 0 || item.Fee > 100000))
            return "科目费用必须在有效范围内";
        if (!PassPolicies.Contains(passPolicy)) return "请选择有效的合格线策略";
        var totalScore = subjects.Sum(item => item.FullScore);
        if (passPolicy == "fixed_score" && (!double.IsFinite(passValue) || passValue < 0 || passValue > totalScore))
            return $"固定合格线必须在 0 与总分 {Format(totalScore)} 之间";
        if (passPolicy == "rank_percent" && (!double.IsFinite(passValue) || passValue <= 0 || passValue > 100))
            return "排名比例必须大于 0 且不超过 100";
        return "";
    }

    private static ManagedExam FromOperational(OperationalExam source)
    {
        var data = source.Data;
        return new ManagedExam(
            source.Id,
            Text(data["code"]),
            Text(data["name"]),
            Text(data["description"]),
            Text(data["registrationStart"]),
            Text(data["registrationEnd"]),
            Text(data["examStart"]),
            Text(data["examEnd"]),
            Text(data["admitDownloadStart"]),
            Text(data["admitDownloadEnd"]),
            Text(data["location"]),
            Text(data["passPolicy"]),
            Number(data["passValue"]),
            Text(data["status"]),
            OptionalText(data["archivedAt"]),
            OptionalText(data["archivedBy"]),
            Text(data["createdAt"]),
            source.Subjects.Select((item, index) => new ManagedSubject(
                Text(item["id"]), Text(item["name"]), Text(item["date"]), Text(item["start"]), Text(item["end"]),
                Number(item["fee"]), Number(item["fullScore"]), Text(item["passRule"]), Number(item["passValue"]),
                OptionalNumber(item["passScore"]), (int)NumberOr(item["order"], index + 1))).ToArray());
    }

    private static JsonObject ExamJson(ManagedExam item) => new()
    {
        ["id"] = item.Id,
        ["code"] = item.Code,
        ["name"] = item.Name,
        ["description"] = item.Description,
        ["registrationStart"] = item.RegistrationStart,
        ["registrationEnd"] = item.RegistrationEnd,
        ["examStart"] = item.ExamStart,
        ["examEnd"] = item.ExamEnd,
        ["admitDownloadStart"] = item.AdmitDownloadStart,
        ["admitDownloadEnd"] = item.AdmitDownloadEnd,
        ["location"] = item.Location,
        ["passPolicy"] = item.PassPolicy,
        ["passValue"] = item.PassValue,
        ["status"] = item.Status,
        ["archivedAt"] = JsonValue.Create(item.ArchivedAt),
        ["archivedBy"] = JsonValue.Create(item.ArchivedBy),
        ["subjects"] = new JsonArray(item.Subjects.Select(SubjectJson).ToArray()),
        ["createdAt"] = item.CreatedAt
    };

    private static JsonObject SubjectJson(ManagedSubject item) => new()
    {
        ["id"] = item.Id,
        ["name"] = item.Name,
        ["date"] = item.Date,
        ["start"] = item.Start,
        ["end"] = item.End,
        ["fee"] = item.Fee,
        ["fullScore"] = item.FullScore,
        ["passRule"] = item.PassRule,
        ["passValue"] = item.PassValue,
        ["passScore"] = JsonValue.Create(item.PassScore),
        ["order"] = item.Order
    };

    private async Task<ResolvedAdmin> ResolveSuperAsync(string token, 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, "当前账号无权执行此操作"));
        return (user.AdminLevel ?? "super") == "super"
            ? new(user, null)
            : ResolvedAdmin.Failed(Error(403, "当前管理员层级无权执行此操作"));
    }

    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 textValue && textValue.TryGetValue<string>(out var text)) return text.Length > 0; if (node is JsonValue numberValue && numberValue.TryGetValue<double>(out var number)) return number != 0 && !double.IsNaN(number); return true; }
    private static string Text(JsonNode? node) => node is JsonValue value && value.TryGetValue<string>(out var text) ? text : node?.ToString() ?? "";
    private static string? OptionalText(JsonNode? node) => node is JsonValue value && value.TryGetValue<string>(out var text) ? text : null;
    private static double Number(JsonNode? node) => double.TryParse(Text(node), NumberStyles.Float, CultureInfo.InvariantCulture, out var value) ? value : double.NaN;
    private static double NumberOr(JsonNode? node, double fallback) => node is null ? fallback : Number(node);
    private static double? OptionalNumber(JsonNode? node) => node is null ? null : Number(node);
    private static string Clean(string value, int maximum) { var cleaned = value.Trim(); return cleaned[..Math.Min(cleaned.Length, maximum)]; }
    private static string Format(double value) => value.ToString("0.##", CultureInfo.InvariantCulture);
    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) => Result(200, body);
    private static AdminEndpointResult Result(int status, JsonObject body) => new(status, body);
    private static AdminEndpointResult Error(int status, string message) => Result(status, new JsonObject { ["ok"] = false, ["message"] = message });
    private sealed record ResolvedAdmin(AuthenticationUser? User, AdminEndpointResult? Error) { public static ResolvedAdmin Failed(AdminEndpointResult error) => new(null, error); }
}
