using System.Data.Common;
using System.Text.Json.Nodes;

namespace Eis.Infrastructure.Public;

public sealed partial class PublicQueryService
{
    private static readonly IReadOnlyDictionary<string, string> SpecialtyCategories =
        new Dictionary<string, string>(StringComparer.Ordinal)
        {
            ["sports"] = "体育",
            ["arts"] = "艺术"
        };

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

    public Task<JsonObject> GetAnnouncementsAsync(CancellationToken cancellationToken) =>
        cache.RememberJsonAsync(
            "public",
            "admission-announcements",
            () => LoadAnnouncementsAsync(cancellationToken));

    private async Task<JsonObject> LoadAnnouncementsAsync(CancellationToken cancellationToken)
    {
        await using var connection = await connectionFactory.OpenAsync(cancellationToken);
        var exams = await QueryAsync(connection,
            "SELECT id, name FROM exams ORDER BY created_at, id",
            reader => new NamedRow(ReadString(reader, "id"), ReadString(reader, "name")), cancellationToken);
        var schools = await QueryAsync(connection,
            "SELECT id, name FROM schools ORDER BY name, id",
            reader => new NamedRow(ReadString(reader, "id"), ReadString(reader, "name")), cancellationToken);
        var users = await QueryAsync(connection,
            "SELECT id, role, active, candidate_number, display_name FROM users ORDER BY created_at, id",
            ReadAnnouncementUser, cancellationToken);
        var profiles = await QueryAsync(connection,
            "SELECT user_id, name, school_id, profile_completed, id_number, phone FROM candidate_profiles ORDER BY updated_at, id",
            ReadAnnouncementProfile, cancellationToken);
        var records = await QueryAsync(connection,
            "SELECT id, kind, exam_id, user_id, school_id, status, payload_json, created_at, updated_at FROM admission_records ORDER BY created_at, id",
            ReadAdmissionRecord, cancellationToken);

        var examNames = exams.ToDictionary(item => item.Id, item => item.Name, StringComparer.Ordinal);
        var schoolNames = schools.ToDictionary(item => item.Id, item => item.Name, StringComparer.Ordinal);
        var usersById = users.ToDictionary(item => item.Id, StringComparer.Ordinal);
        var profilesByUser = profiles.ToDictionary(item => item.UserId, StringComparer.Ordinal);
        var settings = records.Where(item => item.Kind == "setting")
            .GroupBy(item => item.ExamId)
            .ToDictionary(group => group.Key, group => group.First(), StringComparer.Ordinal);

        var plans = records
            .Where(item => item.Kind == "plan" && item.Status == "approved" && GetBoolean(item.Payload, "publicVisible", true))
            .Select(item => BuildPlanAnnouncement(item, examNames, schoolNames))
            .OrderByDescending(item => ParseDate(item["publishedAt"]?.GetValue<string>()))
            .ToArray();

        var qualifications = records
            .Where(item => item.Kind == "qualification_publication" && item.Status == "published" &&
                           GetBoolean(item.Payload, "publicVisible", true) &&
                           QualificationComplete(item, users, profiles, records))
            .Select(item => BuildQualificationAnnouncement(item, examNames, schoolNames))
            .OrderByDescending(item => ParseDate(item["publishedAt"]?.GetValue<string>()))
            .ToArray();

        var roundPublications = BuildRoundPublications(records, settings, examNames, schoolNames, usersById, profilesByUser);
        var finalPublications = records
            .Where(item => item.Kind == "setting" && item.Status == "completed" &&
                           GetBoolean(item.Payload, "autoPublish", true) && GetBoolean(item.Payload, "publicVisible", true))
            .Select(item => new JsonObject
            {
                ["id"] = item.Id,
                ["examId"] = item.ExamId,
                ["examName"] = examNames.GetValueOrDefault(item.ExamId) ?? string.Empty,
                ["title"] = $"{examNames.GetValueOrDefault(item.ExamId) ?? string.Empty}最终录取名单",
                ["publishedAt"] = GetString(item.Payload, "completedAt") ?? item.UpdatedAt,
                ["rows"] = BuildPublicAdmissionRows(records, item.ExamId, 0, schoolNames, usersById, profilesByUser)
            });
        var admissions = roundPublications.Concat(finalPublications)
            .OrderByDescending(item => ParseDate(item["publishedAt"]?.GetValue<string>()))
            .ToArray();

        var cutoffs = records
            .Where(item => item.Kind == "cutoff_publication" && item.Status == "published" &&
                           GetBoolean(item.Payload, "publicVisible", true) &&
                           (!settings.TryGetValue(item.ExamId, out var setting) || GetBoolean(setting.Payload, "autoPublish", true)))
            .Select(item => new JsonObject
            {
                ["id"] = item.Id,
                ["examId"] = item.ExamId,
                ["examName"] = examNames.GetValueOrDefault(item.ExamId) ?? string.Empty,
                ["publishedAt"] = GetString(item.Payload, "publishedAt") ?? item.UpdatedAt,
                ["rows"] = GetArray(item.Payload, "rows").DeepClone()
            })
            .OrderByDescending(item => ParseDate(item["publishedAt"]?.GetValue<string>()))
            .ToArray();

        var reports = records
            .Where(item => item.Kind == "notification" && item.UserId is null && item.Status == "approved" &&
                           GetString(item.Payload, "type") == "admission_reporting" &&
                           GetBoolean(item.Payload, "publicVisible", true))
            .Select(item => BuildReportingAnnouncement(item, examNames, schoolNames))
            .OrderByDescending(item => ParseDate(item["publishedAt"]?.GetValue<string>()))
            .ToArray();

        return new JsonObject
        {
            ["ok"] = true,
            ["plans"] = new JsonArray(plans),
            ["qualifications"] = new JsonArray(qualifications),
            ["admissions"] = new JsonArray(admissions),
            ["cutoffs"] = new JsonArray(cutoffs),
            ["reports"] = new JsonArray(reports)
        };
    }

