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

namespace Eis.Infrastructure.Administration;

internal sealed class AdminAdmissionRepository(
    IRelationalConnectionFactory connectionFactory,
    IApplicationCache cache)
{
    public async Task SaveRecordsAsync(
        IReadOnlyList<CandidateAdmissionRecord> records,
        AdminAuditEntry audit,
        CancellationToken cancellationToken)
    {
        await TransactionAsync(async (connection, transaction) =>
        {
            foreach (var record in records)
            {
                await ExecuteAsync(connection, transaction, "DELETE FROM admission_records WHERE id = @id",
                    [new("@id", record.Id)], cancellationToken);
                await ExecuteAsync(connection, transaction,
                    """
                    INSERT INTO admission_records (
                      id, kind, exam_id, user_id, school_id, status, payload_json, created_at, updated_at
                    ) VALUES (
                      @id, @kind, @examId, @userId, @schoolId, @status, @payload, @createdAt, @updatedAt
                    )
                    """,
                    [
                        new("@id", record.Id), new("@kind", record.Kind), new("@examId", record.ExamId),
                        new("@userId", record.UserId), new("@schoolId", record.SchoolId),
                        new("@status", record.Status), new("@payload", record.Payload.ToJsonString()),
                        new("@createdAt", record.CreatedAt), new("@updatedAt", record.UpdatedAt)
                    ], cancellationToken);
            }
            await InsertAuditAsync(connection, transaction, audit, cancellationToken);
        }, cancellationToken);
        await cache.InvalidateAsync("public");
    }

    public Task CreateAccountAsync(
        AdminUser account,
        string passwordHash,
        AdminAuditEntry audit,
        CancellationToken cancellationToken) =>
        TransactionAsync(async (connection, transaction) =>
        {
            await ExecuteAsync(connection, transaction,
                """
                INSERT INTO users (
                  id, username, password_hash, role, admin_level, school_id, class_id,
                  display_name, active, must_change_password, created_at
                ) VALUES (
                  @id, @username, @passwordHash, 'admission_school', NULL, @schoolId, NULL,
                  @displayName, 1, 0, @createdAt
                )
                """,
                [
                    new("@id", account.Id), new("@username", account.Username), new("@passwordHash", passwordHash),
                    new("@schoolId", account.SchoolId), new("@displayName", account.DisplayName),
                    new("@createdAt", account.CreatedAt)
                ], cancellationToken);
            await InsertAuditAsync(connection, transaction, audit, cancellationToken);
        }, cancellationToken);

    public Task UpdateAccountAsync(
        string accountId,
        string displayName,
        bool active,
        string? passwordHash,
        AdminAuditEntry audit,
        CancellationToken cancellationToken) =>
        TransactionAsync(async (connection, transaction) =>
        {
            await ExecuteAsync(connection, transaction,
                passwordHash is null
                    ? "UPDATE users SET display_name = @displayName, active = @active WHERE id = @id"
                    : "UPDATE users SET display_name = @displayName, active = @active, password_hash = @passwordHash WHERE id = @id",
                [
                    new("@displayName", displayName), new("@active", active ? 1 : 0),
                    new("@passwordHash", passwordHash), new("@id", accountId)
                ], cancellationToken);
            await InsertAuditAsync(connection, transaction, audit, cancellationToken);
        }, cancellationToken);

    private async Task TransactionAsync(
        Func<DbConnection, DbTransaction, Task> operation,
        CancellationToken cancellationToken)
    {
        await using var connection = await connectionFactory.OpenAsync(cancellationToken);
        await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
        try
        {
            await operation(connection, transaction);
            await transaction.CommitAsync(cancellationToken);
        }
        catch
        {
            await transaction.RollbackAsync(cancellationToken);
            throw;
        }
    }

    private static Task InsertAuditAsync(
        DbConnection connection,
        DbTransaction transaction,
        AdminAuditEntry audit,
        CancellationToken cancellationToken) =>
        ExecuteAsync(connection, transaction,
            "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);

    private static async Task ExecuteAsync(
        DbConnection connection,
        DbTransaction transaction,
        string sql,
        IReadOnlyList<SqlValue> values,
        CancellationToken cancellationToken)
    {
        await using var command = connection.CreateCommand();
        command.Transaction = transaction;
        command.CommandText = sql;
        foreach (var value in values)
        {
            var parameter = command.CreateParameter();
            parameter.ParameterName = value.Name;
            parameter.Value = value.Value ?? DBNull.Value;
            command.Parameters.Add(parameter);
        }
        await command.ExecuteNonQueryAsync(cancellationToken);
    }

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