using System.Data.Common;
using System.Globalization;
using Eis.Infrastructure.Data;

namespace Eis.Infrastructure.Candidate;

internal sealed record CandidateReadSnapshot(
    IReadOnlyList<CandidateExam> Exams,
    IReadOnlyList<CandidateRegistration> Registrations,
    IReadOnlyList<CandidateResult> Results,
    IReadOnlyList<CandidateResultSummary> ResultSummaries,
    IReadOnlyList<CandidateWorkflow> Workflows,
    IReadOnlyList<CandidateWorkflowInstance> WorkflowInstances,
    IReadOnlyList<CandidateWorkflowAction> WorkflowActions,
    IReadOnlyDictionary<string, CandidateUserSummary> Users,
    IReadOnlyDictionary<string, CandidateTestCenter> TestCenters,
    IReadOnlyDictionary<string, CandidateTestRoom> TestRooms,
    string OrganizationName);

internal sealed record CandidateExam(
    string Id,
    string Code,
    string Name,
    string Description,
    string RegistrationStart,
    string RegistrationEnd,
    string ExamStart,
    string ExamEnd,
    string AdmitDownloadStart,
    string AdmitDownloadEnd,
    string Location,
    string PassPolicy,
    double PassValue,
    string Status,
    string? ArchivedAt,
    string? ArchivedBy,
    string CreatedAt,
    IReadOnlyList<CandidateSubject> Subjects);

internal sealed record CandidateSubject(
    string Id,
    string Name,
    string Date,
    string Start,
    string End,
    double Fee,
    double FullScore,
    string PassRule,
    double PassValue,
    double? PassScore,
    int Order);

internal sealed record CandidateRegistration(
    string Id,
    string UserId,
    string ExamId,
    IReadOnlyList<string> SubjectIds,
    string Status,
    string PaymentStatus,
    string? PaidAt,
    string? PaidBy,
    string CreatedAt,
    string? ReviewedAt,
    string ReviewNote,
    string RegistrationNumber,
    string? NumberRuleId,
    double FeatureScore,
    CandidateAdmitCard? AdmitCard);

internal sealed record CandidateAdmitCard(
    string PlanId,
    string Number,
    string? CenterId,
    string TestCenter,
    string CenterCode,
    string CenterAddress,
    string Room,
    string Seat,
    IReadOnlyList<CandidateAdmitAssignment> Assignments,
    string GeneratedAt);

internal sealed record CandidateAdmitAssignment(
    string SubjectId,
    string? RoomId,
    string RoomName,
    string RoomCode,
    string ExamRoomCode,
    string Building,
    string Floor,
    string Seat,
    string SubjectSignature);

internal sealed record CandidateTestCenter(
    string Id,
    string Code,
    string Name,
    string ProvinceName,
    string CityName,
    string DistrictName,
    string Address);

internal sealed record CandidateTestRoom(
    string Id,
    string Code,
    string Name,
    string Building,
    string Floor);

internal sealed record CandidateResult(
    string Id,
    string RegistrationId,
    string SubjectId,
    double Score,
    string Grade,
    bool Published,
    string? UpdatedAt,
    string? PublishedAt,
    int? Rank,
    int CohortSize,
    double? RankPercent,
    bool? Qualified,
    double? PassScore,
    int? CutoffRank);

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

internal sealed record CandidateWorkflow(
    string Id,
    string BusinessType,
    string Name,
    bool Active,
    string? UpdatedBy,
    string UpdatedAt,
    IReadOnlyList<CandidateWorkflowStep> Steps);

internal sealed record CandidateWorkflowStep(
    string Id,
    string Name,
    string AdminLevel,
    int Position);

internal sealed record CandidateWorkflowInstance(
    string Id,
    string WorkflowId,
    string BusinessType,
    string BusinessId,
    string Status,
    int CurrentStep,
    string? AssigneeId,
    string CreatedAt,
    string? CompletedAt);

internal sealed record CandidateWorkflowAction(
    string Id,
    string InstanceId,
    string? ActorId,
    string Action,
    string Note,
    string? FromAssigneeId,
    string? ToAssigneeId,
    string CreatedAt);