    private static JsonObject BuildPlanAnnouncement(
        AdmissionRecordRow item,
        IReadOnlyDictionary<string, string> examNames,
        IReadOnlyDictionary<string, string> schoolNames)
    {
        var rows = GetArray(item.Payload, "categories").OfType<JsonObject>().Select(category =>
        {
            var allocations = GetArray(category, "indicatorAllocations").OfType<JsonObject>().ToArray();
            var categoryCode = GetString(category, "specialtyCategory") ?? string.Empty;
            var typeCode = GetString(category, "specialtyType") ?? string.Empty;
            return new JsonObject
            {
                ["code"] = GetString(category, "code") ?? string.Empty,
                ["name"] = GetString(category, "name") ?? string.Empty,
                ["quota"] = GetNumber(category, "quota"),
                ["specialtyCategory"] = categoryCode,
                ["specialtyType"] = typeCode,
                ["specialtyLabel"] = SpecialtyLabel(categoryCode, typeCode) ?? "普通 / 政策类",
                ["indicatorQuota"] = allocations.Sum(allocation => GetNumber(allocation, "quota")),
                ["indicatorAllocations"] = new JsonArray(allocations.Select(allocation => new JsonObject
                {
                    ["sourceSchoolName"] = schoolNames.GetValueOrDefault(GetString(allocation, "sourceSchoolId") ?? string.Empty)
                                           ?? GetString(allocation, "sourceSchoolId") ?? string.Empty,
                    ["quota"] = GetNumber(allocation, "quota")
                }).ToArray())
            };
        }).ToArray();

        return new JsonObject
        {
            ["id"] = item.Id,
            ["examId"] = item.ExamId,
            ["examName"] = examNames.GetValueOrDefault(item.ExamId) ?? string.Empty,
            ["schoolName"] = item.SchoolId is null ? string.Empty : schoolNames.GetValueOrDefault(item.SchoolId) ?? string.Empty,
            ["publishedAt"] = GetString(item.Payload, "reviewedAt") ?? item.UpdatedAt,
            ["note"] = GetString(item.Payload, "note") ?? string.Empty,
            ["rows"] = new JsonArray(rows)
        };
    }

    private static JsonObject BuildQualificationAnnouncement(
        AdmissionRecordRow item,
        IReadOnlyDictionary<string, string> examNames,
        IReadOnlyDictionary<string, string> schoolNames) => new()
        {
            ["id"] = item.Id,
            ["examId"] = item.ExamId,
            ["examName"] = examNames.GetValueOrDefault(item.ExamId) ?? string.Empty,
            ["schoolName"] = item.SchoolId is null ? string.Empty : schoolNames.GetValueOrDefault(item.SchoolId) ?? string.Empty,
            ["publishedAt"] = GetString(item.Payload, "publishedAt") ?? item.UpdatedAt,
            ["rows"] = new JsonArray(GetArray(item.Payload, "rows").OfType<JsonObject>().Select(row => new JsonObject
            {
                ["registrationNumber"] = GetString(row, "registrationNumber") ?? string.Empty,
                ["name"] = GetString(row, "name") ?? string.Empty,
                ["eligible"] = GetBoolean(row, "eligible", false),
                ["specialtyLabel"] = GetString(row, "specialtyLabel") ?? "普通生"
            }).ToArray())
        };

