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

namespace Eis.Infrastructure.Candidate;

internal sealed record SchoolClassSelection(
    string SchoolId,
    string SchoolName,
    string ClassId,
    string ClassName);

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

internal sealed class CandidateWriteRepository(IRelationalConnectionFactory connectionFactory)
{
    public async Task<SchoolClassSelection?> FindSchoolClassAsync(
        string schoolId,
        string classId,
        CancellationToken cancellationToken)
    {
        const string sql = """
            SELECT school.id AS school_id, school.name AS school_name,
              class.id AS class_id, class.name AS class_name
            FROM schools school
            JOIN school_classes class ON class.school_id = school.id
            WHERE school.id = @schoolId AND class.id = @classId
              AND school.active = 1 AND school.is_source_school = 1 AND class.active = 1
            LIMIT 1
            """;
        await using var connection = await connectionFactory.OpenAsync(cancellationToken);
        await using var command = CreateCommand(connection, sql,
        [
            new("@schoolId", schoolId),
            new("@classId", classId)
        ]);
        await using var reader = await command.ExecuteReaderAsync(cancellationToken);
        return await reader.ReadAsync(cancellationToken)
            ? new SchoolClassSelection(
                ReadString(reader, "school_id"),
                ReadString(reader, "school_name"),
                ReadString(reader, "class_id"),
                ReadString(reader, "class_name"))
            : null;
    }

    public async Task<bool> IdNumberExistsAsync(
        string idNumber,
        string excludedProfileId,
        CancellationToken cancellationToken)
    {
        await using var connection = await connectionFactory.OpenAsync(cancellationToken);
        await using var command = CreateCommand(connection,
            "SELECT COUNT(*) FROM candidate_profiles WHERE id <> @profileId AND id_number = @idNumber",
            [new("@profileId", excludedProfileId), new("@idNumber", idNumber)]);
        return Convert.ToInt64(await command.ExecuteScalarAsync(cancellationToken), CultureInfo.InvariantCulture) > 0;
    }

    public async Task<bool> RegistrationExistsAsync(
        string userId,
        string examId,
        CancellationToken cancellationToken)
    {
        await using var connection = await connectionFactory.OpenAsync(cancellationToken);
        await using var command = CreateCommand(connection,
            "SELECT COUNT(*) FROM registrations WHERE user_id = @userId AND exam_id = @examId",
            [new("@userId", userId), new("@examId", examId)]);
        return Convert.ToInt64(await command.ExecuteScalarAsync(cancellationToken), CultureInfo.InvariantCulture) > 0;
    }

    public async Task<string?> GetActiveNumberRuleIdAsync(CancellationToken cancellationToken)
    {
        await using var connection = await connectionFactory.OpenAsync(cancellationToken);
        await using var command = CreateCommand(connection,
            "SELECT id FROM number_rules WHERE active = 1 ORDER BY updated_at DESC, id LIMIT 1",
            []);
        var value = await command.ExecuteScalarAsync(cancellationToken);
        return value is null or DBNull ? null : Convert.ToString(value, CultureInfo.InvariantCulture);
    }