internal sealed record CandidateUserSummary(
    string Id,
    string Username,
    string Role,
    string? AdminLevel,
    string? SchoolId,
    string? ClassId,
    string DisplayName,
    string? CandidateNumber,
    bool Active,
    bool MustChangePassword,
    bool TotpEnabled,
    string? ArchivedAt,
    string CreatedAt);

internal sealed class CandidateReadSnapshotLoader(IRelationalConnectionFactory connectionFactory)
{
    public async Task<CandidateReadSnapshot> LoadAsync(
        string userId,
        CancellationToken cancellationToken)
    {
        await using var connection = await connectionFactory.OpenAsync(cancellationToken);
        var subjects = await QueryAsync(connection,
            "SELECT id, exam_id, name, subject_date, start_time, end_time, fee, full_score, pass_score, pass_rule, pass_value, position FROM exam_subjects ORDER BY exam_id, position, id",
            ReadSubject,
            cancellationToken);
        var subjectsByExam = subjects.GroupBy(item => item.ExamId)
            .ToDictionary(group => group.Key, group => (IReadOnlyList<CandidateSubject>)group.Select(item => item.Subject).ToArray(), StringComparer.Ordinal);
        var subjectPositions = subjects.ToDictionary(item => item.Subject.Id, item => item.Subject.Order, StringComparer.Ordinal);

        var exams = await QueryAsync(connection,
            "SELECT id, code, name, description, registration_start, registration_end, exam_start, exam_end, admit_download_start, admit_download_end, location, pass_policy, pass_value, status, archived_at, archived_by, created_at FROM exams ORDER BY created_at, id",
            reader => ReadExam(reader, subjectsByExam),
            cancellationToken);
        var registrationRows = await QueryAsync(connection,
            "SELECT id, user_id, exam_id, status, payment_status, paid_at, paid_by, created_at, reviewed_at, review_note, registration_number, number_rule_id, feature_score FROM registrations WHERE user_id = @userId ORDER BY created_at, id",
            ReadRegistrationRow,
            cancellationToken,
            new QueryParameter("@userId", userId));

        var registrationSubjects = await QueryAsync(connection,
            "SELECT registration_id, subject_id FROM registration_subjects ORDER BY registration_id, subject_id",
            reader => new RegistrationSubject(ReadString(reader, "registration_id"), ReadString(reader, "subject_id")),
            cancellationToken);
        var subjectIdsByRegistration = registrationSubjects
            .GroupBy(item => item.RegistrationId)
            .ToDictionary(group => group.Key, group => (IReadOnlyList<string>)group.Select(item => item.SubjectId).ToArray(), StringComparer.Ordinal);

        var assignments = await QueryAsync(connection,
            "SELECT registration_id, subject_id, room_id, room, room_code, exam_room_code, building, floor, seat, subject_signature FROM admit_card_subjects ORDER BY registration_id, subject_id",
            ReadAssignment,
            cancellationToken);
        var assignmentsByRegistration = assignments
            .GroupBy(item => item.RegistrationId)
            .ToDictionary(
                group => group.Key,
                group => (IReadOnlyList<CandidateAdmitAssignment>)group
                    .OrderBy(item => subjectPositions.GetValueOrDefault(item.Assignment.SubjectId))
                    .Select(item => item.Assignment)
                    .ToArray(),
                StringComparer.Ordinal);
        var cards = await QueryAsync(connection,
            "SELECT registration_id, plan_id, card_number, center_id, test_center, center_code, center_address, generated_at FROM admit_cards ORDER BY registration_id",
            reader => ReadCard(reader, assignmentsByRegistration),
            cancellationToken);
        var cardsByRegistration = cards
            .ToDictionary(item => item.RegistrationId, item => item.Card, StringComparer.Ordinal);

        var allRegistrations = registrationRows.Select(row => new CandidateRegistration(
            row.Id,
            row.UserId,
            row.ExamId,
            subjectIdsByRegistration.GetValueOrDefault(row.Id) ?? [],
            row.Status,
            row.PaymentStatus,
            row.PaidAt,
            row.PaidBy,
            row.CreatedAt,
            row.ReviewedAt,
            row.ReviewNote,
            row.RegistrationNumber,
            row.NumberRuleId,
            row.FeatureScore,
            cardsByRegistration.GetValueOrDefault(row.Id))).ToArray();
        var registrations = allRegistrations.Where(item => item.UserId == userId).ToArray();

        var results = await QueryAsync(connection,
            """
            SELECT result.id, result.registration_id, result.subject_id, result.score,
              COALESCE(statistic.grade, result.grade) AS grade, result.published,
              result.updated_at, result.published_at, statistic.subject_rank,
              statistic.cohort_size, statistic.rank_percent, statistic.qualified,
              statistic.pass_score, statistic.cutoff_rank
            FROM results result
            JOIN registrations registration ON registration.id = result.registration_id
            LEFT JOIN result_statistics statistic ON statistic.result_id = result.id
            WHERE registration.user_id = @userId ORDER BY result.id
            """,
            ReadResult,
            cancellationToken,
            new QueryParameter("@userId", userId));
        var resultSummaries = await QueryAsync(connection,
            """
            SELECT summary.registration_id, summary.published_subject_count, summary.subject_count,
              summary.complete, summary.total_score, summary.full_score, summary.score_ratio,
              summary.total_rank, summary.cohort_size, summary.rank_percent, summary.qualified,
              summary.cutoff_rank
            FROM registration_result_summaries summary
            JOIN registrations registration ON registration.id = summary.registration_id
            WHERE registration.user_id = @userId ORDER BY summary.registration_id
            """,
            ReadResultSummary,
            cancellationToken,
            new QueryParameter("@userId", userId));
        var workflowSteps = await QueryAsync(connection,
            "SELECT id, workflow_id, name, admin_level, position FROM workflow_steps ORDER BY workflow_id, position, id",
            reader => new WorkflowStepRow(
                ReadString(reader, "workflow_id"),
                new CandidateWorkflowStep(
                    ReadString(reader, "id"),
                    ReadString(reader, "name"),
                    ReadString(reader, "admin_level"),
                    ReadInt32(reader, "position"))),
            cancellationToken);
        var stepsByWorkflow = workflowSteps.GroupBy(item => item.WorkflowId)
            .ToDictionary(group => group.Key, group => (IReadOnlyList<CandidateWorkflowStep>)group.Select(item => item.Step).ToArray(), StringComparer.Ordinal);
        var workflows = await QueryAsync(connection,
            "SELECT id, business_type, name, active, updated_by, updated_at FROM workflow_definitions ORDER BY business_type, id",
            reader =>
            {
                var id = ReadString(reader, "id");
                return new CandidateWorkflow(
                    id,
                    ReadString(reader, "business_type"),
                    ReadString(reader, "name"),
                    ReadBoolean(reader, "active"),
                    ReadOptionalString(reader, "updated_by"),
                    ReadString(reader, "updated_at"),
                    stepsByWorkflow.GetValueOrDefault(id) ?? []);
            },
            cancellationToken);
        var instances = await QueryAsync(connection,
            "SELECT id, workflow_id, business_type, business_id, status, current_step, assignee_id, created_at, completed_at FROM workflow_instances ORDER BY created_at DESC, id",
            ReadWorkflowInstance,
            cancellationToken);
        var actions = await QueryAsync(connection,
            "SELECT id, instance_id, actor_id, action, note, from_assignee_id, to_assignee_id, created_at FROM workflow_actions ORDER BY created_at, id",
            ReadWorkflowAction,
            cancellationToken);
        var users = await QueryAsync(connection,
            "SELECT id, username, role, admin_level, school_id, class_id, display_name, candidate_number, active, must_change_password, totp_enabled, archived_at, created_at FROM users ORDER BY created_at, id",
            ReadUserSummary,
            cancellationToken);
        var centers = await QueryAsync(connection,
            "SELECT id, code, name, province_name, city_name, district_name, address FROM test_centers ORDER BY id",
            reader => new CandidateTestCenter(
                ReadString(reader, "id"),
                ReadString(reader, "code"),
                ReadString(reader, "name"),
                ReadString(reader, "province_name"),
                ReadString(reader, "city_name"),
                ReadString(reader, "district_name"),
                ReadString(reader, "address")),
            cancellationToken);
        var rooms = await QueryAsync(connection,
            "SELECT id, code, name, building, floor FROM test_rooms ORDER BY id",
            reader => new CandidateTestRoom(
                ReadString(reader, "id"),
                ReadString(reader, "code"),
                ReadString(reader, "name"),
                ReadString(reader, "building"),
                ReadOptionalString(reader, "floor") ?? string.Empty),
            cancellationToken);
        await using var organizationCommand = connection.CreateCommand();
        organizationCommand.CommandText = "SELECT name FROM organization WHERE id = 1";
        var organizationValue = await organizationCommand.ExecuteScalarAsync(cancellationToken);
        var organizationName = organizationValue is null or DBNull
            ? "考试服务平台"
            : Convert.ToString(organizationValue, CultureInfo.InvariantCulture) ?? "考试服务平台";

        return new CandidateReadSnapshot(
            exams,
            registrations,
            results,
            resultSummaries,
            workflows,
            instances,
            actions,
            users.ToDictionary(item => item.Id, StringComparer.Ordinal),
            centers.ToDictionary(item => item.Id, StringComparer.Ordinal),
            rooms.ToDictionary(item => item.Id, StringComparer.Ordinal),
            organizationName);
    }

