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

namespace Eis.Infrastructure.Administration;

internal sealed record AdminResultMetric(
    string ResultId,
    int? Rank,
    int CohortSize,
    double? RankPercent,
    string Grade,
    bool? Qualified,
    double? PassScore,
    int? CutoffRank,
    string CalculatedAt);

internal sealed record AdminRegistrationResultSummary(
    string RegistrationId,
    string ExamId,
    int PublishedSubjectCount,
    int SubjectCount,
    bool Complete,
    double TotalScore,
    double FullScore,
    double ScoreRatio,
    int? Rank,
    int CohortSize,
    double? RankPercent,
    bool? Qualified,
    int? CutoffRank,
    string CalculatedAt);

internal sealed record AdminResultCalculation(
    string ExamId,
    IReadOnlyList<AdminResultMetric> Metrics,
    IReadOnlyList<AdminRegistrationResultSummary> Summaries);

internal static class AdminResultStatistics
{
    public static AdminResultCalculation Calculate(
        OperationalExam exam,
        IReadOnlyList<OperationalRegistration> registrations,
        IReadOnlyList<AdminWorkflowResult> results,
        string calculatedAt)
    {
        var approved = registrations
            .Where(item => item.ExamId == exam.Id && item.Status == "approved")
            .ToArray();
        var registrationById = approved.ToDictionary(item => item.Id, StringComparer.Ordinal);
        var examResults = results
            .Where(item => registrationById.ContainsKey(item.RegistrationId))
            .ToArray();
        var subjectById = exam.Subjects
            .Where(item => Text(item["id"]).Length > 0)
            .ToDictionary(item => Text(item["id"]), StringComparer.Ordinal);
        var resultsBySubject = examResults
            .GroupBy(item => item.SubjectId)
            .ToDictionary(group => group.Key, group => group.ToArray(), StringComparer.Ordinal);
        var metrics = new List<AdminResultMetric>(examResults.Length);

        foreach (var subjectGroup in resultsBySubject)
        {
            var subject = subjectById.GetValueOrDefault(subjectGroup.Key);
            var publishedScores = subjectGroup.Value
                .Where(item => item.Published)
                .Select(item => item.Score)
                .OrderByDescending(item => item)
                .ToArray();
            var publishedCount = publishedScores.Length;
            foreach (var result in subjectGroup.Value)
            {
                var cohortSize = publishedCount + (result.Published ? 0 : 1);
                int? rank = cohortSize == 0 ? null : 1 + CountGreater(publishedScores, result.Score);
                double? rankPercent = rank is null
                    ? null
                    : Math.Round(rank.Value / (double)cohortSize * 100, 2, MidpointRounding.AwayFromZero);
                var grade = result.Published && rank is not null ? Grade(rank.Value, cohortSize) : "待发布";
                var pass = Pass(subject, result.Score, rank, cohortSize);
                metrics.Add(new(
                    result.Id,
                    rank,
                    cohortSize,
                    rankPercent,
                    grade,
                    pass.Qualified,
                    pass.PassScore,
                    pass.CutoffRank,
                    calculatedAt));
            }
        }

        var metricByResultId = metrics.ToDictionary(item => item.ResultId, StringComparer.Ordinal);
        var resultsByRegistration = examResults
            .GroupBy(item => item.RegistrationId)
            .ToDictionary(group => group.Key, group => group.ToArray(), StringComparer.Ordinal);
        var states = approved.Select(registration =>
        {
            var subjectIds = SubjectIds(registration)
                .Where(subjectById.ContainsKey)
                .Order(StringComparer.Ordinal)
                .ToArray();
            var publishedBySubject = resultsByRegistration.GetValueOrDefault(registration.Id, [])
                .Where(item => item.Published && subjectIds.Contains(item.SubjectId, StringComparer.Ordinal))
                .GroupBy(item => item.SubjectId)
                .ToDictionary(group => group.Key, group => group.Last(), StringComparer.Ordinal);
            var complete = subjectIds.Length > 0 && subjectIds.All(publishedBySubject.ContainsKey);
            var total = Math.Round(
                subjectIds.Sum(subjectId => publishedBySubject.GetValueOrDefault(subjectId)?.Score ?? 0),
                2,
                MidpointRounding.AwayFromZero);
            var fullScore = Math.Round(
                subjectIds.Sum(subjectId => Number(subjectById[subjectId]["fullScore"])),
                2,
                MidpointRounding.AwayFromZero);
            return new RegistrationState(
                registration,
                subjectIds,
                publishedBySubject,
                complete,
                total,
                fullScore,
                string.Join('|', subjectIds));
        }).ToArray();

        var rankByRegistration = new Dictionary<string, TotalRank>(StringComparer.Ordinal);
        foreach (var group in states.Where(item => item.Complete).GroupBy(item => item.SubjectKey))
        {
            var scores = group.Select(item => item.TotalScore).OrderByDescending(item => item).ToArray();
            foreach (var item in group)
            {
                var rank = 1 + CountGreater(scores, item.TotalScore);
                rankByRegistration[item.Registration.Id] = new(
                    rank,
                    scores.Length,
                    Math.Round(rank / (double)scores.Length * 100, 2, MidpointRounding.AwayFromZero));
            }
        }

        var policy = Text(exam.Data["passPolicy"]);
        if (policy is "" or "score_ratio") policy = "rank_percent";
        var policyValue = Number(exam.Data["passValue"]);
        var summaries = states.Select(item =>
        {
            var totalRank = rankByRegistration.GetValueOrDefault(item.Registration.Id);
            bool? qualified = null;
            int? cutoffRank = null;
            if (item.Complete)
            {
                if (policy == "fixed_score") qualified = item.TotalScore >= policyValue;
                else if (policy == "subject_scores")
                    qualified = item.PublishedBySubject.Values.All(result =>
                        metricByResultId.GetValueOrDefault(result.Id)?.Qualified is not false);
                else if (policy == "rank_percent" && totalRank is not null)
                {
                    cutoffRank = Math.Max(1, (int)Math.Ceiling(totalRank.CohortSize * policyValue / 100));
                    qualified = totalRank.Rank <= cutoffRank;
                }
            }
            var scoreRatio = item.FullScore > 0
                ? Math.Round(item.TotalScore / item.FullScore * 100, 2, MidpointRounding.AwayFromZero)
                : 0;
            return new AdminRegistrationResultSummary(
                item.Registration.Id,
                exam.Id,
                item.PublishedBySubject.Count,
                item.SubjectIds.Length,
                item.Complete,
                item.TotalScore,
                item.FullScore,
                scoreRatio,
                totalRank?.Rank,
                totalRank?.CohortSize ?? 0,
                totalRank?.RankPercent,
                qualified,
                cutoffRank,
                calculatedAt);
        }).ToArray();

        return new(exam.Id, metrics, summaries);
    }