    private static bool QualificationComplete(
        AdmissionRecordRow publication,
        IReadOnlyCollection<AnnouncementUserRow> users,
        IReadOnlyCollection<AnnouncementProfileRow> profiles,
        IReadOnlyCollection<AdmissionRecordRow> records)
    {
        if (publication.SchoolId is null)
        {
            return false;
        }
        var activeCandidates = users.Where(user => user.Role == "candidate" && user.Active)
            .Select(user => user.Id).ToHashSet(StringComparer.Ordinal);
        var expectedUsers = profiles
            .Where(profile => profile.SchoolId == publication.SchoolId && profile.ProfileCompleted && activeCandidates.Contains(profile.UserId))
            .Select(profile => profile.UserId).ToArray();
        if (expectedUsers.Length == 0)
        {
            return false;
        }
        var confirmed = records
            .Where(item => item.Kind == "indicator_qualification" && item.ExamId == publication.ExamId &&
                           item.SchoolId == publication.SchoolId && item.Status == "confirmed" && item.UserId is not null)
            .Select(item => item.UserId!).ToHashSet(StringComparer.Ordinal);
        return expectedUsers.All(confirmed.Contains);
    }

    private static IEnumerable<JsonObject> BuildRoundPublications(
        IReadOnlyCollection<AdmissionRecordRow> records,
        IReadOnlyDictionary<string, AdmissionRecordRow> settings,
        IReadOnlyDictionary<string, string> examNames,
        IReadOnlyDictionary<string, string> schoolNames,
        IReadOnlyDictionary<string, AnnouncementUserRow> users,
        IReadOnlyDictionary<string, AnnouncementProfileRow> profiles)
    {
        var output = new List<RoundPublication>();
        foreach (var item in records.Where(item => item.Kind == "notification" && item.UserId is null &&
                                                   item.Status == "published" && GetString(item.Payload, "type") == "admission_round_publication"))
        {
            output.Add(new RoundPublication(
                item.Id, item.ExamId, Math.Max(1, (int)GetNumber(item.Payload, "round", 1)),
                GetString(item.Payload, "publishedAt") ?? item.UpdatedAt,
                GetArray(item.Payload, "rows").DeepClone().AsArray(), GetBoolean(item.Payload, "publicVisible", true)));
        }
        var keys = output.Select(item => $"{item.ExamId}:{item.Round}").ToHashSet(StringComparer.Ordinal);
        foreach (var setting in records.Where(item => item.Kind == "setting" && item.Status is "reporting" or "supplementary" or "completed"))
        {
            var rounds = records.Where(item => item.Kind == "placement" && item.ExamId == setting.ExamId && item.Status is "final" or "forfeited")
                .Select(item => Math.Max(1, (int)GetNumber(item.Payload, "finalizedRound", 1))).ToArray();
            var round = rounds.Length == 0 ? 0 : rounds.Max();
            if (round == 0 || keys.Contains($"{setting.ExamId}:{round}"))
            {
                continue;
            }
            output.Add(new RoundPublication(
                $"{setting.Id}-round-{round}", setting.ExamId, round,
                GetString(setting.Payload, "roundPublishedAt") ?? setting.UpdatedAt,
                BuildPublicAdmissionRows(records, setting.ExamId, round, schoolNames, users, profiles),
                GetBoolean(setting.Payload, "publicVisible", true)));
        }

        return output
            .Where(item => item.Visible && (!settings.TryGetValue(item.ExamId, out var setting) || GetBoolean(setting.Payload, "autoPublish", true)))
            .Select(item => new JsonObject
            {
                ["id"] = item.Id,
                ["examId"] = item.ExamId,
                ["examName"] = examNames.GetValueOrDefault(item.ExamId) ?? string.Empty,
                ["round"] = item.Round,
                ["title"] = $"{examNames.GetValueOrDefault(item.ExamId) ?? string.Empty}第 {item.Round} 轮录取名单公示",
                ["publishedAt"] = item.PublishedAt,
                ["rows"] = item.Rows.DeepClone()
            });
    }