    private static async Task<IReadOnlyList<T>> QueryAsync<T>(
        DbConnection connection,
        string sql,
        Func<DbDataReader, T> projector,
        CancellationToken cancellationToken,
        params QueryParameter[] parameters)
    {
        await using var command = connection.CreateCommand();
        command.CommandText = sql;
        foreach (var item in parameters)
        {
            var parameter = command.CreateParameter();
            parameter.ParameterName = item.Name;
            parameter.Value = item.Value ?? DBNull.Value;
            command.Parameters.Add(parameter);
        }

        var output = new List<T>();
        await using var reader = await command.ExecuteReaderAsync(cancellationToken);
        while (await reader.ReadAsync(cancellationToken))
        {
            output.Add(projector(reader));
        }

        return output;
    }

    private static SubjectRow ReadSubject(DbDataReader reader)
    {
        var fullScore = ReadDouble(reader, "full_score", 150);
        var rawRule = ReadOptionalString(reader, "pass_rule") ?? "fixed_score";
        var passRule = rawRule == "score_ratio" ? "rank_percent" : rawRule;
        var passValue = ReadNullableDouble(reader, "pass_value")
            ?? ReadNullableDouble(reader, "pass_score")
            ?? fullScore * 0.6;
        return new SubjectRow(
            ReadString(reader, "exam_id"),
            new CandidateSubject(
                ReadString(reader, "id"),
                ReadString(reader, "name"),
                ReadString(reader, "subject_date"),
                ReadString(reader, "start_time"),
                ReadString(reader, "end_time"),
                ReadDouble(reader, "fee"),
                fullScore,
                passRule,
                passValue,
                passRule == "fixed_score" ? Math.Round(passValue, 2) : null,
                ReadInt32(reader, "position")));
    }

