using System.Globalization;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;

namespace Eis.Infrastructure.Administration;

internal static partial class AdminArrangementPlanner
{
    private static readonly IReadOnlyDictionary<string, string> MixingScopeNames =
        new Dictionary<string, string>(StringComparer.Ordinal)
        {
            ["class"] = "班内混编",
            ["school"] = "校内混编",
            ["district"] = "县区内混编",
            ["city"] = "市内混编",
            ["province"] = "省内混编"
        };

    public static ArrangementBuildResult Build(
        AdminOperationalSnapshot operational,
        AdminCenterSnapshot centers,
        AdminArrangementSnapshot arrangements,
        AdminAccountBatchSnapshot directory,
        string examId,
        string requestedScope,
        string numberRuleId,
        string requestedSeed,
        string generatedAt)
    {
        var exam = operational.Exams.FirstOrDefault(item => item.Id == examId)
                   ?? throw Error("考试不存在", 404);
        var mixingScope = requestedScope.Length == 0 ? "school" : requestedScope;
        if (!MixingScopeNames.ContainsKey(mixingScope)) throw Error("请选择有效的混编范围", 400);
        var rule = arrangements.Rules.FirstOrDefault(item => item.Id == numberRuleId && item.Active)
                   ?? throw Error("请选择有效的准考证号规则", 400);
        var seedSource = requestedSeed.Length > 0 ? requestedSeed : First(Text(exam.Data, "code"), exam.Id);
        var seed = seedSource[..Math.Min(seedSource.Length, 80)];
        var warnings = new List<string>();
        void Warn(string message)
        {
            if (!warnings.Contains(message, StringComparer.Ordinal)) warnings.Add(message);
        }

        var subjectOrder = exam.Subjects
            .Select((subject, index) => new { Id = Text(subject, "id"), Index = index })
            .ToDictionary(item => item.Id, item => item.Index, StringComparer.Ordinal);
        var approvedRegistrations = operational.Registrations
            .Where(item => item.ExamId == exam.Id && item.Status == "approved")
            .ToArray();
        if (approvedRegistrations.Length == 0)
            throw Error("该考试没有已审核通过的报名，无法编排");

        var candidates = approvedRegistrations.Select(registration =>
        {
            var profile = operational.Profiles.FirstOrDefault(item => item.UserId == registration.UserId)
                          ?? throw Error($"报名 {registration.Id} 缺少考生档案");
            var boundary = ScopeValue(profile, mixingScope);
            if (boundary.Length == 0)
            {
                var scopeLabel = MixingScopeNames[mixingScope].Replace("内混编", "", StringComparison.Ordinal);
                throw Error($"{Text(profile.Data, "name")} 缺少{scopeLabel}信息");
            }
            var requestedSubjectIds = (registration.Data["subjectIds"] as JsonArray ?? [])
                .Select(item => item?.GetValue<string>() ?? "")
                .ToArray();
            var subjects = requestedSubjectIds
                .Select(id => exam.Subjects.FirstOrDefault(subject => Text(subject, "id") == id))
                .Where(item => item is not null)
                .Cast<JsonObject>()
                .ToArray();
            if (subjects.Length != requestedSubjectIds.Length || subjects.Length == 0)
                throw Error($"{Text(profile.Data, "name")} 的报考科目无效");
            var overlap = OverlappingSubjects(subjects);
            if (overlap is not null)
                throw Error($"{Text(profile.Data, "name")} 报考的“{Text(overlap.Value.Left, "name")}”与“{Text(overlap.Value.Right, "name")}”时间冲突");
            var orderedSubjectIds = subjects
                .OrderBy(subject => subjectOrder.GetValueOrDefault(Text(subject, "id")))
                .Select(subject => Text(subject, "id"))
                .ToArray();
            return new CandidateWork(
                registration,
                profile,
                orderedSubjectIds,
                string.Join('|', orderedSubjectIds),
                $"{mixingScope}:{boundary}");
        }).ToList();

        var activeCenters = centers.Centers
            .Where(center => center.Status == "active")
            .Select(center =>
            {
                var allRooms = centers.Rooms
                    .Where(room => room.CenterId == center.Id && room.Status == "active")
                    .ToArray();
                var regularRooms = allRooms.Where(room => room.RoomType != "spare").ToArray();
                var rooms = regularRooms.Length > 0 ? regularRooms : allRooms;
                if (regularRooms.Length == 0 && allRooms.Length > 0)
                    Warn($"{center.Name} 没有普通启用考场，本次将使用备用考场");
                return new CenterWork(center, rooms, allRooms);
            })
            .Where(center => center.Rooms.Count > 0)
            .ToArray();
        if (activeCenters.Length == 0) throw Error("没有可用的启用考点和考场");

        var remaining = activeCenters.ToDictionary(
            center => center.Center.Id,
            center => exam.Subjects.ToDictionary(
                subject => Text(subject, "id"),
                _ => center.Rooms.Sum(room => room.Capacity),
                StringComparer.Ordinal),
            StringComparer.Ordinal);
        var assignedCounts = activeCenters.ToDictionary(item => item.Center.Id, _ => 0, StringComparer.Ordinal);
        var affinity = new Dictionary<string, int>(StringComparer.Ordinal);
        var groups = candidates
            .GroupBy(candidate => $"{candidate.ScopeKey}:{candidate.Signature}", StringComparer.Ordinal)
            .OrderByDescending(group => group.Count())
            .ThenBy(group => group.Key, StringComparer.Ordinal)
            .ToArray();

        foreach (var group in groups)
        {
            var ordered = group.ToList();
            ordered.Sort((left, right) => StableCompare(seed, left, right));
            foreach (var candidate in ordered)
            {
                var eligible = activeCenters.Where(center =>
                    candidate.SubjectIds.All(subjectId =>
                        remaining[center.Center.Id].GetValueOrDefault(subjectId) > 0)).ToArray();
                if (eligible.Length == 0)
                    throw Error($"{Text(candidate.Profile.Data, "name")} 的全部报考科目无法在同一考点容纳；请增加考场容量或缩小本次报名范围");
                var local = eligible.Where(center => CenterMatchesProfile(center.Center, candidate.Profile, mixingScope)).ToArray();
                if (local.Length == 0)
                    Warn($"{Text(candidate.Profile.Data, "name")} 所属范围内没有足够考点，已跨范围使用可用考点");
                var pool = local.Length > 0 ? local : eligible;
                var selected = pool
                    .OrderByDescending(center => CenterScore(center, candidate, remaining, affinity, assignedCounts))
                    .ThenBy(center => center.Center.Code, StringComparer.Ordinal)
                    .First();
                candidate.CenterId = selected.Center.Id;
                foreach (var subjectId in candidate.SubjectIds) remaining[selected.Center.Id][subjectId]--;
                assignedCounts[selected.Center.Id]++;
                var affinityKey = $"{selected.Center.Id}:{candidate.Signature}";
                affinity[affinityKey] = affinity.GetValueOrDefault(affinityKey) + 1;
            }
        }

        var assignments = candidates.ToDictionary(
            item => item.Registration.Id,
            _ => new List<ArrangementAssignment>(),
            StringComparer.Ordinal);
        var roomUses = new List<RoomUse>();
        foreach (var subject in exam.Subjects)
        {
            var subjectId = Text(subject, "id");
            foreach (var center in activeCenters)
            {
                var subjectCandidates = candidates
                    .Where(candidate => candidate.CenterId == center.Center.Id && candidate.SubjectIds.Contains(subjectId, StringComparer.Ordinal))
                    .ToList();
                if (subjectCandidates.Count == 0) continue;
                subjectCandidates.Sort((left, right) =>
                {
                    var scope = string.Compare(left.ScopeKey, right.ScopeKey, StringComparison.Ordinal);
                    if (scope != 0) return scope;
                    var signature = string.Compare(left.Signature, right.Signature, StringComparison.Ordinal);
                    return signature != 0 ? signature : StableCompare(seed, left, right);
                });
                var roomIndex = 0;
                var seatIndex = 0;
                var currentScopeKey = "";
                var rooms = center.Rooms
                    .OrderBy(room => room.Code, StringComparer.Ordinal)
                    .ThenBy(room => room.Id, StringComparer.Ordinal)
                    .ToArray();
                foreach (var candidate in subjectCandidates)
                {
                    if (currentScopeKey.Length > 0 && candidate.ScopeKey != currentScopeKey && seatIndex > 0)
                    {
                        roomIndex++;
                        seatIndex = 0;
                    }
                    currentScopeKey = candidate.ScopeKey;
                    while (roomIndex < rooms.Length && seatIndex >= rooms[roomIndex].Capacity)
                    {
                        roomIndex++;
                        seatIndex = 0;
                    }
                    if (roomIndex >= rooms.Length)
                        throw Error($"{center.Center.Name} 在“{Text(subject, "name")}”科目下按{MixingScopeNames[mixingScope]}隔离后考场不足；请增加考场或扩大混编范围");
                    var room = rooms[roomIndex];
                    var seat = (room.SeatStart + seatIndex).ToString("00", CultureInfo.InvariantCulture);
                    assignments[candidate.Registration.Id].Add(new ArrangementAssignment(
                        candidate.Registration.Id,
                        subjectId,
                        center.Center.Id,
                        center.Center.Name,
                        room.Id,
                        room.Name,
                        room.Code,
                        room.Building,
                        room.Floor,
                        "",
                        seat,
                        candidate.Signature));
                    roomUses.Add(new(center.Center.Code, room.Code, room.Id));
                    seatIndex++;
                }
            }
        }

        var examRoomCodes = roomUses
            .GroupBy(item => item.RoomId, StringComparer.Ordinal)
            .Select(group => group.Last())
            .OrderBy(item => item.CenterCode, StringComparer.Ordinal)
            .ThenBy(item => item.RoomCode, StringComparer.Ordinal)
            .ThenBy(item => item.RoomId, StringComparer.Ordinal)
            .Select((item, index) => new { item.RoomId, Code = (index + 1).ToString("000", CultureInfo.InvariantCulture) })
            .ToDictionary(item => item.RoomId, item => item.Code, StringComparer.Ordinal);
        foreach (var registrationId in assignments.Keys.ToArray())
        {
            assignments[registrationId] = assignments[registrationId]
                .OrderBy(item => subjectOrder.GetValueOrDefault(item.SubjectId))
                .Select(item => item with { ExamRoomCode = examRoomCodes[item.RoomId] })
                .ToList();
        }

        var sequenceCounters = new Dictionary<string, int>(StringComparer.Ordinal);
        var numbers = new HashSet<string>(StringComparer.Ordinal);
        candidates.Sort((left, right) =>
        {
            var scope = string.Compare(left.ScopeKey, right.ScopeKey, StringComparison.Ordinal);
            return scope != 0 ? scope : StableCompare(seed, left, right);
        });
        var cards = candidates.Select(candidate =>
        {
            var center = activeCenters.First(item => item.Center.Id == candidate.CenterId);
            var primary = assignments[candidate.Registration.Id][0];
            var candidateSchool = directory.Schools.FirstOrDefault(item => item.Id == candidate.Profile.SchoolId);
            var centerSchool = directory.Schools.FirstOrDefault(item => item.Id == center.Center.SchoolId);
            var sources = new Dictionary<string, string>(StringComparer.Ordinal)
            {
                ["district_code"] = NormalizedCode(First(Text(candidate.Profile.Data, "districtCode"), center.Center.DistrictCode)),
                ["center_school_code"] = NormalizedCode(centerSchool?.Code ?? ""),
                ["candidate_school_code"] = NormalizedCode(candidateSchool?.Code ?? ""),
                ["exam_room_code"] = primary.ExamRoomCode,
                ["seat"] = primary.Seat
            };
            var counterKey = SequenceGroup(rule, sources);
            var sequence = sequenceCounters.GetValueOrDefault(counterKey) + 1;
            sequenceCounters[counterKey] = sequence;
            var number = FormatAdmissionNumber(rule, sources, sequence);
            if (!numbers.Add(number))
                throw Error($"规则“{rule.Name}”生成了重复准考证号 {number}，请检查规则组成");
            return new ArrangementCard(
                candidate.Registration.Id,
                number,
                center.Center.Id,
                center.Center.Name,
                center.Center.Code,
                string.Join(' ', new[]
                {
                    center.Center.ProvinceName,
                    center.Center.CityName,
                    center.Center.DistrictName,
                    center.Center.Address
                }.Where(value => value.Length > 0)),
                generatedAt,
                assignments[candidate.Registration.Id]);
        }).ToArray();

        var usedCenters = cards.Select(card => card.CenterId).ToHashSet(StringComparer.Ordinal);
        var sameSchoolCenterCount = candidates.Count(candidate =>
            activeCenters.First(center => center.Center.Id == candidate.CenterId).Center.SchoolId == candidate.Profile.SchoolId);
        var sameSchoolRate = Math.Round(
            sameSchoolCenterCount * 100d / candidates.Count,
            1,
            MidpointRounding.AwayFromZero);
        var reservedSpareRooms = activeCenters.Sum(center =>
        {
            var usedRoomIds = center.Rooms.Select(room => room.Id).ToHashSet(StringComparer.Ordinal);
            return center.AllRooms.Count(room => room.RoomType == "spare" && !usedRoomIds.Contains(room.Id));
        });
        var summary = new ArrangementSummary(
            candidates.Count,
            usedCenters.Count,
            cards.Sum(card => card.Assignments.Count),
            candidates.Select(candidate => candidate.Signature).Distinct(StringComparer.Ordinal).Count(),
            sameSchoolCenterCount,
            sameSchoolRate,
            reservedSpareRooms);
        return new(exam, rule, mixingScope, seed, warnings, summary, cards);
    }

