functions/generation/Invoke-SldgDataGeneration.ps1

function Invoke-SldgDataGeneration {
    <#
    .SYNOPSIS
        Executes data generation according to a generation plan.

    .DESCRIPTION
        Generates synthetic data for all tables in the plan, respecting FK dependencies,
        unique constraints, and custom rules. Data is generated in topological order
        so that parent tables are populated before child tables.

        Within each row, columns with -CrossColumnDependency rules (set via Set-SldgGenerationRule)
        are automatically reordered so that dependency columns are generated first. This enables
        context-dependent AI generation — e.g., a JSON column can vary its structure based on
        the value of a report-type column in the same row.

    .PARAMETER Plan
        The generation plan from New-SldgGenerationPlan.

    .PARAMETER ConnectionInfo
        Target database connection. If not specified, uses the active connection.

    .PARAMETER WhatIf
        Shows what would be generated without actually inserting data.

    .PARAMETER NoInsert
        Generates data in memory but does not write to the database.
        Use this with -PassThru to get the generated DataTables.

    .PARAMETER PassThru
        Returns the generated data as part of the result object.

    .PARAMETER UseTransaction
        Wraps all inserts in a single database transaction. If any table fails,
        all previously inserted data is rolled back.

    .PARAMETER Parallel
        Generates independent tables in parallel (Synthetic and Scenario modes; masking stays sequential).

    .PARAMETER ThrottleLimit
        Maximum number of tables to generate in parallel when using -Parallel.

    .PARAMETER ValidateAfterGeneration
        Runs Test-SldgGeneratedData after inserts complete and attaches the validation results to the generation result.

    .PARAMETER FailOnValidationError
        Turns validation errors from -ValidateAfterGeneration into a terminating error.

    .PARAMETER FailOnSkippedRows
        Turns any skipped/ignored inserted rows into a terminating error. This is useful for strict data quality gates.

    .PARAMETER Confirm
        Prompts for confirmation before inserting data into each table.

    .EXAMPLE
        PS C:\> $result = Invoke-SldgDataGeneration -Plan $plan

        Generates and inserts data for all tables in the plan.

    .EXAMPLE
        PS C:\> $result = Invoke-SldgDataGeneration -Plan $plan -NoInsert -PassThru

        Generates data in memory without inserting.

    .EXAMPLE
        PS C:\> $result = Invoke-SldgDataGeneration -Plan $plan -UseTransaction

        Generates and inserts data within a single transaction. If any table fails,
        all previously inserted data is rolled back.

    .EXAMPLE
        PS C:\> $result = Invoke-SldgDataGeneration -Plan $plan -Parallel -ThrottleLimit 4

        Independent tables are generated in parallel, up to 4 at a time.
    #>

    [OutputType([SqlLabDataGenerator.GenerationResult])]
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [SqlLabDataGenerator.GenerationPlan]$Plan,

        [SqlLabDataGenerator.Connection]$ConnectionInfo,

        [switch]$NoInsert,

        [switch]$PassThru,

        [switch]$UseTransaction,

        [switch]$Parallel,

        [int]$ThrottleLimit,

        [switch]$ValidateAfterGeneration,

        [switch]$FailOnValidationError,

        [switch]$FailOnSkippedRows
    )

    process {
    if (-not $ConnectionInfo) { $ConnectionInfo = $script:SldgState.ActiveConnection }
    if (-not $ConnectionInfo -and -not $NoInsert) {
        Stop-PSFFunction -String 'Connect.NoActiveConnectionOrNoInsert' -EnableException $true
    }

    if ($ConnectionInfo) { Assert-SldgConnectionOpen -ConnectionInfo $ConnectionInfo }
    if ($ValidateAfterGeneration -and ($NoInsert -or -not $ConnectionInfo)) {
        Stop-PSFFunction -String 'Validation.RequiresInsertedData' -EnableException $true
    }

    $provider = if ($ConnectionInfo) { Get-SldgProviderInternal -ConnectionInfo $ConnectionInfo } else { $null }

    # Resolve all configuration + per-run identity in one place (refactor extract).
    $ctx = Initialize-SldgGenerationContext -ThrottleLimitOverride $ThrottleLimit
    $batchSize          = $ctx.BatchSize
    $streamingThreshold = $ctx.StreamingThreshold
    $streamingChunkSize = $ctx.StreamingChunkSize
    $fkQueryLimit       = $ctx.FkQueryLimit
    $uniqueQueryLimit   = $ctx.UniqueQueryLimit
    $dbCommandTimeout   = $ctx.DbCommandTimeout
    $ThrottleLimit      = $ctx.ThrottleLimit
    $correlationId      = $ctx.CorrelationId
    $executingUser      = $ctx.ExecutingUser
    $generationStartTime = $ctx.GenerationStartTime

    Write-PSFMessage -Level Host -Message ($script:strings.'Generation.Starting' -f $Plan.TableCount, $Plan.Mode)

    # Seed for reproducibility
    if ($ctx.Seed -gt 0) {
        $null = Get-Random -SetSeed $ctx.Seed
    }

    $fkValues = @{}
    $script:SldgFkWarningIssued.Clear()
    $tableResults = [System.Collections.Generic.List[object]]::new()
    $fkFallbackStats = [System.Collections.Generic.List[object]]::new()
    $totalInserted = 0
    $transaction = $null
    Write-PSFMessage -Level Verbose -Message ('Generation correlation id: {0}' -f $correlationId)

    Write-PSFMessage -Level Verbose -Message ($script:strings.'Generation.AuditStart' -f $executingUser, $Plan.Database, $Plan.TableCount, $Plan.Mode)

    # Start a transaction if requested
    if ($UseTransaction -and -not $NoInsert -and $ConnectionInfo) {
        $transaction = $ConnectionInfo.DbConnection.BeginTransaction()
        Write-PSFMessage -Level Verbose -Message ($script:strings.'Generation.TransactionStarted' -f $ConnectionInfo.Provider)
    }

    # Parallel config
    $useParallel = $Parallel -and -not ($Plan.Mode -eq 'Masking')

    $generationFailed = $false
    $validationResults = @()
    $isMaskingMode = $Plan.Mode -eq 'Masking'
    $failedTables = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)

    # S4: Auto-enable transaction for masking mode to prevent data loss from DELETE+INSERT
    if ($isMaskingMode -and -not $NoInsert -and $ConnectionInfo -and -not $transaction) {
        $transaction = $ConnectionInfo.DbConnection.BeginTransaction()
        Write-PSFMessage -Level Verbose -Message $script:strings.'Generation.MaskingTransactionStarted'
    }

    $tableIndex = 0
    $tableTotal = $Plan.Tables.Count

    # Auto-enable AI row-level generation when the plan was created with -UseAI
    $aiGenOverrideApplied = $false
    if ($Plan.UseAIGeneration) {
        $currentAIGen = Get-PSFConfigValue -FullName 'SqlLabDataGenerator.Generation.AIGeneration'
        if (-not $currentAIGen) {
            Set-PSFConfig -FullName 'SqlLabDataGenerator.Generation.AIGeneration' -Value $true
            $aiGenOverrideApplied = $true
            Write-PSFMessage -Level Verbose -Message $script:strings.'Generation.AIGenOverride'
        }
    }

    # A2: Pre-scan and disable FK constraints for circular dependency tables before insertion
    $circularTables = @($Plan.Tables | Where-Object { $_.HasCircularDependency })
    $disabledFKInfo = $null
    if ($circularTables.Count -gt 0 -and -not $NoInsert -and $ConnectionInfo -and $ConnectionInfo.DbConnection) {
        $disableParams = @{
            CircularTables = $circularTables
            ConnectionInfo = $ConnectionInfo
        }
        if ($transaction) { $disableParams['Transaction'] = $transaction }
        $disabledFKInfo = Disable-SldgCircularFKConstraint @disableParams
    }

    # ── Parallel generation path (Synthetic/Scenario only) ──
    if ($useParallel -and $Plan.Tables.Count -gt 0) {
        $parallelResult = Invoke-SldgParallelTableGeneration -Plan $Plan -FkValues $fkValues `
            -ConnectionInfo $ConnectionInfo -Provider $provider -Transaction $transaction `
            -BatchSize $batchSize -ThrottleLimit $ThrottleLimit `
            -StreamingThreshold $streamingThreshold -StreamingChunkSize $streamingChunkSize `
            -NoInsert:$NoInsert -PassThru:$PassThru

        $tableResults.AddRange($parallelResult.TableResults)
        $totalInserted = $parallelResult.TotalInserted
        $generationFailed = $parallelResult.GenerationFailed
        foreach ($fallback in @($parallelResult.FKFallbackStats)) { $fkFallbackStats.Add($fallback) }
        if ($generationFailed -and $transaction) {
            try { $transaction.Rollback() } catch { Write-PSFMessage -Level Warning -Message ($script:strings.'Generation.ParallelRollbackFailed' -f $_) }
            $transaction = $null
        }
    }
    # ── Sequential generation path (original) ──
    else {

    foreach ($tablePlan in $Plan.Tables) {
        $tableIndex++
        $pct = [int](($tableIndex - 1) / [Math]::Max($tableTotal, 1) * 100)
        Write-Progress -Activity 'Generating data' -Status "Table $tableIndex of ${tableTotal}: $($tablePlan.FullName)" -PercentComplete $pct

        if (-not $PSCmdlet.ShouldProcess("$($tablePlan.FullName) ($($tablePlan.RowCount) rows)", "Generate data")) {
            continue
        }

        # Per-table stopwatch for audit DurationMs (correlation traceability)
        $tableStopwatch = [System.Diagnostics.Stopwatch]::StartNew()
        $tableResultCountBefore = $tableResults.Count

        # Masking mode: read existing data, mask PII columns, write back
        if ($isMaskingMode) {
            if (-not $ConnectionInfo -or -not $provider) {
                Stop-PSFFunction -Message $script:strings.'Generation.MaskingNotSupported' -EnableException $true
            }

            Invoke-PSFProtectedCommand -ActionString 'Generation.MaskingTable' -ActionStringValues $tablePlan.RowCount, $tablePlan.SchemaName, $tablePlan.TableName -Target $tablePlan.FullName -ScriptBlock {
                $maskParams = @{
                    TablePlan      = $tablePlan
                    ConnectionInfo = $ConnectionInfo
                    Provider       = $provider
                    Plan           = $Plan
                    BatchSize      = $batchSize
                    NoInsert       = $NoInsert
                    PassThru       = $PassThru
                }
                if ($transaction) { $maskParams['Transaction'] = $transaction }

                $maskResult = Invoke-SldgMaskingTable @maskParams
                $totalInserted += $maskResult.RowCount
                $tableResults.Add($maskResult)
            } -PSCmdlet $PSCmdlet -EnableException $false

            if (Test-PSFFunctionInterrupt) {
                $tableResults.Add([SqlLabDataGenerator.TableResult]@{
                        TableName  = $tablePlan.FullName
                        RowCount   = 0
                        Success    = $false
                        Error      = $Error[0].Exception.Message
                    })
                if ($transaction) {
                    $generationFailed = $true
                Write-PSFMessage -Level Warning -Message ($script:strings.'Generation.MaskingRollingBack' -f $tablePlan.FullName)
                try { $transaction.Rollback() }
                catch {
                    Write-PSFMessage -Level Error -Message ($script:strings.'Generation.MaskingRollbackCritical' -f $_)
                    }
                    $transaction = $null
                    $totalInserted = 0
                Stop-PSFFunction -String 'Generation.MaskingRolledBack' -StringValues $tablePlan.FullName -EnableException $true
                }
            }
            continue
        }

        # Skip tables whose FK parent tables have failed — child will inevitably fail without FK values
        if ($failedTables.Count -gt 0 -and $tablePlan.ForeignKeys -and $tablePlan.ForeignKeys.Count -gt 0) {
            $failedParents = @($tablePlan.ForeignKeys | ForEach-Object { "$($_.ReferencedSchema).$($_.ReferencedTable)" } | Where-Object { $failedTables.Contains($_) } | Select-Object -Unique)
            if ($failedParents.Count -gt 0) {
                Write-PSFMessage -Level Warning -Message ($script:strings.'Generation.SkippedDueToParent' -f $tablePlan.FullName, ($failedParents -join ', '))
                [void]$failedTables.Add($tablePlan.FullName)
                $tableResults.Add([SqlLabDataGenerator.TableResult]@{
                        TableName  = $tablePlan.FullName
                        RowCount   = 0
                        Success    = $false
                        Error      = "Skipped: parent table(s) failed: $($failedParents -join ', ')"
                    })
                continue
            }
        }

        Write-PSFMessage -Level Host -Message ($script:strings.'Generation.Table' -f $tablePlan.RowCount, $tablePlan.SchemaName, $tablePlan.TableName)

        # FK DB fallback: batch-load missing FK parent values grouped by parent table
        if ($tablePlan.ForeignKeys -and $tablePlan.ForeignKeys.Count -gt 0 -and $ConnectionInfo -and $provider) {
            $fkFallbackParams = @{
                TablePlan      = $tablePlan
                FkValues       = $fkValues
                ConnectionInfo = $ConnectionInfo
                FkQueryLimit   = $fkQueryLimit
                CommandTimeout = $dbCommandTimeout
            }
            if ($transaction) { $fkFallbackParams['Transaction'] = $transaction }
            $loadedFallbacks = Resolve-SldgForeignKeyFallback @fkFallbackParams
            foreach ($fallback in @($loadedFallbacks)) { $fkFallbackStats.Add($fallback) }
        }

        # Get table info from schema (need full column info)
        $tableRules = if ($Plan.GenerationRules.ContainsKey($tablePlan.FullName)) {
            $Plan.GenerationRules[$tablePlan.FullName]
        }
        else { $null }

        # Build a table info object with semantic types
        $tableInfo = ConvertTo-SldgTableInfo -TablePlan $tablePlan

        # For non-identity integer PK columns, query MAX(PK) so we can auto-generate sequential values
        if ($ConnectionInfo) {
            Set-SldgPrimaryKeyStartValue -TableInfo $tableInfo -TablePlan $tablePlan `
                -ConnectionInfo $ConnectionInfo -Transaction $transaction -CommandTimeout $dbCommandTimeout
        }

        Invoke-PSFProtectedCommand -ActionString 'Generation.InsertingTable' -ActionStringValues $tablePlan.RowCount, $tablePlan.SchemaName, $tablePlan.TableName -Target $tablePlan.FullName -ScriptBlock {

            # Streaming mode: large tables generate and write in chunks to keep memory bounded
            if ($streamingThreshold -gt 0 -and $tablePlan.RowCount -gt $streamingThreshold) {
                Write-PSFMessage -Level Host -Message ($script:strings.'Generation.StreamingStarting' -f $tablePlan.FullName, $tablePlan.RowCount, $streamingChunkSize)

                $streamParams = @{
                    TableInfo        = $tableInfo
                    TotalRowCount    = $tablePlan.RowCount
                    ChunkSize        = $streamingChunkSize
                    GeneratorMap     = $Plan.GeneratorMap
                    ForeignKeyValues = $fkValues
                    TableRules       = $tableRules
                    BatchSize        = $batchSize
                    NoInsert         = $NoInsert
                    PassThru         = $PassThru
                }
                if ($ConnectionInfo) { $streamParams['ConnectionInfo'] = $ConnectionInfo }
                if ($transaction) { $streamParams['Transaction'] = $transaction }
                if ($provider) { $streamParams['WriteFunction'] = $provider.FunctionMap.WriteData }

                # Query existing unique values for streaming mode (same as non-streaming path)
                if ($ConnectionInfo -and $provider) {
                    $uqParams = @{
                        TableInfo        = $tableInfo
                        TablePlan        = $tablePlan
                        ConnectionInfo   = $ConnectionInfo
                        UniqueQueryLimit = $uniqueQueryLimit
                        CommandTimeout   = $dbCommandTimeout
                    }
                    if ($transaction) { $uqParams['Transaction'] = $transaction }
                    $streamExistingUnique = Get-SldgExistingUniqueValue @uqParams
                    if ($streamExistingUnique) { $streamParams['ExistingUniqueValues'] = $streamExistingUnique }
                }

                # Two-tier AI: pass per-table generation notes to streaming mode
                if ($Plan.AIAdvice -and $Plan.AIAdvice.TableGenerationNotes -and $Plan.AIAdvice.TableGenerationNotes.ContainsKey($tablePlan.FullName)) {
                    $streamParams['TableNotes'] = $Plan.AIAdvice.TableGenerationNotes[$tablePlan.FullName]
                }

                $streamResult = Invoke-SldgStreamingGeneration @streamParams

                foreach ($key in $streamResult.GeneratedValues.Keys) {
                    $fkValues[$key] = $streamResult.GeneratedValues[$key]
                }
                $insertedCount = $streamResult.InsertedCount
            }
            else {
                # Query existing unique values from the DB in a single batched query
                $existingUnique = $null
                if ($ConnectionInfo -and $provider) {
                    $uqParams = @{
                        TableInfo      = $tableInfo
                        TablePlan      = $tablePlan
                        ConnectionInfo = $ConnectionInfo
                        UniqueQueryLimit = $uniqueQueryLimit
                        CommandTimeout = $dbCommandTimeout
                    }
                    if ($transaction) { $uqParams['Transaction'] = $transaction }
                    $existingUnique = Get-SldgExistingUniqueValue @uqParams
                }

                $rowSetParams = @{
                    TableInfo           = $tableInfo
                    RowCount            = $tablePlan.RowCount
                    GeneratorMap        = $Plan.GeneratorMap
                    ForeignKeyValues    = $fkValues
                    TableRules          = $tableRules
                    ExistingUniqueValues = $existingUnique
                }

                # Two-tier AI: pass per-table generation notes from schema analysis
                if ($Plan.AIAdvice -and $Plan.AIAdvice.TableGenerationNotes -and $Plan.AIAdvice.TableGenerationNotes.ContainsKey($tablePlan.FullName)) {
                    $rowSetParams['TableNotes'] = $Plan.AIAdvice.TableGenerationNotes[$tablePlan.FullName]
                }

                $rowSet = New-SldgRowSet @rowSetParams

                # Merge generated FK values for child tables
                foreach ($key in $rowSet.GeneratedValues.Keys) {
                    $fkValues[$key] = $rowSet.GeneratedValues[$key]
                }

                $insertedCount = 0
                if (-not $NoInsert -and $ConnectionInfo) {
                    $writeParams = @{
                        ConnectionInfo = $ConnectionInfo
                        SchemaName     = $tablePlan.SchemaName
                        TableName      = $tablePlan.TableName
                        Data           = $rowSet.DataTable
                        BatchSize      = $batchSize
                    }
                    if ($transaction) { $writeParams['Transaction'] = $transaction }
                    $insertedCount = & $provider.FunctionMap.WriteData @writeParams

                    # Post-insert: collect actual PK values from DB for identity/auto-increment columns
                    # that are NOT in the in-memory DataTable. Child tables need these FK references.
                    $collectedPK = Get-SldgPostInsertPrimaryKeyValue -TablePlan $tablePlan -ExistingFkValues $fkValues `
                        -ConnectionInfo $ConnectionInfo -Transaction $transaction `
                        -FkQueryLimit $fkQueryLimit -CommandTimeout $dbCommandTimeout
                    foreach ($pkKey in $collectedPK.Keys) {
                        $fkValues[$pkKey] = $collectedPK[$pkKey]
                    }
                }
                else {
                    $insertedCount = $rowSet.RowCount
                }
            }

            $totalInserted += $insertedCount
            Write-PSFMessage -Level Host -Message ($script:strings.'Generation.TableComplete' -f $tablePlan.FullName, $insertedCount)

            $tableResult = [SqlLabDataGenerator.TableResult]@{
                TableName  = $tablePlan.FullName
                RowCount   = $insertedCount
                Success    = $true
                Error      = $null
            }
            $skippedRows = [Math]::Max(0, [int]$tablePlan.RowCount - [int]$insertedCount)
            $tableResult.RequestedRows = [int]$tablePlan.RowCount
            $tableResult.SkippedRows = $skippedRows
            if ($PassThru -and $rowSet) {
                $tableResult.DataTable = $rowSet.DataTable
            }
            elseif ($PassThru -and $streamResult -and $streamResult.DataTable) {
                $tableResult.DataTable = $streamResult.DataTable
            }
            elseif ($rowSet -and $rowSet.DataTable) {
                # Release DataTable memory when not returning to caller
                $rowSet.DataTable.Dispose()
            }
            $tableResults.Add($tableResult)
        } -PSCmdlet $PSCmdlet -EnableException $false

        if (Test-PSFFunctionInterrupt) {
            [void]$failedTables.Add($tablePlan.FullName)
            $tableResults.Add([SqlLabDataGenerator.TableResult]@{
                    TableName  = $tablePlan.FullName
                    RowCount   = 0
                    Success    = $false
                    Error      = $Error[0].Exception.Message
                })
            $failedResult = $tableResults[$tableResults.Count - 1]
            $failedResult.RequestedRows = [int]$tablePlan.RowCount
            $failedResult.SkippedRows = [int]$tablePlan.RowCount

            if ($transaction) {
                $generationFailed = $true
                Write-PSFMessage -Level Warning -Message ($script:strings.'Generation.RollingBack' -f $tablePlan.FullName)
                try { $transaction.Rollback() }
                catch {
                    Write-PSFMessage -Level Error -Message ($script:strings.'Generation.RollbackCritical' -f $_)
                }
                $transaction = $null
                $totalInserted = 0
                foreach ($tr in $tableResults) {
                    if ($tr.Success) {
                        $tr.RolledBack = $true
                    }
                }
                Stop-PSFFunction -String 'Generation.DataRolledBack' -StringValues $tablePlan.FullName -EnableException $true
            }
        }

        # Stamp per-table duration on any TableResult(s) added during this iteration
        $tableStopwatch.Stop()
        $tableElapsedMs = [int]$tableStopwatch.Elapsed.TotalMilliseconds
        for ($i = $tableResultCountBefore; $i -lt $tableResults.Count; $i++) {
            $tableResults[$i].DurationMs = $tableElapsedMs
        }
    }

    } # end: sequential/parallel branch

    # Re-enable FK constraints for all circular dependency tables after insertion
    $fkReenableFailures = [System.Collections.Generic.List[string]]::new()

    if ($disabledFKInfo -and $disabledFKInfo.DisabledTables.Count -gt 0 -and $ConnectionInfo -and $ConnectionInfo.DbConnection) {
        $reenableParams = @{
            DisabledInfo   = $disabledFKInfo
            ConnectionInfo = $ConnectionInfo
        }
        if ($transaction) { $reenableParams['Transaction'] = $transaction }
        $fkReenableFailures = Enable-SldgCircularFKConstraint @reenableParams
    }
    if ($fkReenableFailures.Count -gt 0) {
        $generationFailed = $true
        if ($transaction) {
            try { $transaction.Rollback() } catch { Write-PSFMessage -Level Error -Message ($script:strings.'Generation.FKReenableRollbackFailed' -f $_) }
            $transaction = $null
        }
        Stop-PSFFunction -String 'Generation.FKReenableCritical' -StringValues ($fkReenableFailures -join ', ') -EnableException $true
    }

    # Normalize per-table quality metrics. Parallel generation and skipped-parent paths may
    # create TableResult objects outside the sequential table block.
    foreach ($tableResult in $tableResults) {
        $matchingPlanTable = $Plan.Tables | Where-Object { $_.FullName -eq $tableResult.TableName } | Select-Object -First 1
        $requestedRowsForTable = if ($matchingPlanTable) { [int]$matchingPlanTable.RowCount } else { [int]$tableResult.RowCount }
        $skippedRowsForTable = if ($tableResult.Success) { [Math]::Max(0, $requestedRowsForTable - [int]$tableResult.RowCount) } else { 0 }
        $tableResult.RequestedRows = $requestedRowsForTable
        $tableResult.SkippedRows = $skippedRowsForTable
    }

    # Strict row-quality gate: detect provider fallbacks such as SQLite INSERT OR IGNORE.
    # Run before commit so -UseTransaction can still roll back strict failures.
    $skippedRowCount = ($tableResults | ForEach-Object { [int]$_.SkippedRows } | Measure-Object -Sum).Sum
    if ($null -eq $skippedRowCount) { $skippedRowCount = 0 }
    if ($FailOnSkippedRows -and $skippedRowCount -gt 0) {
        $generationFailed = $true
        if ($transaction) {
            try { $transaction.Rollback() } catch { Write-PSFMessage -Level Error -Message ($script:strings.'Generation.RollbackCritical' -f $_) }
            $transaction = $null
            $totalInserted = 0
        }
        Stop-PSFFunction -String 'Generation.SkippedRowsStrictFailure' -StringValues $skippedRowCount -EnableException $true
    }

    # Commit transaction if all succeeded
    if ($transaction -and -not $generationFailed) {
        try {
            $transaction.Commit()
            Write-PSFMessage -Level Verbose -Message $script:strings.'Generation.TransactionCommitted'
        }
        catch {
            Write-PSFMessage -Level Warning -Message ($script:strings.'Generation.CommitFailed' -f $_)
            try { $transaction.Rollback() }
            catch {
                Write-PSFMessage -Level Error -Message ($script:strings.'Generation.CommitRollbackCritical' -f $_)
            }
            $totalInserted = 0
            $generationFailed = $true
        }
    }

    # Optional post-generation database validation
    if ($ValidateAfterGeneration) {
        # The plan carries everything the validators need; rebuild a SchemaModel from it so
        # validation runs against the same tables and columns that were generated.
        $validationSchema = [SqlLabDataGenerator.SchemaModel]@{
            Database     = $Plan.Database
            Tables       = @($Plan.Tables | ForEach-Object { ConvertTo-SldgTableInfo -TablePlan $_ })
            TableCount   = $Plan.TableCount
            DiscoveredAt = Get-Date
        }
        $validationResults = @(Test-SldgGeneratedData -Schema $validationSchema -ConnectionInfo $ConnectionInfo)
        $validationErrors = @($validationResults | Where-Object { -not $_.Passed -and $_.Severity -eq 'Error' })
        if ($FailOnValidationError -and $validationErrors.Count -gt 0) {
            Stop-PSFFunction -String 'Validation.StrictFailure' -StringValues $validationErrors.Count -EnableException $true
        }
    }

    Write-Progress -Activity 'Generating data' -Completed

    $generationDuration = (Get-Date) - $generationStartTime
    Write-PSFMessage -Level Host -Message ($script:strings.'Generation.Complete' -f $Plan.TableCount, $totalInserted)
    Write-PSFMessage -Level Verbose -Message ($script:strings.'Generation.AuditComplete' -f $executingUser, $totalInserted, $generationDuration.TotalSeconds.ToString('F1'), $generationFailed)

    # Restore AI generation config if we overrode it
    if ($aiGenOverrideApplied) {
        Set-PSFConfig -FullName 'SqlLabDataGenerator.Generation.AIGeneration' -Value $false
    }

    # Persistent audit log — append a JSON record for compliance/traceability
    Write-SldgAuditRecord -Plan $Plan -TotalInserted $totalInserted -StartTime $generationStartTime `
        -User $executingUser -GenerationFailed $generationFailed -TableResults $tableResults `
        -CorrelationId $correlationId

    # Store generated data reference
    $script:SldgState.GeneratedData[$Plan.Database] = $tableResults

    $qualityReport = Get-SldgGenerationQualityReport -Plan $Plan -TableResults $tableResults `
        -TotalInserted $totalInserted -FkFallbackStats $fkFallbackStats `
        -ValidationRun:$ValidateAfterGeneration -ValidationResults $validationResults

    $result = [SqlLabDataGenerator.GenerationResult]@{
        Database      = $Plan.Database
        Mode          = $Plan.Mode
        TableCount    = $Plan.TableCount
        TotalRows     = $totalInserted
        Tables        = $tableResults.ToArray()
        SuccessCount  = ($tableResults | Where-Object Success).Count
        FailureCount  = ($tableResults | Where-Object { -not $_.Success }).Count
        StartedAt     = $generationStartTime
        CompletedAt   = Get-Date
        Duration      = $generationDuration
        User          = $executingUser
    }
    $result.QualityReport = $qualityReport
    $result.ValidationResults = @($validationResults | Where-Object { $null -ne $_ })
    $result
    }
}