    private static CandidateExam ReadExam(
        DbDataReader reader,
        IReadOnlyDictionary<string, IReadOnlyList<CandidateSubject>> subjectsByExam)
    {
        var id = ReadString(reader, "id");
        var rawPolicy = ReadOptionalString(reader, "pass_policy") ?? "rank_percent";
        return new CandidateExam(
            id,
            ReadString(reader, "code"),
            ReadString(reader, "name"),
            ReadString(reader, "description"),
            ReadString(reader, "registration_start"),
            ReadString(reader, "registration_end"),
            ReadString(reader, "exam_start"),
            ReadString(reader, "exam_end"),
            ReadString(reader, "admit_download_start"),
            ReadString(reader, "admit_download_end"),
            ReadString(reader, "location"),
            rawPolicy == "score_ratio" ? "rank_percent" : rawPolicy,
            ReadDouble(reader, "pass_value", 60),
            ReadString(reader, "status"),
            ReadOptionalString(reader, "archived_at"),
            ReadOptionalString(reader, "archived_by"),
            ReadString(reader, "created_at"),
            subjectsByExam.GetValueOrDefault(id) ?? []);
    }

    private static RegistrationRow ReadRegistrationRow(DbDataReader reader) => new(
        ReadString(reader, "id"),
        ReadString(reader, "user_id"),
        ReadString(reader, "exam_id"),
        ReadString(reader, "status"),
        ReadString(reader, "payment_status"),
        ReadOptionalString(reader, "paid_at"),
        ReadOptionalString(reader, "paid_by"),
        ReadString(reader, "created_at"),
        ReadOptionalString(reader, "reviewed_at"),
        ReadOptionalString(reader, "review_note") ?? string.Empty,
        ReadOptionalString(reader, "registration_number") ?? string.Empty,
        ReadOptionalString(reader, "number_rule_id"),
        ReadDouble(reader, "feature_score"));