    public async Task UpdateProfileAsync(
        JsonObject profile,
        string displayName,
        CandidateWorkflowInstance? instance,
        CandidateWorkflowAction? action,
        CancellationToken cancellationToken)
    {
        const string sql = """
            UPDATE candidate_profiles SET
              name = @name, gender = @gender, id_number = @idNumber, phone = @phone, email = @email,
              school = @school, grade = @grade, province_code = @provinceCode, province_name = @provinceName,
              city_code = @cityCode, city_name = @cityName, district_code = @districtCode,
              district_name = @districtName, address = @address, school_id = @schoolId, class_id = @classId,
              emergency_contact = @emergencyContact, emergency_phone = @emergencyPhone, status = @status,
              review_note = @reviewNote, native_place = @nativePlace, birth_date = @birthDate,
              ethnicity = @ethnicity, postal_code = @postalCode, guardian_name = @guardianName,
              guardian_phone = @guardianPhone, specialty_category = @specialtyCategory,
              specialty_type = @specialtyType, specialty_types = @specialtyTypes,
              specialty_certificate = @specialtyCertificate, policy_eligibility = @policyEligibility,
              profile_completed = @profileCompleted, reviewed_at = @reviewedAt, reviewer_id = @reviewerId,
              updated_at = @updatedAt
            WHERE id = @id
            """;
        var parameters = new QueryParameter[]
        {
            new("@name", Text(profile, "name")),
            new("@gender", Optional(Text(profile, "gender"))),
            new("@idNumber", Text(profile, "idNumber")),
            new("@phone", Text(profile, "phone")),
            new("@email", Optional(Text(profile, "email"))),
            new("@school", Optional(Text(profile, "school"))),
            new("@grade", Optional(Text(profile, "grade"))),
            new("@provinceCode", Optional(Text(profile, "provinceCode"))),
            new("@provinceName", Optional(Text(profile, "provinceName"))),
            new("@cityCode", Optional(Text(profile, "cityCode"))),
            new("@cityName", Optional(Text(profile, "cityName"))),
            new("@districtCode", Optional(Text(profile, "districtCode"))),
            new("@districtName", Optional(Text(profile, "districtName"))),
            new("@address", Optional(Text(profile, "address"))),
            new("@schoolId", Optional(Text(profile, "schoolId"))),
            new("@classId", Optional(Text(profile, "classId"))),
            new("@emergencyContact", Optional(Text(profile, "emergencyContact"))),
            new("@emergencyPhone", Optional(Text(profile, "emergencyPhone"))),
            new("@status", Text(profile, "status")),
            new("@reviewNote", Optional(Text(profile, "reviewNote"))),
            new("@nativePlace", Optional(Text(profile, "nativePlace"))),
            new("@birthDate", Optional(Text(profile, "birthDate"))),
            new("@ethnicity", Optional(Text(profile, "ethnicity"))),
            new("@postalCode", Optional(Text(profile, "postalCode"))),
            new("@guardianName", Optional(Text(profile, "guardianName"))),
            new("@guardianPhone", Optional(Text(profile, "guardianPhone"))),
            new("@specialtyCategory", Optional(Text(profile, "specialtyCategory"))),
            new("@specialtyType", Optional(Text(profile, "specialtyType"))),
            new("@specialtyTypes", profile["specialtyTypes"]?.ToJsonString() ?? "[]"),
            new("@specialtyCertificate", Optional(Text(profile, "specialtyCertificate"))),
            new("@policyEligibility", Optional(Text(profile, "policyEligibility"))),
            new("@profileCompleted", Boolean(profile, "profileCompleted") ? 1 : 0),
            new("@reviewedAt", Optional(Text(profile, "reviewedAt"))),
            new("@reviewerId", Optional(Text(profile, "reviewerId"))),
            new("@updatedAt", Text(profile, "updatedAt")),
            new("@id", Text(profile, "id"))
        };

        await using var connection = await connectionFactory.OpenAsync(cancellationToken);
        await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
        try
        {
            await ExecuteAsync(connection, transaction, sql, parameters, cancellationToken);
            await ExecuteAsync(connection, transaction,
                "UPDATE users SET display_name = @displayName WHERE id = @userId",
                [new("@displayName", displayName), new("@userId", Text(profile, "userId"))],
                cancellationToken);
            if (instance is not null && action is not null)
            {
                await InsertWorkflowAsync(connection, transaction, instance, action, cancellationToken);
            }

            await transaction.CommitAsync(cancellationToken);
        }
        catch
        {
            await transaction.RollbackAsync(cancellationToken);
            throw;
        }
    }

    public async Task CreateRegistrationAsync(
        CandidateRegistration registration,
        CandidateWorkflowInstance? instance,
        CandidateWorkflowAction? action,
        CancellationToken cancellationToken)
    {
        const string sql = """
            INSERT INTO registrations (
              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
            ) VALUES (
              @id, @userId, @examId, @status, @paymentStatus, @paidAt, @paidBy, @createdAt,
              @reviewedAt, @reviewNote, @registrationNumber, @numberRuleId, @featureScore
            )
            """;
        await using var connection = await connectionFactory.OpenAsync(cancellationToken);
        await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
        try
        {
            await ExecuteAsync(connection, transaction, sql,
            [
                new("@id", registration.Id),
                new("@userId", registration.UserId),
                new("@examId", registration.ExamId),
                new("@status", registration.Status),
                new("@paymentStatus", registration.PaymentStatus),
                new("@paidAt", registration.PaidAt),
                new("@paidBy", registration.PaidBy),
                new("@createdAt", registration.CreatedAt),
                new("@reviewedAt", registration.ReviewedAt),
                new("@reviewNote", Optional(registration.ReviewNote)),
                new("@registrationNumber", Optional(registration.RegistrationNumber)),
                new("@numberRuleId", registration.NumberRuleId),
                new("@featureScore", registration.FeatureScore)
            ], cancellationToken);
            foreach (var subjectId in registration.SubjectIds)
            {
                await ExecuteAsync(connection, transaction,
                    "INSERT INTO registration_subjects (registration_id, subject_id) VALUES (@registrationId, @subjectId)",
                    [new("@registrationId", registration.Id), new("@subjectId", subjectId)],
                    cancellationToken);
            }

            if (instance is not null && action is not null)
            {
                await InsertWorkflowAsync(connection, transaction, instance, action, cancellationToken);
            }

            await transaction.CommitAsync(cancellationToken);
        }
        catch
        {
            await transaction.RollbackAsync(cancellationToken);
            throw;
        }
    }

