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

namespace Eis.Infrastructure.Candidate;

internal sealed partial class CandidateService(
    IAuthenticationStateStore authenticationState,
    AuthenticationRepository authenticationRepository,
    CandidateReadSnapshotLoader snapshotLoader,
    IPublicQueryService publicQueries,
    CandidateWriteRepository writeRepository,
    RegionCatalog regionCatalog,
    DocumentVerificationCodeService documentCodes,
    CandidateAdmissionRepository admissionRepository,
    IApplicationCache cache) : ICandidateService
{
    public async Task<CandidateEndpointResult> GetDashboardAsync(
        string sessionToken,
        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 snapshot = await snapshotLoader.LoadAsync(user.Id, cancellationToken);
        var registrations = snapshot.Registrations.Select(item => RegistrationView(snapshot, item)).ToArray();
        var registrationIds = snapshot.Registrations.Select(item => item.Id).ToHashSet(StringComparer.Ordinal);
        var results = snapshot.Results
            .Where(item => item.Published && registrationIds.Contains(item.RegistrationId))
            .Select(ResultJson)
            .ToArray();
        var home = await publicQueries.GetHomeAsync(cancellationToken);
        var notices = home["notices"]?.AsArray()
            .OfType<JsonObject>()
            .OrderByDescending(item => ParseDate(item["publishAt"]?.GetValue<string>()))
            .Take(5)
            .Select(item => item?.DeepClone())
            .ToArray() ?? [];
        var profileInstance = FindInstance(snapshot, "profile_change", ProfileId(profile));

        return Success(new JsonObject
        {
            ["ok"] = true,
            ["profile"] = profile,
            ["profileWorkflow"] = WorkflowView(snapshot, profileInstance),
            ["registrations"] = new JsonArray(registrations),
            ["results"] = new JsonArray(results),
            ["notices"] = new JsonArray(notices)
        });
    }

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

        var home = await publicQueries.GetHomeAsync(cancellationToken);
        return Success(new JsonObject
        {
            ["ok"] = true,
            ["notices"] = home["notices"]?.DeepClone() ?? new JsonArray()
        });
    }

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

        var snapshot = await snapshotLoader.LoadAsync(context.User!.Id, cancellationToken);
        var profile = context.Profile!;
        var instance = FindInstance(snapshot, "profile_change", ProfileId(profile));
        var home = await publicQueries.GetHomeAsync(cancellationToken);
        return Success(new JsonObject
        {
            ["ok"] = true,
            ["profile"] = profile,
            ["workflow"] = WorkflowView(snapshot, instance),
            ["schools"] = home["schools"]?.DeepClone() ?? new JsonArray(),
            ["classes"] = home["classes"]?.DeepClone() ?? new JsonArray()
        });
    }

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

        var snapshot = await snapshotLoader.LoadAsync(context.User!.Id, cancellationToken);
        var registrationsByExam = snapshot.Registrations
            .GroupBy(item => item.ExamId)
            .ToDictionary(group => group.Key, group => group.First(), StringComparer.Ordinal);
        var home = await publicQueries.GetHomeAsync(cancellationToken);
        var exams = home["exams"]?.AsArray().OfType<JsonObject>().Select(item =>
        {
            var exam = item.DeepClone().AsObject();
            exam.Remove("registrationCount");
            var examId = exam["id"]?.GetValue<string>() ?? string.Empty;
            exam["registration"] = registrationsByExam.TryGetValue(examId, out var registration)
                ? RegistrationJson(registration)
                : null;
            return exam;
        }).ToArray() ?? [];

        return Success(new JsonObject
        {
            ["ok"] = true,
            ["profileStatus"] = context.Profile!["status"]?.GetValue<string>() ?? string.Empty,
            ["exams"] = new JsonArray(exams)
        });
    }

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

        var snapshot = await snapshotLoader.LoadAsync(context.User!.Id, cancellationToken);
        return Success(new JsonObject
        {
            ["ok"] = true,
            ["registrations"] = new JsonArray(snapshot.Registrations.Select(item => RegistrationView(snapshot, item)).ToArray())
        });
    }

    private async Task<ResolvedCandidate> ResolveAsync(
        string sessionToken,
        bool profileRoute,
        CancellationToken cancellationToken)
    {
        if (sessionToken.Length == 0)
        {
            return ResolvedCandidate.Failed(Error(401, "请先登录"));
        }

        var userId = await authenticationState.GetSessionUserIdAsync(sessionToken);
        if (userId is null)
        {
            return ResolvedCandidate.Failed(Error(401, "请先登录"));
        }

        var user = await authenticationRepository.FindUserByIdAsync(userId, cancellationToken);
        if (user is not { Active: true, ArchivedAt: null })
        {
            return ResolvedCandidate.Failed(Error(401, "请先登录"));
        }

        if (user.Role != "candidate")
        {
            return ResolvedCandidate.Failed(Error(403, "当前账号无权执行此操作"));
        }

        var profile = await authenticationRepository.GetCandidateProfileAsync(user.Id, cancellationToken);
        if (profile is null)
        {
            return ResolvedCandidate.Failed(Error(404, "考生资料不存在"));
        }

        if (user.MustChangePassword)
        {
            return ResolvedCandidate.Failed(Error(428, "首次登录必须先修改初始密码"));
        }

        if (!profileRoute && profile["profileCompleted"]?.GetValue<bool>() != true)
        {
            return ResolvedCandidate.Failed(Error(428, "请先补全个人信息并提交审核"));
        }

        return new ResolvedCandidate(user, profile, null);
    }

    private static CandidateWorkflowInstance? FindInstance(
        CandidateReadSnapshot snapshot,
        string businessType,
        string businessId)
    {
        var matches = snapshot.WorkflowInstances
            .Where(item => item.BusinessType == businessType && item.BusinessId == businessId)
            .ToArray();
        return matches.FirstOrDefault(item => item.Status == "pending") ?? matches.FirstOrDefault();
    }

    private static JsonObject RegistrationView(CandidateReadSnapshot snapshot, CandidateRegistration registration)
    {
        var json = RegistrationJson(registration);
        var exam = snapshot.Exams.FirstOrDefault(item => item.Id == registration.ExamId);
        var subjects = exam?.Subjects.Where(item => registration.SubjectIds.Contains(item.Id, StringComparer.Ordinal)).ToArray() ?? [];
        var instance = FindInstance(snapshot, "registration_review", registration.Id);
        json["exam"] = exam is null ? null : ExamJson(exam);
        json["subjects"] = new JsonArray(subjects.Select(SubjectJson).ToArray());
        json["amountDue"] = Math.Round(subjects.Sum(item => item.Fee), 2, MidpointRounding.AwayFromZero);
        json["paidByName"] = registration.PaidBy is not null && snapshot.Users.TryGetValue(registration.PaidBy, out var payer)
            ? payer.DisplayName
            : string.Empty;
        json["workflow"] = WorkflowView(snapshot, instance);
        return json;
    }

    private static JsonObject RegistrationJson(CandidateRegistration item) => new()
    {
        ["id"] = item.Id,
        ["userId"] = item.UserId,
        ["examId"] = item.ExamId,
        ["subjectIds"] = new JsonArray(item.SubjectIds.Select(value => JsonValue.Create(value)).ToArray()),
        ["status"] = item.Status,
        ["paymentStatus"] = item.PaymentStatus,
        ["paidAt"] = JsonValue.Create(item.PaidAt),
        ["paidBy"] = JsonValue.Create(item.PaidBy),
        ["createdAt"] = item.CreatedAt,
        ["reviewedAt"] = JsonValue.Create(item.ReviewedAt),
        ["reviewNote"] = item.ReviewNote,
        ["registrationNumber"] = item.RegistrationNumber,
        ["numberRuleId"] = JsonValue.Create(item.NumberRuleId),
        ["featureScore"] = item.FeatureScore,
        ["admitCard"] = item.AdmitCard is null ? null : AdmitCardJson(item.AdmitCard)
    };

    private static JsonObject AdmitCardJson(CandidateAdmitCard item) => new()
    {
        ["planId"] = item.PlanId,
        ["number"] = item.Number,
        ["centerId"] = JsonValue.Create(item.CenterId),
        ["testCenter"] = item.TestCenter,
        ["centerCode"] = item.CenterCode,
        ["centerAddress"] = item.CenterAddress,
        ["room"] = item.Room,
        ["seat"] = item.Seat,
        ["assignments"] = new JsonArray(item.Assignments.Select(AssignmentJson).ToArray()),
        ["generatedAt"] = item.GeneratedAt
    };

    private static JsonObject AssignmentJson(CandidateAdmitAssignment item) => new()
    {
        ["subjectId"] = item.SubjectId,
        ["roomId"] = JsonValue.Create(item.RoomId),
        ["roomName"] = item.RoomName,
        ["room"] = item.RoomName,
        ["roomCode"] = item.RoomCode,
        ["examRoomCode"] = item.ExamRoomCode,
        ["building"] = item.Building,
        ["floor"] = item.Floor,
        ["seat"] = item.Seat,
        ["subjectSignature"] = item.SubjectSignature
    };

    private static JsonObject ExamJson(CandidateExam 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),
        ["createdAt"] = item.CreatedAt,
        ["subjects"] = new JsonArray(item.Subjects.Select(SubjectJson).ToArray())
    };

    private static JsonObject SubjectJson(CandidateSubject 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 static JsonObject ResultJson(CandidateResult item) => new()
    {
        ["id"] = item.Id,
        ["registrationId"] = item.RegistrationId,
        ["subjectId"] = item.SubjectId,
        ["score"] = item.Score,
        ["grade"] = item.Grade,
        ["published"] = item.Published,
        ["updatedAt"] = JsonValue.Create(item.UpdatedAt),
        ["publishedAt"] = JsonValue.Create(item.PublishedAt)
    };

    private static JsonObject? WorkflowView(
        CandidateReadSnapshot snapshot,
        CandidateWorkflowInstance? instance)
    {
        if (instance is null)
        {
            return null;
        }

        var workflow = snapshot.Workflows.FirstOrDefault(item => item.Id == instance.WorkflowId);
        var steps = workflow?.Steps ?? [];
        snapshot.Users.TryGetValue(instance.AssigneeId ?? string.Empty, out var assignee);
        var actions = snapshot.WorkflowActions.Where(item => item.InstanceId == instance.Id).Select(item => new JsonObject
        {
            ["id"] = item.Id,
            ["instanceId"] = item.InstanceId,
            ["actorId"] = JsonValue.Create(item.ActorId),
            ["action"] = item.Action,
            ["note"] = item.Note,
            ["fromAssigneeId"] = JsonValue.Create(item.FromAssigneeId),
            ["toAssigneeId"] = JsonValue.Create(item.ToAssigneeId),
            ["createdAt"] = item.CreatedAt,
            ["actorName"] = UserName(snapshot, item.ActorId, "系统"),
            ["fromAssigneeName"] = UserName(snapshot, item.FromAssigneeId, string.Empty),
            ["toAssigneeName"] = UserName(snapshot, item.ToAssigneeId, string.Empty)
        }).ToArray();
        return new JsonObject
        {
            ["id"] = instance.Id,
            ["workflowId"] = instance.WorkflowId,
            ["businessType"] = instance.BusinessType,
            ["businessId"] = instance.BusinessId,
            ["status"] = instance.Status,
            ["currentStep"] = instance.CurrentStep,
            ["assigneeId"] = JsonValue.Create(instance.AssigneeId),
            ["createdAt"] = instance.CreatedAt,
            ["completedAt"] = JsonValue.Create(instance.CompletedAt),
            ["workflowName"] = workflow?.Name ?? "未命名流程",
            ["steps"] = new JsonArray(steps.Select(WorkflowStepJson).ToArray()),
            ["currentStepDetail"] = steps.FirstOrDefault(item => item.Position == instance.CurrentStep) is { } current
                ? WorkflowStepJson(current)
                : null,
            ["assignee"] = assignee is null ? null : SafeUser(assignee),
            ["actions"] = new JsonArray(actions)
        };
    }

    private static JsonObject WorkflowStepJson(CandidateWorkflowStep item) => new()
    {
        ["id"] = item.Id,
        ["name"] = item.Name,
        ["adminLevel"] = item.AdminLevel,
        ["position"] = item.Position
    };

    private static JsonObject SafeUser(CandidateUserSummary user) => new()
    {
        ["id"] = user.Id,
        ["username"] = user.Username,
        ["role"] = user.Role,
        ["adminLevel"] = user.Role == "admin" ? user.AdminLevel ?? "super" : null,
        ["schoolId"] = JsonValue.Create(user.SchoolId),
        ["classId"] = JsonValue.Create(user.ClassId),
        ["displayName"] = user.DisplayName,
        ["candidateNumber"] = JsonValue.Create(user.CandidateNumber),
        ["mustChangePassword"] = user.MustChangePassword,
        ["totpEnabled"] = user.TotpEnabled,
        ["archived"] = user.ArchivedAt is not null
    };

    private static string UserName(CandidateReadSnapshot snapshot, string? id, string fallback) =>
        id is not null && snapshot.Users.TryGetValue(id, out var user) ? user.DisplayName : fallback;

    private static string ProfileId(JsonObject profile) => profile["id"]?.GetValue<string>() ?? string.Empty;

    private static DateTimeOffset ParseDate(string? value) =>
        DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var parsed)
            ? parsed
            : DateTimeOffset.MinValue;

    private static CandidateEndpointResult Success(JsonObject body) => new(200, body);

    private static CandidateEndpointResult Error(int statusCode, string message) => new(
        statusCode,
        new JsonObject { ["ok"] = false, ["message"] = message });

    private sealed record ResolvedCandidate(
        AuthenticationUser? User,
        JsonObject? Profile,
        CandidateEndpointResult? Error)
    {
        public static ResolvedCandidate Failed(CandidateEndpointResult error) => new(null, null, error);
    }
}
