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

namespace Eis.Infrastructure.Administration;

internal sealed record GeneratedBatchCandidate(
    string UserId,
    string CandidateNumber,
    string PasswordHash,
    string DisplayName,
    string SchoolId,
    string ClassId,
    string CreatedAt,
    string ProfileId,
    string SchoolName,
    string ClassName,
    string UpdatedAt);

internal sealed class AdminAccountBatchRepository(IRelationalConnectionFactory connectionFactory)
{
    public Task CreateAsync(
        AccountBatch batch,
        IReadOnlyList<AccountBatchItem> items,
        AccountWorkflowInstance instance,
        AccountWorkflowAction action,
        AdminAuditEntry audit,
        CancellationToken cancellationToken)
    {
        var operations = new List<SqlOperation>
        {
            new("""
                INSERT INTO candidate_account_batches (id, school_id, requested_by, status, review_note, created_at, reviewed_at)
                VALUES (@id, @schoolId, @requestedBy, @status, @reviewNote, @createdAt, @reviewedAt)
                """, [new("@id", batch.Id), new("@schoolId", batch.SchoolId), new("@requestedBy", batch.RequestedBy), new("@status", batch.Status), new("@reviewNote", Optional(batch.ReviewNote)), new("@createdAt", batch.CreatedAt), new("@reviewedAt", batch.ReviewedAt)])
        };
        operations.AddRange(items.Select(item => new SqlOperation("""
            INSERT INTO candidate_account_batch_items (id, batch_id, class_id, position, candidate_number, initial_password, user_id, created_at)
            VALUES (@id, @batchId, @classId, @position, @candidateNumber, @initialPassword, @userId, @createdAt)
            """, [new("@id", item.Id), new("@batchId", item.BatchId), new("@classId", item.ClassId), new("@position", item.Position), new("@candidateNumber", Optional(item.CandidateNumber)), new("@initialPassword", Optional(item.InitialPassword)), new("@userId", item.UserId), new("@createdAt", item.CreatedAt)])));
        operations.Add(InsertInstance(instance));
        operations.Add(InsertAction(action));
        operations.Add(InsertAudit(audit));
        return ExecuteTransactionAsync(operations, cancellationToken);
    }

    public Task ProcessAsync(
        AccountBatch batch,
        AccountWorkflowInstance instance,
        AccountWorkflowAction action,
        AdminAuditEntry audit,
        CancellationToken cancellationToken) => ExecuteTransactionAsync(
        [UpdateInstance(instance), InsertAction(action), UpdateBatch(batch), InsertAudit(audit)], cancellationToken);

    public Task CompleteAsync(
        AccountBatch batch,
        IReadOnlyList<AccountBatchItem> items,
        IReadOnlyList<GeneratedBatchCandidate> candidates,
        AccountWorkflowInstance instance,
        AccountWorkflowAction action,
        AdminAuditEntry audit,
        CancellationToken cancellationToken)
    {
        var operations = new List<SqlOperation> { UpdateInstance(instance), InsertAction(action), UpdateBatch(batch) };
        for (var index = 0; index < candidates.Count; index++)
        {
            var candidate = candidates[index];
            var item = items[index];
            operations.Add(new SqlOperation("""
                INSERT INTO users (
                  id, username, candidate_number, password_hash, role, admin_level, school_id, class_id, active,
                  must_change_password, archived_at, archived_by, display_name, created_at
                ) VALUES (
                  @id, @username, @candidateNumber, @passwordHash, 'candidate', NULL, @schoolId, @classId, 1,
                  1, NULL, NULL, @displayName, @createdAt
                )
                """, [new("@id", candidate.UserId), new("@username", candidate.CandidateNumber), new("@candidateNumber", candidate.CandidateNumber), new("@passwordHash", candidate.PasswordHash), new("@schoolId", candidate.SchoolId), new("@classId", candidate.ClassId), new("@displayName", candidate.DisplayName), new("@createdAt", candidate.CreatedAt)]));
            operations.Add(new SqlOperation("""
                INSERT INTO candidate_profiles (
                  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, profile_completed, status, review_note, reviewed_at, reviewer_id, updated_at
                ) VALUES (
                  @id, @userId, @name, NULL, @idNumber, '', NULL, @school, @grade, @schoolId, @classId,
                  NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
                  NULL, 0, 'pending', NULL, NULL, NULL, @updatedAt
                )
                """, [new("@id", candidate.ProfileId), new("@userId", candidate.UserId), new("@name", candidate.DisplayName), new("@idNumber", $"PENDING-{candidate.UserId}"), new("@school", candidate.SchoolName), new("@grade", candidate.ClassName), new("@schoolId", candidate.SchoolId), new("@classId", candidate.ClassId), new("@updatedAt", candidate.UpdatedAt)]));
            operations.Add(new SqlOperation("""
                UPDATE candidate_account_batch_items SET candidate_number = @candidateNumber,
                  initial_password = @initialPassword, user_id = @userId, created_at = @createdAt WHERE id = @id
                """, [new("@candidateNumber", item.CandidateNumber), new("@initialPassword", item.InitialPassword), new("@userId", item.UserId), new("@createdAt", item.CreatedAt), new("@id", item.Id)]));
        }
        operations.Add(InsertAudit(audit));
        return ExecuteTransactionAsync(operations, cancellationToken);
    }

    private async Task ExecuteTransactionAsync(IReadOnlyList<SqlOperation> operations, 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 transaction.CommitAsync(cancellationToken);
        }
        catch
        {
            await transaction.RollbackAsync(cancellationToken);
            throw;
        }
    }

    private static SqlOperation InsertInstance(AccountWorkflowInstance item) => new("""
        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)
        """, [new("@id", item.Id), new("@workflowId", item.WorkflowId), new("@businessType", item.BusinessType), new("@businessId", item.BusinessId), new("@status", item.Status), new("@currentStep", item.CurrentStep), new("@assigneeId", item.AssigneeId), new("@createdAt", item.CreatedAt), new("@completedAt", item.CompletedAt)]);

    private static SqlOperation UpdateInstance(AccountWorkflowInstance item) => new("""
        UPDATE workflow_instances SET status = @status, current_step = @currentStep,
          assignee_id = @assigneeId, completed_at = @completedAt WHERE id = @id
        """, [new("@status", item.Status), new("@currentStep", item.CurrentStep), new("@assigneeId", item.AssigneeId), new("@completedAt", item.CompletedAt), new("@id", item.Id)]);

    private static SqlOperation InsertAction(AccountWorkflowAction item) => new("""
        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", item.Id), new("@instanceId", item.InstanceId), new("@actorId", item.ActorId), new("@action", item.Action), new("@note", Optional(item.Note)), new("@fromAssigneeId", item.FromAssigneeId), new("@toAssigneeId", item.ToAssigneeId), new("@createdAt", item.CreatedAt)]);

    private static SqlOperation UpdateBatch(AccountBatch item) => new("""
        UPDATE candidate_account_batches SET status = @status, review_note = @reviewNote, reviewed_at = @reviewedAt WHERE id = @id
        """, [new("@status", item.Status), new("@reviewNote", Optional(item.ReviewNote)), new("@reviewedAt", item.ReviewedAt), new("@id", item.Id)]);

    private static SqlOperation InsertAudit(AdminAuditEntry item) => new("""
        INSERT INTO audit_logs (id, actor_id, action, detail, created_at) VALUES (@id, @actorId, @action, @detail, @createdAt)
        """, [new("@id", item.Id), new("@actorId", item.ActorId), new("@action", item.Action), new("@detail", item.Detail), new("@createdAt", item.CreatedAt)]);

    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 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);
}