    public async Task CreateWorkflowAsync(
        CandidateWorkflowInstance instance,
        CandidateWorkflowAction action,
        CandidateAuditLog log,
        CancellationToken cancellationToken)
    {
        await using var connection = await connectionFactory.OpenAsync(cancellationToken);
        await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
        try
        {
            await InsertWorkflowAsync(connection, transaction, instance, action, cancellationToken);
            await ExecuteAsync(connection, transaction,
                "INSERT INTO audit_logs (id, actor_id, action, detail, created_at) VALUES (@id, @actorId, @action, @detail, @createdAt)",
                [
                    new("@id", log.Id),
                    new("@actorId", log.ActorId),
                    new("@action", log.Action),
                    new("@detail", log.Detail),
                    new("@createdAt", log.CreatedAt)
                ],
                cancellationToken);
            await transaction.CommitAsync(cancellationToken);
        }
        catch
        {
            await transaction.RollbackAsync(cancellationToken);
            throw;
        }
    }

    private static async Task InsertWorkflowAsync(
        DbConnection connection,
        DbTransaction transaction,
        CandidateWorkflowInstance instance,
        CandidateWorkflowAction action,
        CancellationToken cancellationToken)
    {
        const string instanceSql = """
            INSERT INTO workflow_instances (
              id, workflow_id, business_type, business_id, status, current_step,
              assignee_id, created_at, completed_at
            ) VALUES (
              @id, @workflowId, @businessType, @businessId, @status, @currentStep,
              @assigneeId, @createdAt, @completedAt
            )
            """;
        await ExecuteAsync(connection, transaction, instanceSql,
        [
            new("@id", instance.Id),
            new("@workflowId", instance.WorkflowId),
            new("@businessType", instance.BusinessType),
            new("@businessId", instance.BusinessId),
            new("@status", instance.Status),
            new("@currentStep", instance.CurrentStep),
            new("@assigneeId", instance.AssigneeId),
            new("@createdAt", instance.CreatedAt),
            new("@completedAt", instance.CompletedAt)
        ], cancellationToken);
        const string actionSql = """
            INSERT INTO workflow_actions (
              id, instance_id, actor_id, action, note, from_assignee_id, to_assignee_id, created_at
            ) VALUES (
              @id, @instanceId, @actorId, @action, @note, @fromAssigneeId, @toAssigneeId, @createdAt
            )
            """;
        await ExecuteAsync(connection, transaction, actionSql,
        [
            new("@id", action.Id),
            new("@instanceId", action.InstanceId),
            new("@actorId", action.ActorId),
            new("@action", action.Action),
            new("@note", Optional(action.Note)),
            new("@fromAssigneeId", action.FromAssigneeId),
            new("@toAssigneeId", action.ToAssigneeId),
            new("@createdAt", action.CreatedAt)
        ], cancellationToken);
    }

    private static async Task ExecuteAsync(
        DbConnection connection,
        DbTransaction transaction,
        string sql,
        IReadOnlyList<QueryParameter> parameters,
        CancellationToken cancellationToken)
    {
        await using var command = CreateCommand(connection, sql, parameters, transaction);
        await command.ExecuteNonQueryAsync(cancellationToken);
    }

    private static DbCommand CreateCommand(
        DbConnection connection,
        string sql,
        IReadOnlyList<QueryParameter> parameters,
        DbTransaction? transaction = null)
    {
        var command = connection.CreateCommand();
        command.CommandText = sql;
        command.Transaction = transaction;
        foreach (var item in parameters)
        {
            var parameter = command.CreateParameter();
            parameter.ParameterName = item.Name;
            parameter.Value = item.Value ?? DBNull.Value;
            command.Parameters.Add(parameter);
        }

        return command;
    }

    private static string Text(JsonObject value, string property) => value[property]?.ToString() ?? string.Empty;

    private static bool Boolean(JsonObject value, string property) => value[property]?.GetValue<bool>() == true;

    private static object? Optional(string? value) => string.IsNullOrEmpty(value) ? null : value;

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

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