    private static AssignmentRow ReadAssignment(DbDataReader reader) => new(
        ReadString(reader, "registration_id"),
        new CandidateAdmitAssignment(
            ReadString(reader, "subject_id"),
            ReadOptionalString(reader, "room_id"),
            ReadString(reader, "room"),
            ReadString(reader, "room_code"),
            ReadString(reader, "exam_room_code"),
            ReadOptionalString(reader, "building") ?? string.Empty,
            ReadOptionalString(reader, "floor") ?? string.Empty,
            ReadString(reader, "seat"),
            ReadOptionalString(reader, "subject_signature") ?? string.Empty));

    private static CardRow ReadCard(
        DbDataReader reader,
        IReadOnlyDictionary<string, IReadOnlyList<CandidateAdmitAssignment>> assignmentsByRegistration)
    {
        var registrationId = ReadString(reader, "registration_id");
        var assignments = assignmentsByRegistration.GetValueOrDefault(registrationId) ?? [];
        var primary = assignments.FirstOrDefault();
        return new CardRow(
            registrationId,
            new CandidateAdmitCard(
                ReadString(reader, "plan_id"),
                ReadString(reader, "card_number"),
                ReadOptionalString(reader, "center_id"),
                ReadString(reader, "test_center"),
                ReadOptionalString(reader, "center_code") ?? string.Empty,
                ReadOptionalString(reader, "center_address") ?? string.Empty,
                primary?.RoomName ?? string.Empty,
                primary?.Seat ?? string.Empty,
                assignments,
                ReadString(reader, "generated_at")));
    }

    private static CandidateResult ReadResult(DbDataReader reader) => new(
        ReadString(reader, "id"),
        ReadString(reader, "registration_id"),
        ReadString(reader, "subject_id"),
        ReadDouble(reader, "score"),
        ReadString(reader, "grade"),
        ReadBoolean(reader, "published"),
        ReadOptionalString(reader, "updated_at"),
        ReadOptionalString(reader, "published_at"),
        ReadNullableInt32(reader, "subject_rank"),
        ReadNullableInt32(reader, "cohort_size") ?? 0,
        ReadNullableDouble(reader, "rank_percent"),
        ReadNullableBoolean(reader, "qualified"),
        ReadNullableDouble(reader, "pass_score"),
        ReadNullableInt32(reader, "cutoff_rank"));

    private static CandidateResultSummary ReadResultSummary(DbDataReader reader) => new(
        ReadString(reader, "registration_id"),
        ReadInt32(reader, "published_subject_count"),
        ReadInt32(reader, "subject_count"),
        ReadBoolean(reader, "complete"),
        ReadDouble(reader, "total_score"),
        ReadDouble(reader, "full_score"),
        ReadDouble(reader, "score_ratio"),
        ReadNullableInt32(reader, "total_rank"),
        ReadInt32(reader, "cohort_size"),
        ReadNullableDouble(reader, "rank_percent"),
        ReadNullableBoolean(reader, "qualified"),
        ReadNullableInt32(reader, "cutoff_rank"));

    private static CandidateWorkflowInstance ReadWorkflowInstance(DbDataReader reader) => new(
        ReadString(reader, "id"),
        ReadString(reader, "workflow_id"),
        ReadString(reader, "business_type"),
        ReadString(reader, "business_id"),
        ReadString(reader, "status"),
        ReadInt32(reader, "current_step"),
        ReadOptionalString(reader, "assignee_id"),
        ReadString(reader, "created_at"),
        ReadOptionalString(reader, "completed_at"));