    private static long CenterScore(
        CenterWork center,
        CandidateWork candidate,
        IReadOnlyDictionary<string, Dictionary<string, int>> remaining,
        IReadOnlyDictionary<string, int> affinity,
        IReadOnlyDictionary<string, int> assignedCounts)
    {
        var affinityValue = affinity.GetValueOrDefault($"{center.Center.Id}:{candidate.Signature}");
        var capacity = candidate.SubjectIds.Min(subjectId => remaining[center.Center.Id][subjectId]);
        return affinityValue * 100000L + LocalityScore(center.Center, candidate.Profile) + capacity -
               assignedCounts.GetValueOrDefault(center.Center.Id);
    }

    private static bool CenterMatchesProfile(AdminCenter center, OperationalProfile profile, string scope) => scope switch
    {
        "class" or "school" => center.SchoolId == profile.SchoolId,
        "district" => Text(profile.Data, "districtCode") is { Length: > 0 } district && center.DistrictCode == district,
        "city" => Text(profile.Data, "cityCode") is { Length: > 0 } city && center.CityCode == city,
        _ => Text(profile.Data, "provinceCode") is { Length: > 0 } province && center.ProvinceCode == province
    };

    private static int LocalityScore(AdminCenter center, OperationalProfile profile)
    {
        if (center.SchoolId == profile.SchoolId) return 5000;
        if (Text(profile.Data, "districtCode") is { Length: > 0 } district && center.DistrictCode == district) return 1000;
        if (Text(profile.Data, "cityCode") is { Length: > 0 } city && center.CityCode == city) return 300;
        return Text(profile.Data, "provinceCode") is { Length: > 0 } province && center.ProvinceCode == province ? 100 : 0;
    }

