using System.Globalization;
using System.Text.Json.Nodes;
using Eis.Application.Candidate;
using Eis.Infrastructure.Authentication;
using Eis.Infrastructure.Security;
using QRCoder;

namespace Eis.Infrastructure.Candidate;

internal sealed partial class CandidateService
{
    public async Task<CandidateEndpointResult> GetResultsAsync(
        string sessionToken,
        string verificationBaseUrl,
        CancellationToken cancellationToken)
    {
        var context = await ResolveAsync(sessionToken, profileRoute: false, cancellationToken);
        if (context.Error is not null)
        {
            return context.Error;
        }

        var user = context.User!;
        var profile = context.Profile!;
        var body = await cache.RememberJsonAsync(
            "results",
            $"candidate:{Uri.EscapeDataString(user.Id)}",
            () => LoadResultsAsync(user, profile, verificationBaseUrl, cancellationToken),
            PositiveInteger(Environment.GetEnvironmentVariable("REDIS_RESULTS_CACHE_TTL_SECONDS"), 86400));
        return Success(body);
    }

    private async Task<JsonObject> LoadResultsAsync(
        AuthenticationUser user,
        JsonObject profile,
        string verificationBaseUrl,
        CancellationToken cancellationToken)
    {
        var snapshot = await snapshotLoader.LoadAsync(user.Id, cancellationToken);
        var registrationById = snapshot.Registrations.ToDictionary(item => item.Id, StringComparer.Ordinal);
        var results = snapshot.Results
            .Where(item => item.Published && registrationById.ContainsKey(item.RegistrationId))
            .Select(item => ResultView(snapshot, item, registrationById[item.RegistrationId]))
            .ToArray();
        var summaries = new List<JsonObject>();
        foreach (var registration in snapshot.Registrations)
        {
            var summary = ExamResultSummary(snapshot, registration);
            if (summary is null || summary["publishedSubjects"]?.GetValue<int>() is not > 0)
            {
                continue;
            }

            var exam = snapshot.Exams.First(item => item.Id == registration.ExamId);
            var reportResults = snapshot.Results
                .Where(item => item.RegistrationId == registration.Id && item.Published)
                .Select(item => new ScoreSignatureItem(item.SubjectId, item.Score, item.PublishedAt, item.UpdatedAt));
            var code = documentCodes.ScoreReportCode(registration.Id, registration.UserId, exam.Id, reportResults);
            summary["verificationCode"] = code;
            summary["verificationQr"] = CreateQrCodeDataUrl($"{verificationBaseUrl}/#verify/{Uri.EscapeDataString(code)}");
            summaries.Add(summary);
        }

        return new JsonObject
        {
            ["ok"] = true,
            ["results"] = new JsonArray(results),
            ["summaries"] = new JsonArray(summaries.Select(item => (JsonNode)item).ToArray()),
            ["candidate"] = new JsonObject
            {
                ["name"] = Text(profile, "name").Length > 0 ? Text(profile, "name") : user.DisplayName,
                ["candidateNumber"] = user.CandidateNumber ?? string.Empty
            }
        };
    }

    private static int PositiveInteger(string? value, int fallback) =>
        int.TryParse(value, out var parsed) && parsed > 0 ? Math.Min(parsed, 86400) : fallback;

    public async Task<CandidateEndpointResult> CreateScoreAppealAsync(
        string sessionToken,
        string resultId,
        JsonObject body,
        CancellationToken cancellationToken)
    {
        var context = await ResolveAsync(sessionToken, profileRoute: false, cancellationToken);
        if (context.Error is not null)
        {
            return context.Error;
        }

        var user = context.User!;
        var snapshot = await snapshotLoader.LoadAsync(user.Id, cancellationToken);
        var result = snapshot.Results.FirstOrDefault(item => item.Id == resultId && item.Published);
        var registration = result is null
            ? null
            : snapshot.Registrations.FirstOrDefault(item => item.Id == result.RegistrationId);
        if (result is null || registration is null)
        {
            return Error(404, "已发布成绩不存在或不属于当前考生");
        }

        var exam = snapshot.Exams.FirstOrDefault(item => item.Id == registration.ExamId);
        if (exam?.ArchivedAt is not null)
        {
            return Error(409, "该考试已归档，成绩及复议入口已永久锁定");
        }

        if (FindInstance(snapshot, "score_appeal", result.Id) is { Status: "pending" })
        {
            return Error(409, "该科成绩已有待处理复议，请勿重复提交");
        }

        var reason = Clean(body, "reason", 500);
        if (reason.Length < 5)
        {
            return Error(400, "请至少填写 5 个字的复议理由");
        }

        var workflowResult = CreateWorkflowSubmission(
            snapshot,
            "score_appeal",
            result.Id,
            context.Profile!,
            user.Id);
        if (workflowResult.Error is not null)
        {
            return workflowResult.Error;
        }

        var submission = workflowResult.Submission!;
        var action = submission.Action with { Note = reason };
        var subject = exam?.Subjects.FirstOrDefault(item => item.Id == result.SubjectId);
        var log = new CandidateAuditLog(
            Uid("log"),
            user.Id,
            "提交成绩复议",
            $"{exam?.Name ?? string.Empty} · {subject?.Name ?? string.Empty}",
            NowIso());
        await writeRepository.CreateWorkflowAsync(submission.Instance, action, log, cancellationToken);

        var responseSnapshot = snapshot with
        {
            WorkflowActions = [.. snapshot.WorkflowActions, action]
        };
        return new CandidateEndpointResult(201, new JsonObject
        {
            ["ok"] = true,
            ["workflow"] = WorkflowView(responseSnapshot, submission.Instance)
        });
    }