    private static JsonArray BuildPublicAdmissionRows(
        IReadOnlyCollection<AdmissionRecordRow> records,
        string examId,
        int round,
        IReadOnlyDictionary<string, string> schoolNames,
        IReadOnlyDictionary<string, AnnouncementUserRow> users,
        IReadOnlyDictionary<string, AnnouncementProfileRow> profiles)
    {
        var rows = records.Where(item => item.Kind == "placement" && item.ExamId == examId &&
                                         (round > 0
                                             ? (item.Status is "final" or "forfeited") && (int)GetNumber(item.Payload, "finalizedRound", 1) == round
                                             : item.Status == "final"))
            .Select(item =>
            {
                users.TryGetValue(item.UserId ?? string.Empty, out var user);
                profiles.TryGetValue(item.UserId ?? string.Empty, out var profile);
                return new JsonObject
                {
                    ["registrationNumber"] = user?.CandidateNumber ?? string.Empty,
                    ["name"] = profile?.Name ?? user?.DisplayName ?? string.Empty,
                    ["totalScore"] = GetNumber(item.Payload, "totalScore"),
                    ["admittedSchool"] = item.SchoolId is null ? string.Empty : schoolNames.GetValueOrDefault(item.SchoolId) ?? string.Empty,
                    ["categoryName"] = GetString(item.Payload, "categoryName") ?? string.Empty,
                    ["idNumberMasked"] = MaskIdNumber(profile?.IdNumber),
                    ["phoneMasked"] = MaskPhone(profile?.Phone)
                };
            })
            .OrderByDescending(item => item["totalScore"]?.GetValue<double>() ?? 0)
            .ThenBy(item => item["registrationNumber"]?.GetValue<string>(), StringComparer.Ordinal)
            .ToArray();
        return new JsonArray(rows);
    }

    private static JsonObject BuildReportingAnnouncement(
        AdmissionRecordRow item,
        IReadOnlyDictionary<string, string> examNames,
        IReadOnlyDictionary<string, string> schoolNames)
    {
        var statistics = GetObject(item.Payload, "statistics");
        var supplement = GetString(item.Payload, "supplementDecision") == "supplement";
        var examName = examNames.GetValueOrDefault(item.ExamId) ?? string.Empty;
        var schoolName = item.SchoolId is null ? string.Empty : schoolNames.GetValueOrDefault(item.SchoolId) ?? string.Empty;
        var summary = $"计划 {GetNumber(statistics, "totalQuota"):0.##} 人，已报到 {GetNumber(statistics, "reportedCount"):0.##} 人，完成率 {GetNumber(statistics, "reportingRate"):0.##}%。";
        return new JsonObject
        {
            ["id"] = item.Id,
            ["examId"] = item.ExamId,
            ["schoolId"] = JsonValue.Create(item.SchoolId),
            ["examName"] = examName,
            ["schoolName"] = schoolName,
            ["title"] = supplement ? $"{examName} · {schoolName}考生报到情况及补录说明" : $"{examName} · {schoolName}考生报到情况公示",
            ["summary"] = summary,
            ["publishedAt"] = GetString(item.Payload, "approvedAt") ?? item.UpdatedAt,
            ["statistics"] = statistics.DeepClone(),
            ["supplementDecision"] = GetString(item.Payload, "supplementDecision") ?? string.Empty,
            ["decisionNote"] = GetString(item.Payload, "decisionNote") ?? string.Empty
        };
    }

    private static string? SpecialtyLabel(string categoryCode, string typeCode)
    {
        if (!SpecialtyCategories.TryGetValue(categoryCode, out var categoryName))
        {
            return null;
        }
        return SpecialtyTypes.TryGetValue(typeCode, out var type) && type.Category == categoryCode
            ? $"{categoryName}·{type.Name}"
            : categoryName;
    }

    private static string MaskIdNumber(string? value) => string.IsNullOrEmpty(value)
        ? string.Empty
        : $"{value[..Math.Min(3, value.Length)]}***********{value[Math.Max(0, value.Length - 2)..]}";

    private static string MaskPhone(string? value) => string.IsNullOrEmpty(value)
        ? string.Empty
        : $"{value[..Math.Min(3, value.Length)]}****{value[Math.Max(0, value.Length - 4)..]}";

    private static AnnouncementUserRow ReadAnnouncementUser(DbDataReader reader) => new(
        ReadString(reader, "id"), ReadString(reader, "role"), ReadBoolean(reader, "active"),
        ReadNullableString(reader, "candidate_number") ?? string.Empty, ReadString(reader, "display_name"));

    private static AnnouncementProfileRow ReadAnnouncementProfile(DbDataReader reader) => new(
        ReadString(reader, "user_id"), ReadString(reader, "name"), ReadNullableString(reader, "school_id"),
        ReadBoolean(reader, "profile_completed"), ReadString(reader, "id_number"), ReadString(reader, "phone"));

    private sealed record NamedRow(string Id, string Name);
    private sealed record AnnouncementUserRow(string Id, string Role, bool Active, string CandidateNumber, string DisplayName);
    private sealed record AnnouncementProfileRow(string UserId, string Name, string? SchoolId, bool ProfileCompleted, string IdNumber, string Phone);
    private sealed record RoundPublication(string Id, string ExamId, int Round, string PublishedAt, JsonArray Rows, bool Visible);
}