    private static CandidateWorkflowAction ReadWorkflowAction(DbDataReader reader) => new(
        ReadString(reader, "id"),
        ReadString(reader, "instance_id"),
        ReadOptionalString(reader, "actor_id"),
        ReadString(reader, "action"),
        ReadOptionalString(reader, "note") ?? string.Empty,
        ReadOptionalString(reader, "from_assignee_id"),
        ReadOptionalString(reader, "to_assignee_id"),
        ReadString(reader, "created_at"));

    private static CandidateUserSummary ReadUserSummary(DbDataReader reader)
    {
        var role = ReadString(reader, "role");
        return new CandidateUserSummary(
            ReadString(reader, "id"),
            ReadString(reader, "username"),
            role,
            ReadOptionalString(reader, "admin_level") ?? (role == "admin" ? "super" : null),
            ReadOptionalString(reader, "school_id"),
            ReadOptionalString(reader, "class_id"),
            ReadString(reader, "display_name"),
            ReadOptionalString(reader, "candidate_number"),
            ReadBoolean(reader, "active"),
            ReadBoolean(reader, "must_change_password"),
            ReadBoolean(reader, "totp_enabled"),
            ReadOptionalString(reader, "archived_at"),
            ReadString(reader, "created_at"));
    }

    private static string ReadString(DbDataReader reader, string name) =>
        Convert.ToString(reader.GetValue(reader.GetOrdinal(name)), CultureInfo.InvariantCulture) ?? string.Empty;

    private static string? ReadOptionalString(DbDataReader reader, string name)
    {
        var ordinal = reader.GetOrdinal(name);
        return reader.IsDBNull(ordinal) ? null : Convert.ToString(reader.GetValue(ordinal), CultureInfo.InvariantCulture);
    }

    private static bool ReadBoolean(DbDataReader reader, string name)
    {
        var value = reader.GetValue(reader.GetOrdinal(name));
        return value is bool boolean ? boolean : Convert.ToInt64(value, CultureInfo.InvariantCulture) != 0;
    }

    private static int ReadInt32(DbDataReader reader, string name) =>
        Convert.ToInt32(reader.GetValue(reader.GetOrdinal(name)), CultureInfo.InvariantCulture);

    private static int? ReadNullableInt32(DbDataReader reader, string name)
    {
        var ordinal = reader.GetOrdinal(name);
        return reader.IsDBNull(ordinal) ? null : Convert.ToInt32(reader.GetValue(ordinal), CultureInfo.InvariantCulture);
    }

    private static bool? ReadNullableBoolean(DbDataReader reader, string name)
    {
        var ordinal = reader.GetOrdinal(name);
        return reader.IsDBNull(ordinal)
            ? null
            : Convert.ToInt64(reader.GetValue(ordinal), CultureInfo.InvariantCulture) != 0;
    }

    private static double ReadDouble(DbDataReader reader, string name, double fallback = 0)
    {
        var ordinal = reader.GetOrdinal(name);
        return reader.IsDBNull(ordinal) ? fallback : Convert.ToDouble(reader.GetValue(ordinal), CultureInfo.InvariantCulture);
    }

    private static double? ReadNullableDouble(DbDataReader reader, string name)
    {
        var ordinal = reader.GetOrdinal(name);
        return reader.IsDBNull(ordinal) ? null : Convert.ToDouble(reader.GetValue(ordinal), CultureInfo.InvariantCulture);
    }

    private sealed record QueryParameter(string Name, object? Value);

    private sealed record SubjectRow(string ExamId, CandidateSubject Subject);

    private sealed record RegistrationRow(
        string Id,
        string UserId,
        string ExamId,
        string Status,
        string PaymentStatus,
        string? PaidAt,
        string? PaidBy,
        string CreatedAt,
        string? ReviewedAt,
        string ReviewNote,
        string RegistrationNumber,
        string? NumberRuleId,
        double FeatureScore);

    private sealed record RegistrationSubject(string RegistrationId, string SubjectId);

    private sealed record AssignmentRow(string RegistrationId, CandidateAdmitAssignment Assignment);

    private sealed record CardRow(string RegistrationId, CandidateAdmitCard Card);

    private sealed record WorkflowStepRow(string WorkflowId, CandidateWorkflowStep Step);
}