    private static string ScopeValue(OperationalProfile profile, string scope) => scope switch
    {
        "class" => profile.ClassId ?? "",
        "school" => profile.SchoolId ?? "",
        "district" => Text(profile.Data, "districtCode"),
        "city" => Text(profile.Data, "cityCode"),
        _ => Text(profile.Data, "provinceCode")
    };

    private static (JsonObject Left, JsonObject Right)? OverlappingSubjects(IReadOnlyList<JsonObject> subjects)
    {
        for (var left = 0; left < subjects.Count; left++)
        {
            for (var right = left + 1; right < subjects.Count; right++)
            {
                var first = subjects[left];
                var second = subjects[right];
                if (Text(first, "date") != Text(second, "date")) continue;
                var firstStart = Minutes(Text(first, "start"));
                var firstEnd = Minutes(Text(first, "end"));
                var secondStart = Minutes(Text(second, "start"));
                var secondEnd = Minutes(Text(second, "end"));
                if (firstStart is not null && firstEnd is not null && secondStart is not null && secondEnd is not null &&
                    firstStart < secondEnd && secondStart < firstEnd)
                    return (first, second);
            }
        }
        return null;
    }

    private static int? Minutes(string value)
    {
        var parts = value.Split(':');
        return parts.Length >= 2 &&
               int.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var hour) &&
               int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var minute)
            ? hour * 60 + minute
            : null;
    }

    private static string FormatAdmissionNumber(
        AdmissionNumberRule rule,
        IReadOnlyDictionary<string, string> sources,
        int sequence)
    {
        if (rule.Segments.Count == 0) throw Error("所选准考证号规则没有可用的组成段");
        return string.Join(rule.Separator, rule.Segments.Select(segment =>
        {
            var value = segment.Source == "sequence"
                ? sequence.ToString(CultureInfo.InvariantCulture)
                : sources.GetValueOrDefault(segment.Source) ?? "";
            if (value.Length == 0)
                throw Error($"准考证号规则需要“{First(segment.Label, segment.Source)}”，但考生或考点档案中缺少该值");
            return segment.Width is > 0 ? value.PadLeft(segment.Width.Value, '0') : value;
        }));
    }

    private static string SequenceGroup(AdmissionNumberRule rule, IReadOnlyDictionary<string, string> sources)
    {
        if (rule.Code.StartsWith("district_", StringComparison.Ordinal)) return sources["district_code"];
        if (rule.Code.StartsWith("center_school_", StringComparison.Ordinal)) return sources["center_school_code"];
        if (rule.Code.StartsWith("candidate_school_", StringComparison.Ordinal)) return sources["candidate_school_code"];
        return "all";
    }

    private static int StableCompare(string seed, CandidateWork left, CandidateWork right)
    {
        var leftHash = HashText($"{seed}:{left.Registration.Id}");
        var rightHash = HashText($"{seed}:{right.Registration.Id}");
        var comparison = leftHash.CompareTo(rightHash);
        return comparison != 0
            ? comparison
            : string.Compare(left.Registration.Id, right.Registration.Id, StringComparison.Ordinal);
    }

    private static uint HashText(string value)
    {
        unchecked
        {
            uint hash = 2166136261;
            foreach (var character in value)
            {
                hash ^= character;
                hash *= 16777619;
            }
            return hash;
        }
    }

    private static string NormalizedCode(string value) =>
        NonAlphaNumeric().Replace(value, "").ToUpperInvariant();

    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 AdminArrangementException Error(string message, int status = 409) => new(status, message);

    [GeneratedRegex("[^0-9A-Z]", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
    private static partial Regex NonAlphaNumeric();

    private sealed class CandidateWork(
        OperationalRegistration registration,
        OperationalProfile profile,
        IReadOnlyList<string> subjectIds,
        string signature,
        string scopeKey)
    {
        public OperationalRegistration Registration { get; } = registration;
        public OperationalProfile Profile { get; } = profile;
        public IReadOnlyList<string> SubjectIds { get; } = subjectIds;
        public string Signature { get; } = signature;
        public string ScopeKey { get; } = scopeKey;
        public string? CenterId { get; set; }
    }

    private sealed record CenterWork(
        AdminCenter Center,
        IReadOnlyList<AdminCenterRoom> Rooms,
        IReadOnlyList<AdminCenterRoom> AllRooms);

    private sealed record RoomUse(string CenterCode, string RoomCode, string RoomId);
}
