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

namespace Eis.Infrastructure.Administration;

internal sealed record AdminReadSnapshot(
    IReadOnlyList<AdminSchool> Schools,
    IReadOnlyList<AdminClass> Classes,
    IReadOnlyList<AdminUser> Users,
    IReadOnlyList<AdminProfile> Profiles,
    IReadOnlyList<AdminRegistration> Registrations,
    IReadOnlyList<AdminExam> Exams,
    IReadOnlyList<AdminWorkflowInstance> WorkflowInstances,
    IReadOnlyList<AdminScopeBusiness> CenterChanges,
    IReadOnlyList<AdminScopeBusiness> AccountBatches,
    IReadOnlyList<AdminResultScope> Results,
    IReadOnlyList<AdminAuditLog> AuditLogs,
    IReadOnlyList<string> CenterSchoolIds,
    int PublishedNoticeCount,
    bool SelfRegistrationEnabled);

internal sealed record AdminSchool(
    string Id,
    string Name,
    string Code,
    string Address,
    bool IsSourceSchool,
    bool IsAdmissionSchool,
    bool Active);

internal sealed record AdminClass(string Id, string SchoolId, string Name, string Grade, bool Active);

internal sealed record AdminUser(
    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 record AdminProfile(string Id, string UserId, string? SchoolId, string? ClassId, string Status);

internal sealed record AdminRegistration(
    string Id,
    string UserId,
    string ExamId,
    string Status,
    string PaymentStatus);

internal sealed record AdminExam(
    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<AdminSubject> Subjects);

internal sealed record AdminSubject(
    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 AdminWorkflowInstance(
    string Id,
    string BusinessType,
    string BusinessId,
    string Status,
    string? AssigneeId);

internal sealed record AdminScopeBusiness(string Id, string SchoolId);

internal sealed record AdminResultScope(string Id, string RegistrationId);

internal sealed record AdminAuditLog(string Id, string? ActorId, string Action, string Detail, string CreatedAt);

internal sealed class AdminReadSnapshotLoader(IRelationalConnectionFactory connectionFactory)
{
    public async Task<AdminReadSnapshot> LoadAsync(CancellationToken cancellationToken)
    {
        await using var connection = await connectionFactory.OpenAsync(cancellationToken);
        var schools = await QueryAsync(connection,
            "SELECT id, name, code, address, is_source_school, is_admission_school, active FROM schools ORDER BY name, id",
            reader => new AdminSchool(
                Text(reader, "id"), Text(reader, "name"), Text(reader, "code"), Optional(reader, "address") ?? string.Empty,
                Boolean(reader, "is_source_school"), Boolean(reader, "is_admission_school"), Boolean(reader, "active")),
            cancellationToken);
        var classes = await QueryAsync(connection,
            "SELECT id, school_id, name, grade, active FROM school_classes ORDER BY school_id, grade, name, id",
            reader => new AdminClass(Text(reader, "id"), Text(reader, "school_id"), Text(reader, "name"), Text(reader, "grade"), Boolean(reader, "active")),
            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",
            reader => new AdminUser(
                Text(reader, "id"), Text(reader, "username"), Text(reader, "role"), Optional(reader, "admin_level"),
                Optional(reader, "school_id"), Optional(reader, "class_id"), Text(reader, "display_name"), Optional(reader, "candidate_number"),
                Boolean(reader, "active"), Boolean(reader, "must_change_password"), Boolean(reader, "totp_enabled"),
                Optional(reader, "archived_at"), Text(reader, "created_at")),
            cancellationToken);
        var profiles = await QueryAsync(connection,
            "SELECT id, user_id, school_id, class_id, status FROM candidate_profiles ORDER BY updated_at, id",
            reader => new AdminProfile(Text(reader, "id"), Text(reader, "user_id"), Optional(reader, "school_id"), Optional(reader, "class_id"), Text(reader, "status")),
            cancellationToken);
        var registrations = await QueryAsync(connection,
            "SELECT id, user_id, exam_id, status, payment_status FROM registrations ORDER BY created_at, id",
            reader => new AdminRegistration(Text(reader, "id"), Text(reader, "user_id"), Text(reader, "exam_id"), Text(reader, "status"), Text(reader, "payment_status")),
            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<AdminSubject>)group.Select(item => item.Subject).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 instances = await QueryAsync(connection,
            "SELECT id, business_type, business_id, status, assignee_id FROM workflow_instances ORDER BY created_at DESC, id",
            reader => new AdminWorkflowInstance(
                Text(reader, "id"), Text(reader, "business_type"), Text(reader, "business_id"), Text(reader, "status"), Optional(reader, "assignee_id")),
            cancellationToken);
        var centerChanges = await QueryAsync(connection,
            "SELECT id, school_id FROM center_change_requests ORDER BY created_at DESC, id",
            reader => new AdminScopeBusiness(Text(reader, "id"), Text(reader, "school_id")),
            cancellationToken);
        var batches = await QueryAsync(connection,
            "SELECT id, school_id FROM candidate_account_batches ORDER BY created_at DESC, id",
            reader => new AdminScopeBusiness(Text(reader, "id"), Text(reader, "school_id")),
            cancellationToken);
        var results = await QueryAsync(connection,
            "SELECT id, registration_id FROM results ORDER BY id",
            reader => new AdminResultScope(Text(reader, "id"), Text(reader, "registration_id")),
            cancellationToken);
        var logs = await QueryAsync(connection,
            "SELECT id, actor_id, action, detail, created_at FROM audit_logs ORDER BY created_at DESC, id DESC",
            reader => new AdminAuditLog(Text(reader, "id"), Optional(reader, "actor_id"), Text(reader, "action"), Text(reader, "detail"), Text(reader, "created_at")),
            cancellationToken);

        var centerSchoolIds = await QueryAsync(connection,
            "SELECT school_id FROM test_centers ORDER BY school_id, name, id",
            reader => Text(reader, "school_id"),
            cancellationToken);
        var noticeCount = await ScalarIntAsync(connection, "SELECT COUNT(*) FROM notices WHERE status = 'published'", cancellationToken);
        var selfRegistration = await ScalarIntAsync(connection, "SELECT self_registration_enabled FROM schema_metadata WHERE id = 1", cancellationToken) != 0;
        return new AdminReadSnapshot(
            schools, classes, users, profiles, registrations, exams, instances, centerChanges, batches, results, logs,
            centerSchoolIds, noticeCount, selfRegistration);
    }

    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;
        return new SubjectRow(
            Text(reader, "exam_id"),
            new AdminSubject(
                Text(reader, "id"), Text(reader, "name"), Text(reader, "subject_date"), Text(reader, "start_time"), Text(reader, "end_time"),
                Number(reader, "fee"), fullScore, passRule, passValue,
                passRule == "fixed_score" ? Math.Round(passValue, 2, MidpointRounding.AwayFromZero) : null,
                Integer(reader, "position")));
    }

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

    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 async Task<int> ScalarIntAsync(DbConnection connection, string sql, CancellationToken cancellationToken)
    {
        await using var command = connection.CreateCommand();
        command.CommandText = sql;
        return Convert.ToInt32(await command.ExecuteScalarAsync(cancellationToken), CultureInfo.InvariantCulture);
    }

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

    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 ExamId, AdminSubject Subject);
}