    private static JsonObject ResultView(
        CandidateReadSnapshot snapshot,
        CandidateResult result,
        CandidateRegistration registration)
    {
        var exam = snapshot.Exams.First(item => item.Id == registration.ExamId);
        var subject = exam.Subjects.FirstOrDefault(item => item.Id == result.SubjectId);
        var appealInstance = FindInstance(snapshot, "score_appeal", result.Id);
        var appeal = WorkflowView(snapshot, appealInstance);
        if (appeal is not null)
        {
            appeal["reason"] = appeal["actions"]?.AsArray().OfType<JsonObject>()
                .FirstOrDefault(item => item["action"]?.GetValue<string>() == "submit")?["note"]?.GetValue<string>() ?? string.Empty;
        }

        var json = ResultJson(result);
        json["rank"] = JsonValue.Create(result.Rank);
        json["cohortSize"] = result.CohortSize;
        json["rankPercent"] = JsonValue.Create(result.RankPercent);
        json["grade"] = result.Grade;
        json["examId"] = exam.Id;
        json["examName"] = exam.Name;
        json["examCode"] = exam.Code;
        json["examStart"] = exam.ExamStart;
        json["archivedAt"] = JsonValue.Create(exam.ArchivedAt);
        json["subjectName"] = subject?.Name ?? result.SubjectId;
        json["fullScore"] = subject?.FullScore ?? 150;
        json["passRule"] = subject?.PassRule ?? "fixed_score";
        json["passValue"] = subject?.PassValue ?? subject?.PassScore;
        json["passScore"] = JsonValue.Create(result.PassScore);
        json["cutoffRank"] = JsonValue.Create(result.CutoffRank);
        json["passText"] = SubjectPassText(subject);
        json["qualified"] = JsonValue.Create(result.Qualified);
        json["appeal"] = appeal;
        return json;
    }

    private static JsonObject? ExamResultSummary(
        CandidateReadSnapshot snapshot,
        CandidateRegistration registration)
    {
        if (registration.Status != "approved")
        {
            return null;
        }

        var exam = snapshot.Exams.FirstOrDefault(item => item.Id == registration.ExamId);
        if (exam is null)
        {
            return null;
        }

        var subjects = exam.Subjects.Where(item => registration.SubjectIds.Contains(item.Id, StringComparer.Ordinal)).ToArray();
        var published = snapshot.Results
            .Where(item => item.RegistrationId == registration.Id && item.Published)
            .ToArray();
        var stored = snapshot.ResultSummaries.FirstOrDefault(item => item.RegistrationId == registration.Id);
        var complete = stored?.Complete ?? false;
        var total = stored?.TotalScore ?? 0;
        var fullScore = stored?.FullScore ?? subjects.Sum(item => item.FullScore);
        var scoreRatio = stored?.ScoreRatio ?? 0;
        var policy = exam.PassPolicy == "score_ratio" ? "rank_percent" : exam.PassPolicy;
        if (policy.Length == 0)
        {
            policy = "rank_percent";
        }

        var value = exam.PassValue;

        return new JsonObject
        {
            ["examId"] = exam.Id,
            ["examName"] = exam.Name,
            ["examCode"] = exam.Code,
            ["examStart"] = exam.ExamStart,
            ["archivedAt"] = JsonValue.Create(exam.ArchivedAt),
            ["complete"] = complete,
            ["publishedSubjects"] = published.Length,
            ["subjectCount"] = subjects.Length,
            ["featureScore"] = registration.FeatureScore,
            ["total"] = total,
            ["fullScore"] = fullScore,
            ["scoreRatio"] = Math.Round(scoreRatio, 2, MidpointRounding.AwayFromZero),
            ["passPolicy"] = policy,
            ["passValue"] = value,
            ["qualified"] = JsonValue.Create(stored?.Qualified),
            ["rank"] = JsonValue.Create(stored?.Rank),
            ["cohortSize"] = JsonValue.Create(stored?.CohortSize),
            ["rankPercent"] = JsonValue.Create(stored?.RankPercent),
            ["cutoffRank"] = JsonValue.Create(stored?.CutoffRank)
        };
    }

    private static string SubjectPassText(CandidateSubject? subject) => (subject?.PassRule ?? "fixed_score") switch
    {
        "none" => "不设单科线",
        "rank_percent" => $"本科排名前 {FormatNumber(subject?.PassValue ?? 60)}% 达线",
        _ => $"固定 {FormatNumber(Math.Round(subject?.PassValue ?? subject?.PassScore ?? 0, 2, MidpointRounding.AwayFromZero))} 分"
    };


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

    private static string CreateQrCodeDataUrl(string value)
    {
        using var generator = new QRCodeGenerator();
        using var data = generator.CreateQrCode(value, QRCodeGenerator.ECCLevel.M);
        using var qrCode = new PngByteQRCode(data);
        return $"data:image/png;base64,{Convert.ToBase64String(qrCode.GetGraphic(10))}";
    }

}
