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

namespace Eis.Infrastructure.Administration;

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

internal sealed class AdminWriteRepository(
    IRelationalConnectionFactory connectionFactory,
    IApplicationCache cache)
{
    public Task SaveSchoolAsync(
        AdminSchool school,
        bool isNew,
        AdminAuditEntry audit,
        CancellationToken cancellationToken)
    {
        var operation = isNew
            ? new SqlOperation(
                """
                INSERT INTO schools (id, name, code, address, is_source_school, is_admission_school, active)
                VALUES (@id, @name, @code, @address, @isSourceSchool, @isAdmissionSchool, @active)
                """,
                [
                    new("@id", school.Id), new("@name", school.Name), new("@code", school.Code),
                    new("@address", Optional(school.Address)), new("@isSourceSchool", Number(school.IsSourceSchool)),
                    new("@isAdmissionSchool", Number(school.IsAdmissionSchool)), new("@active", Number(school.Active))
                ])
            : new SqlOperation(
                """
                UPDATE schools SET name = @name, code = @code, address = @address,
                  is_source_school = @isSourceSchool, is_admission_school = @isAdmissionSchool, active = @active
                WHERE id = @id
                """,
                [
                    new("@name", school.Name), new("@code", school.Code), new("@address", Optional(school.Address)),
                    new("@isSourceSchool", Number(school.IsSourceSchool)), new("@isAdmissionSchool", Number(school.IsAdmissionSchool)),
                    new("@active", Number(school.Active)), new("@id", school.Id)
                ]);
        return ExecuteWithAuditAsync(operation, audit, cancellationToken);
    }

    public Task SaveClassAsync(
        AdminClass schoolClass,
        bool isNew,
        AdminAuditEntry audit,
        CancellationToken cancellationToken)
    {
        var operation = isNew
            ? new SqlOperation(
                """
                INSERT INTO school_classes (id, school_id, name, grade, active)
                VALUES (@id, @schoolId, @name, @grade, @active)
                """,
                [
                    new("@id", schoolClass.Id), new("@schoolId", schoolClass.SchoolId), new("@name", schoolClass.Name),
                    new("@grade", schoolClass.Grade), new("@active", Number(schoolClass.Active))
                ])
            : new SqlOperation(
                "UPDATE school_classes SET name = @name, grade = @grade, active = @active WHERE id = @id",
                [
                    new("@name", schoolClass.Name), new("@grade", schoolClass.Grade),
                    new("@active", Number(schoolClass.Active)), new("@id", schoolClass.Id)
                ]);
        return ExecuteWithAuditAsync(operation, audit, cancellationToken);
    }

    public Task CreateAdminAsync(
        AdminUser user,
        string passwordHash,
        AdminAuditEntry audit,
        CancellationToken cancellationToken) => ExecuteWithAuditAsync(
            new SqlOperation(
                """
                INSERT INTO users (
                  id, username, password_hash, role, admin_level, school_id, class_id, active, display_name, created_at
                ) VALUES (
                  @id, @username, @passwordHash, 'admin', @adminLevel, @schoolId, @classId, @active, @displayName, @createdAt
                )
                """,
                [
                    new("@id", user.Id), new("@username", user.Username), new("@passwordHash", passwordHash),
                    new("@adminLevel", user.AdminLevel), new("@schoolId", user.SchoolId), new("@classId", user.ClassId),
                    new("@active", Number(user.Active)), new("@displayName", user.DisplayName), new("@createdAt", user.CreatedAt)
                ]),
            audit,
            cancellationToken);

    public Task UpdateAdminAsync(
        string userId,
        string displayName,
        string? classId,
        bool active,
        string? passwordHash,
        AdminAuditEntry audit,
        CancellationToken cancellationToken)
    {
        var operation = passwordHash is null
            ? new SqlOperation(
                "UPDATE users SET display_name = @displayName, class_id = @classId, active = @active WHERE id = @id",
                [
                    new("@displayName", displayName), new("@classId", classId),
                    new("@active", Number(active)), new("@id", userId)
                ])
            : new SqlOperation(
                """
                UPDATE users SET display_name = @displayName, class_id = @classId, active = @active,
                  password_hash = @passwordHash WHERE id = @id
                """,
                [
                    new("@displayName", displayName), new("@classId", classId), new("@active", Number(active)),
                    new("@passwordHash", passwordHash), new("@id", userId)
                ]);
        return ExecuteWithAuditAsync(operation, audit, cancellationToken);
    }

    public Task UpdateSelfRegistrationAsync(
        bool enabled,
        AdminAuditEntry audit,
        CancellationToken cancellationToken) => ExecuteWithAuditAsync(
            new SqlOperation(
                "UPDATE schema_metadata SET self_registration_enabled = @enabled WHERE id = 1",
                [new("@enabled", Number(enabled))]),
            audit,
            cancellationToken);

    public Task UpdateCandidateArchivesAsync(
        IReadOnlyList<string> userIds,
        string? archivedAt,
        string? archivedBy,
        AdminAuditEntry audit,
        CancellationToken cancellationToken) => ExecuteWithAuditAsync(
            userIds.Select(userId => new SqlOperation(
                "UPDATE users SET archived_at = @archivedAt, archived_by = @archivedBy WHERE id = @id",
                [new("@archivedAt", archivedAt), new("@archivedBy", archivedBy), new("@id", userId)])),
            audit,
            cancellationToken);

    public Task ResetCandidatePasswordAsync(
        string userId,
        string passwordHash,
        AdminAuditEntry audit,
        CancellationToken cancellationToken) => ExecuteWithAuditAsync(
            new SqlOperation(
                "UPDATE users SET password_hash = @passwordHash, must_change_password = 1 WHERE id = @id",
                [new("@passwordHash", passwordHash), new("@id", userId)]),
            audit,
            cancellationToken);

    public Task ProcessProfileWorkflowAsync(
        AccountWorkflowInstance instance,
        AccountWorkflowAction action,
        string profileId,
        string profileStatus,
        string reviewNote,
        string? reviewedAt,
        string? reviewerId,
        AdminAuditEntry audit,
        CancellationToken cancellationToken) => ExecuteWithAuditAsync(
            [
                new SqlOperation(
                    """
                    UPDATE workflow_instances SET status = @status, current_step = @currentStep,
                      assignee_id = @assigneeId, completed_at = @completedAt WHERE id = @id
                    """,
                    [
                        new("@status", instance.Status), new("@currentStep", instance.CurrentStep),
                        new("@assigneeId", instance.AssigneeId), new("@completedAt", instance.CompletedAt),
                        new("@id", instance.Id)
                    ]),
                new SqlOperation(
                    """
                    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
                    )
                    """,
                    [
                        new("@id", action.Id), new("@instanceId", action.InstanceId), new("@actorId", action.ActorId),
                        new("@action", action.Action), new("@note", action.Note), new("@fromAssigneeId", action.FromAssigneeId),
                        new("@toAssigneeId", action.ToAssigneeId), new("@createdAt", action.CreatedAt)
                    ]),
                new SqlOperation(
                    """
                    UPDATE candidate_profiles SET status = @status, review_note = @reviewNote,
                      reviewed_at = @reviewedAt, reviewer_id = @reviewerId WHERE id = @id
                    """,
                    [
                        new("@status", profileStatus), new("@reviewNote", Optional(reviewNote)),
                        new("@reviewedAt", reviewedAt), new("@reviewerId", reviewerId), new("@id", profileId)
                    ])
            ],
            audit,
            cancellationToken);

    public Task UpdateRegistrationPaymentAsync(
        string registrationId,
        string paymentStatus,
        string? paidAt,
        string? paidBy,
        AdminAuditEntry audit,
        CancellationToken cancellationToken) => ExecuteWithAuditAsync(
            new SqlOperation(
                """
                UPDATE registrations SET payment_status = @paymentStatus, paid_at = @paidAt, paid_by = @paidBy
                WHERE id = @id
                """,
                [
                    new("@paymentStatus", paymentStatus), new("@paidAt", paidAt),
                    new("@paidBy", paidBy), new("@id", registrationId)
                ]),
            audit,
            cancellationToken);

    public Task ProcessRegistrationWorkflowAsync(
        AccountWorkflowInstance instance,
        AccountWorkflowAction action,
        string registrationId,
        string status,
        string paymentStatus,
        string? paidAt,
        string? paidBy,
        string? reviewedAt,
        string reviewNote,
        string registrationNumber,
        string? numberRuleId,
        AdminAuditEntry audit,
        CancellationToken cancellationToken) => ExecuteWithAuditAsync(
            [
                new SqlOperation(
                    """
                    UPDATE workflow_instances SET status = @status, current_step = @currentStep,
                      assignee_id = @assigneeId, completed_at = @completedAt WHERE id = @id
                    """,
                    [
                        new("@status", instance.Status), new("@currentStep", instance.CurrentStep),
                        new("@assigneeId", instance.AssigneeId), new("@completedAt", instance.CompletedAt),
                        new("@id", instance.Id)
                    ]),
                new SqlOperation(
                    """
                    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
                    )
                    """,
                    [
                        new("@id", action.Id), new("@instanceId", action.InstanceId), new("@actorId", action.ActorId),
                        new("@action", action.Action), new("@note", action.Note), new("@fromAssigneeId", action.FromAssigneeId),
                        new("@toAssigneeId", action.ToAssigneeId), new("@createdAt", action.CreatedAt)
                    ]),
                new SqlOperation(
                    """
                    UPDATE registrations SET status = @status, payment_status = @paymentStatus,
                      paid_at = @paidAt, paid_by = @paidBy, reviewed_at = @reviewedAt,
                      review_note = @reviewNote, registration_number = @registrationNumber,
                      number_rule_id = @numberRuleId WHERE id = @id
                    """,
                    [
                        new("@status", status), new("@paymentStatus", paymentStatus), new("@paidAt", paidAt),
                        new("@paidBy", paidBy), new("@reviewedAt", reviewedAt), new("@reviewNote", Optional(reviewNote)),
                        new("@registrationNumber", Optional(registrationNumber)), new("@numberRuleId", numberRuleId),
                        new("@id", registrationId)
                    ])
            ],
            audit,
            cancellationToken);

    private async Task ExecuteWithAuditAsync(
        SqlOperation operation,
        AdminAuditEntry audit,
        CancellationToken cancellationToken) => await ExecuteWithAuditAsync([operation], audit, cancellationToken);

    private async Task ExecuteWithAuditAsync(
        IEnumerable<SqlOperation> operations,
        AdminAuditEntry audit,
        CancellationToken cancellationToken)
    {
        await using var connection = await connectionFactory.OpenAsync(cancellationToken);
        await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
        try
        {
            foreach (var operation in operations)
            {
                await ExecuteAsync(connection, transaction, operation, cancellationToken);
            }
            await ExecuteAsync(connection, transaction, new SqlOperation(
                """
                INSERT INTO audit_logs (id, actor_id, action, detail, created_at)
                VALUES (@id, @actorId, @action, @detail, @createdAt)
                """,
                [
                    new("@id", audit.Id), new("@actorId", audit.ActorId), new("@action", audit.Action),
                    new("@detail", audit.Detail), new("@createdAt", audit.CreatedAt)
                ]), cancellationToken);
            await transaction.CommitAsync(cancellationToken);
        }
        catch
        {
            await transaction.RollbackAsync(cancellationToken);
            throw;
        }
        await cache.InvalidateAsync("public");
    }

    private static async Task ExecuteAsync(
        DbConnection connection,
        DbTransaction transaction,
        SqlOperation operation,
        CancellationToken cancellationToken)
    {
        await using var command = connection.CreateCommand();
        command.Transaction = transaction;
        command.CommandText = operation.Sql;
        foreach (var item in operation.Parameters)
        {
            var parameter = command.CreateParameter();
            parameter.ParameterName = item.Name;
            parameter.Value = item.Value ?? DBNull.Value;
            command.Parameters.Add(parameter);
        }
        await command.ExecuteNonQueryAsync(cancellationToken);
    }

    private static int Number(bool value) => value ? 1 : 0;

    private static string? Optional(string value) => value.Length == 0 ? null : value;

    private sealed record SqlOperation(string Sql, IReadOnlyList<SqlParameterValue> Parameters);

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