using System.Data.Common;
using System.Globalization;
using System.Text.Json.Nodes;
using Eis.Infrastructure.Data;

namespace Eis.Infrastructure.Administration;

internal sealed record AdminOperationalSnapshot(
    IReadOnlyList<OperationalUser> Users,
    IReadOnlyList<OperationalProfile> Profiles,
    IReadOnlyList<OperationalExam> Exams,
    IReadOnlyList<OperationalRegistration> Registrations);

internal sealed record OperationalUser(
    string Id,
    string Username,
    string CandidateNumber,
    bool MustChangePassword,
    string? ArchivedAt,
    string? ArchivedBy,
    string DisplayName);

internal sealed record OperationalProfile(
    string Id,
    string UserId,
    string? SchoolId,
    string? ClassId,
    JsonObject Data);

internal sealed record OperationalExam(string Id, JsonObject Data, IReadOnlyList<JsonObject> Subjects);

internal sealed record OperationalRegistration(
    string Id,
    string UserId,
    string ExamId,
    string Status,
    string PaymentStatus,
    string? PaidBy,
    string CreatedAt,
    JsonObject Data);

internal sealed class AdminOperationalSnapshotLoader(IRelationalConnectionFactory connectionFactory)
{
    public async Task<AdminOperationalSnapshot> LoadAsync(CancellationToken cancellationToken)
    {
        await using var connection = await connectionFactory.OpenAsync(cancellationToken);
        var users = await QueryAsync(connection,
            "SELECT id, username, candidate_number, must_change_password, archived_at, archived_by, display_name FROM users ORDER BY created_at, id",
            reader => new OperationalUser(
                Text(reader, "id"), Text(reader, "username"), Optional(reader, "candidate_number") ?? "",
                Boolean(reader, "must_change_password"), Optional(reader, "archived_at"), Optional(reader, "archived_by"), Text(reader, "display_name")),
            cancellationToken);
        var profiles = await QueryAsync(connection,
            """
            SELECT id, user_id, name, gender, id_number, phone, email, school, grade, school_id, class_id,
              province_code, province_name, city_code, city_name, district_code, district_name, address,
              emergency_contact, emergency_phone, native_place, birth_date, ethnicity, postal_code,
              guardian_name, guardian_phone, specialty_category, specialty_type, specialty_types,
              specialty_certificate, policy_eligibility, profile_completed, status, review_note,
              reviewed_at, reviewer_id, updated_at
            FROM candidate_profiles ORDER BY updated_at, id
            """,
            ReadProfile,
            cancellationToken);
        var subjectRows = 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 = subjectRows.GroupBy(item => item.ExamId)
            .ToDictionary(group => group.Key, group => (IReadOnlyList<JsonObject>)group.Select(item => item.Data).ToArray(), 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 registrationSubjects = await QueryAsync(connection,
            "SELECT registration_id, subject_id FROM registration_subjects ORDER BY registration_id, subject_id",
            reader => new RegistrationSubject(Text(reader, "registration_id"), Text(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 subjectOrder = subjectRows.ToDictionary(item => item.Id, item => item.Order, 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<JsonObject>)group.OrderBy(item => subjectOrder.GetValueOrDefault(item.SubjectId)).Select(item => item.Data).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.Data, StringComparer.Ordinal);
        var registrations = 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 ORDER BY created_at, id
            """,
            reader => ReadRegistration(reader, subjectIdsByRegistration, cardsByRegistration),
            cancellationToken);
        return new(users, profiles, exams, registrations);
    }

    private static OperationalProfile ReadProfile(DbDataReader reader)
    {
        var id = Text(reader, "id");
        var userId = Text(reader, "user_id");
        var schoolId = Optional(reader, "school_id");
        var classId = Optional(reader, "class_id");
        return new(id, userId, schoolId, classId, new JsonObject
        {
            ["id"] = id,
            ["userId"] = userId,
            ["name"] = Text(reader, "name"),
            ["gender"] = Optional(reader, "gender") ?? "",
            ["idNumber"] = Text(reader, "id_number"),
            ["phone"] = Text(reader, "phone"),
            ["email"] = Optional(reader, "email") ?? "",
            ["school"] = Optional(reader, "school") ?? "",
            ["grade"] = Optional(reader, "grade") ?? "",
            ["schoolId"] = JsonValue.Create(schoolId),
            ["classId"] = JsonValue.Create(classId),
            ["provinceCode"] = Optional(reader, "province_code") ?? "",
            ["provinceName"] = Optional(reader, "province_name") ?? "",
            ["cityCode"] = Optional(reader, "city_code") ?? "",
            ["cityName"] = Optional(reader, "city_name") ?? "",
            ["districtCode"] = Optional(reader, "district_code") ?? "",
            ["districtName"] = Optional(reader, "district_name") ?? "",
            ["address"] = Optional(reader, "address") ?? "",
            ["emergencyContact"] = Optional(reader, "emergency_contact") ?? "",
            ["emergencyPhone"] = Optional(reader, "emergency_phone") ?? "",
            ["nativePlace"] = Optional(reader, "native_place") ?? "",
            ["birthDate"] = Optional(reader, "birth_date") ?? "",
            ["ethnicity"] = Optional(reader, "ethnicity") ?? "",
            ["postalCode"] = Optional(reader, "postal_code") ?? "",
            ["guardianName"] = Optional(reader, "guardian_name") ?? "",
            ["guardianPhone"] = Optional(reader, "guardian_phone") ?? "",
            ["specialtyCategory"] = Optional(reader, "specialty_category") ?? "",
            ["specialtyType"] = Optional(reader, "specialty_type") ?? "",
            ["specialtyTypes"] = ParseArray(Optional(reader, "specialty_types")),
            ["specialtyCertificate"] = Optional(reader, "specialty_certificate") ?? "",
            ["policyEligibility"] = Optional(reader, "policy_eligibility") ?? "",
            ["profileCompleted"] = Boolean(reader, "profile_completed"),
            ["status"] = Text(reader, "status"),
            ["reviewNote"] = Optional(reader, "review_note") ?? "",
            ["reviewedAt"] = JsonValue.Create(Optional(reader, "reviewed_at")),
            ["reviewerId"] = JsonValue.Create(Optional(reader, "reviewer_id")),
            ["updatedAt"] = Text(reader, "updated_at")
        });
    }

    private static SubjectRow ReadSubject(DbDataReader reader)
    {
        var fullScore = Number(reader, "full_score", 150);
        var rawRule = Optional(reader, "pass_rule") ?? "fixed_score";
        var passRule = rawRule == "score_ratio" ? "rank_percent" : rawRule;
        var passValue = NullableNumber(reader, "pass_value") ?? NullableNumber(reader, "pass_score") ?? fullScore * 0.6;
        var order = Integer(reader, "position");
        return new(Text(reader, "id"), Text(reader, "exam_id"), order, new JsonObject
        {
            ["id"] = Text(reader, "id"),
            ["name"] = Text(reader, "name"),
            ["date"] = Text(reader, "subject_date"),
            ["start"] = Text(reader, "start_time"),
            ["end"] = Text(reader, "end_time"),
            ["fee"] = Number(reader, "fee"),
            ["fullScore"] = fullScore,
            ["passRule"] = passRule,
            ["passValue"] = passValue,
            ["passScore"] = passRule == "fixed_score" ? Math.Round(passValue, 2, MidpointRounding.AwayFromZero) : null,
            ["order"] = order
        });
    }

    private static OperationalExam ReadExam(DbDataReader reader, IReadOnlyDictionary<string, IReadOnlyList<JsonObject>> subjectsByExam)
    {
        var id = Text(reader, "id");
        var subjects = subjectsByExam.GetValueOrDefault(id) ?? [];
        var rawPolicy = Optional(reader, "pass_policy") ?? "rank_percent";
        var data = new JsonObject
        {
            ["id"] = id,
            ["code"] = Text(reader, "code"),
            ["name"] = Text(reader, "name"),
            ["description"] = Text(reader, "description"),
            ["registrationStart"] = Text(reader, "registration_start"),
            ["registrationEnd"] = Text(reader, "registration_end"),
            ["examStart"] = Text(reader, "exam_start"),
            ["examEnd"] = Text(reader, "exam_end"),
            ["admitDownloadStart"] = Text(reader, "admit_download_start"),
            ["admitDownloadEnd"] = Text(reader, "admit_download_end"),
            ["location"] = Text(reader, "location"),
            ["passPolicy"] = rawPolicy == "score_ratio" ? "rank_percent" : rawPolicy,
            ["passValue"] = Number(reader, "pass_value", 60),
            ["status"] = Text(reader, "status"),
            ["archivedAt"] = JsonValue.Create(Optional(reader, "archived_at")),
            ["archivedBy"] = JsonValue.Create(Optional(reader, "archived_by")),
            ["createdAt"] = Text(reader, "created_at"),
            ["subjects"] = new JsonArray(subjects.Select(item => item.DeepClone()).ToArray())
        };
        return new(id, data, subjects);
    }

    private static AssignmentRow ReadAssignment(DbDataReader reader)
    {
        var subjectId = Text(reader, "subject_id");
        var room = Text(reader, "room");
        return new(Text(reader, "registration_id"), subjectId, new JsonObject
        {
            ["subjectId"] = subjectId,
            ["roomId"] = JsonValue.Create(Optional(reader, "room_id")),
            ["roomName"] = room,
            ["room"] = room,
            ["roomCode"] = Text(reader, "room_code"),
            ["examRoomCode"] = Text(reader, "exam_room_code"),
            ["building"] = Optional(reader, "building") ?? "",
            ["floor"] = Optional(reader, "floor") ?? "",
            ["seat"] = Text(reader, "seat"),
            ["subjectSignature"] = Optional(reader, "subject_signature") ?? ""
        });
    }

    private static CardRow ReadCard(DbDataReader reader, IReadOnlyDictionary<string, IReadOnlyList<JsonObject>> assignmentsByRegistration)
    {
        var registrationId = Text(reader, "registration_id");
        var assignments = assignmentsByRegistration.GetValueOrDefault(registrationId) ?? [];
        var primary = assignments.FirstOrDefault();
        return new(registrationId, new JsonObject
        {
            ["planId"] = JsonValue.Create(Optional(reader, "plan_id")),
            ["number"] = Text(reader, "card_number"),
            ["centerId"] = JsonValue.Create(Optional(reader, "center_id")),
            ["testCenter"] = Text(reader, "test_center"),
            ["centerCode"] = Optional(reader, "center_code") ?? "",
            ["centerAddress"] = Optional(reader, "center_address") ?? "",
            ["room"] = primary?["roomName"]?.GetValue<string>() ?? "",
            ["seat"] = primary?["seat"]?.GetValue<string>() ?? "",
            ["assignments"] = new JsonArray(assignments.Select(item => item.DeepClone()).ToArray()),
            ["generatedAt"] = Text(reader, "generated_at")
        });
    }

    private static OperationalRegistration ReadRegistration(
        DbDataReader reader,
        IReadOnlyDictionary<string, IReadOnlyList<string>> subjectIdsByRegistration,
        IReadOnlyDictionary<string, JsonObject> cardsByRegistration)
    {
        var id = Text(reader, "id");
        var userId = Text(reader, "user_id");
        var examId = Text(reader, "exam_id");
        var status = Text(reader, "status");
        var paymentStatus = Text(reader, "payment_status");
        var paidBy = Optional(reader, "paid_by");
        var createdAt = Text(reader, "created_at");
        return new(id, userId, examId, status, paymentStatus, paidBy, createdAt, new JsonObject
        {
            ["id"] = id,
            ["userId"] = userId,
            ["examId"] = examId,
            ["subjectIds"] = new JsonArray((subjectIdsByRegistration.GetValueOrDefault(id) ?? []).Select(subjectId => JsonValue.Create(subjectId)).ToArray()),
            ["status"] = status,
            ["paymentStatus"] = paymentStatus,
            ["paidAt"] = JsonValue.Create(Optional(reader, "paid_at")),
            ["paidBy"] = JsonValue.Create(paidBy),
            ["createdAt"] = createdAt,
            ["reviewedAt"] = JsonValue.Create(Optional(reader, "reviewed_at")),
            ["reviewNote"] = Optional(reader, "review_note") ?? "",
            ["registrationNumber"] = Optional(reader, "registration_number") ?? "",
            ["numberRuleId"] = JsonValue.Create(Optional(reader, "number_rule_id")),
            ["featureScore"] = Number(reader, "feature_score"),
            ["admitCard"] = cardsByRegistration.TryGetValue(id, out var card) ? card.DeepClone() : null
        });
    }

    private static async Task<IReadOnlyList<T>> QueryAsync<T>(DbConnection connection, string sql, Func<DbDataReader, T> map, CancellationToken cancellationToken)
    {
        await using var command = connection.CreateCommand();
        command.CommandText = sql;
        await using var reader = await command.ExecuteReaderAsync(cancellationToken);
        var output = new List<T>();
        while (await reader.ReadAsync(cancellationToken)) output.Add(map(reader));
        return output;
    }

    private static JsonArray ParseArray(string? value)
    {
        try { return JsonNode.Parse(value ?? "[]") as JsonArray ?? []; }
        catch { return []; }
    }

    private static string Text(DbDataReader reader, string name) => Convert.ToString(reader.GetValue(reader.GetOrdinal(name)), CultureInfo.InvariantCulture) ?? "";
    private static string? Optional(DbDataReader reader, string name) { var ordinal = reader.GetOrdinal(name); return reader.IsDBNull(ordinal) ? null : Convert.ToString(reader.GetValue(ordinal), CultureInfo.InvariantCulture); }
    private static bool Boolean(DbDataReader reader, string name) => Convert.ToInt64(reader.GetValue(reader.GetOrdinal(name)), CultureInfo.InvariantCulture) != 0;
    private static int Integer(DbDataReader reader, string name) => Convert.ToInt32(reader.GetValue(reader.GetOrdinal(name)), CultureInfo.InvariantCulture);
    private static double Number(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? NullableNumber(DbDataReader reader, string name) { var ordinal = reader.GetOrdinal(name); return reader.IsDBNull(ordinal) ? null : Convert.ToDouble(reader.GetValue(ordinal), CultureInfo.InvariantCulture); }
    private sealed record SubjectRow(string Id, string ExamId, int Order, JsonObject Data);
    private sealed record RegistrationSubject(string RegistrationId, string SubjectId);
    private sealed record AssignmentRow(string RegistrationId, string SubjectId, JsonObject Data);
    private sealed record CardRow(string RegistrationId, JsonObject Data);
}