    public static string PassText(JsonObject? subject)
    {
        var rule = Text(subject?["passRule"]);
        if (rule == "none") return "不设单科线";
        if (rule == "rank_percent")
            return $"本科排名前 {Format(Number(subject?["passValue"], 60))}% 达线";
        return $"固定 {Format(Math.Round(Number(subject?["passValue"] ?? subject?["passScore"]), 2, MidpointRounding.AwayFromZero))} 分";
    }

    public static string Format(double value) => value.ToString("0.##", CultureInfo.InvariantCulture);

    public static bool IsValidScore(double score, double fullScore) =>
        double.IsFinite(score) &&
        score >= 0 &&
        score <= fullScore &&
        Math.Abs(score * 2 - Math.Round(score * 2, MidpointRounding.AwayFromZero)) < 0.0000001;

    private static SubjectPass Pass(JsonObject? subject, double score, int? rank, int cohortSize)
    {
        var rule = Text(subject?["passRule"]);
        if (rule.Length == 0) rule = "fixed_score";
        if (rule == "none") return new(null, null, null);
        if (rule == "rank_percent")
        {
            var cutoff = Math.Max(1, (int)Math.Ceiling(cohortSize * Number(subject?["passValue"], 60) / 100));
            return new(rank <= cutoff, null, cutoff);
        }
        var threshold = Math.Round(
            Number(subject?["passValue"] ?? subject?["passScore"]),
            2,
            MidpointRounding.AwayFromZero);
        return new(score >= threshold, threshold, null);
    }

    private static int CountGreater(IReadOnlyList<double> descendingScores, double score)
    {
        var low = 0;
        var high = descendingScores.Count;
        while (low < high)
        {
            var middle = low + (high - low) / 2;
            if (descendingScores[middle] > score) low = middle + 1;
            else high = middle;
        }
        return low;
    }

    private static string Grade(int rank, int cohortSize)
    {
        int Cutoff(double ratio) => Math.Max(1, (int)Math.Ceiling(cohortSize * ratio));
        if (rank <= Cutoff(0.1)) return "A+";
        if (rank <= Cutoff(0.25)) return "A";
        if (rank <= Cutoff(0.5)) return "B+";
        if (rank <= Cutoff(0.7)) return "B";
        return rank <= Cutoff(0.9) ? "C" : "D";
    }

    private static string[] SubjectIds(OperationalRegistration registration) =>
        registration.Data["subjectIds"] is JsonArray items
            ? items.Select(Text).Where(item => item.Length > 0).Distinct(StringComparer.Ordinal).ToArray()
            : [];

    private static double Number(JsonNode? node, double fallback = 0) =>
        double.TryParse(Text(node), NumberStyles.Float, CultureInfo.InvariantCulture, out var value)
            ? value
            : fallback;

    private static string Text(JsonNode? node) =>
        node is JsonValue value && value.TryGetValue<string>(out var text)
            ? text
            : node?.ToString() ?? "";

    private sealed record SubjectPass(bool? Qualified, double? PassScore, int? CutoffRank);
    private sealed record TotalRank(int Rank, int CohortSize, double RankPercent);
    private sealed record RegistrationState(
        OperationalRegistration Registration,
        string[] SubjectIds,
        IReadOnlyDictionary<string, AdminWorkflowResult> PublishedBySubject,
        bool Complete,
        double TotalScore,
        double FullScore,
        string SubjectKey);
}
