internal/functions/generation/New-SldgGeneratedValue.ps1

function New-SldgGeneratedValue {
    <#
    .SYNOPSIS
        Generates a single value for a column based on its semantic type and generation rule.
    .DESCRIPTION
        Core value generation function. Handles FK lookups, identity skipping,
        nullable randomization, custom value lists, and generator dispatch.

        When a CustomRule contains AIGenerationHint and/or CrossColumnDependency,
        and the column generator is Json or Xml, passes the AI hint and the dependency
        column value (from RowContext) to New-SldgStructuredData for context-dependent
        AI-powered structured data generation.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        $Column,

        [hashtable]$GeneratorMap,

        [hashtable]$ForeignKeyValues,

        [hashtable]$CustomRule,

        [int]$NullProbability = -1,

        [hashtable]$RowContext
    )

    # Skip identity and computed columns
    if ($Column.IsIdentity -or $Column.IsComputed) { return $null }

    # Handle FK columns - pick from already-generated parent values
    if ($Column.ForeignKey -and $ForeignKeyValues) {
        $refKey = "$($Column.ForeignKey.ReferencedSchema).$($Column.ForeignKey.ReferencedTable).$($Column.ForeignKey.ReferencedColumn)"
        $parentValues = $ForeignKeyValues[$refKey]
        if ($parentValues -and $parentValues.Count -gt 0) {
            return ($parentValues | Get-Random)
        }

        # FK column but no parent values available — warn ONCE per column and return sentinel $null
        # so the caller (New-SldgRowSet) can handle the missing FK with its own fallback. The same
        # situation repeats for every row of the table; one warning carries the information.
        if ($script:SldgFkWarningIssued.Add("$($Column.ColumnName)|$refKey")) {
            Write-PSFMessage -Level Warning -Message ($script:strings.'Generation.FKParentValuesNotFound' -f $Column.ColumnName, $refKey)
        }
        return $null
    }

    # Handle custom rule override
    if ($CustomRule) {
        if ($CustomRule.ContainsKey('ValueList') -and $CustomRule.ValueList) {
            return ($CustomRule.ValueList | Get-Random)
        }
        if ($CustomRule.ContainsKey('StaticValue')) {
            return $CustomRule.StaticValue
        }
        if ($CustomRule.ContainsKey('ScriptBlock') -and $CustomRule.ScriptBlock) {
            return (& $CustomRule.ScriptBlock)
        }
        if ($CustomRule.ContainsKey('Generator') -and $CustomRule.Generator) {
            $gen = $GeneratorMap[$CustomRule.Generator]
            if ($gen) {
                $params = if ($CustomRule.ContainsKey('Params')) { $CustomRule.Params } else { $gen.Params }
                return (& $gen.Function @params)
            }
        }
    }

    # Handle nullable columns (configurable chance of NULL for nullable non-FK columns)
    # Exempt PII columns and unique columns — they must always have values
    if ($Column.IsNullable -and -not $Column.ForeignKey -and -not $Column.IsPrimaryKey -and -not $Column.IsUnique -and -not ($Column.Classification -and $Column.Classification.IsPII)) {
        $nullProb = if ($NullProbability -ge 0) { $NullProbability } else { Get-PSFConfigValue -FullName 'SqlLabDataGenerator.Generation.NullProbability' }
        if ((Get-Random -Minimum 0 -Maximum 100) -lt $nullProb) { return [DBNull]::Value }
    }

    # Get semantic type (from classification or fallback)
    $semanticType = if ($Column.SemanticType) { $Column.SemanticType }
    elseif ($Column.Classification) { $Column.Classification.SemanticType }
    else { $null }

    if (-not $semanticType) {
        $fallback = Resolve-SldgSemanticType -DataType $Column.DataType -MaxLength $Column.MaxLength -IsNullable $Column.IsNullable
        $semanticType = $fallback.Type
    }

    # Look up generator
    $gen = $GeneratorMap[$semanticType]
    if (-not $gen) {
        # Ultimate fallback based on data type
        $typeInfo = Resolve-SldgSemanticType -DataType $Column.DataType -MaxLength $Column.MaxLength -IsNullable $Column.IsNullable
        $gen = $GeneratorMap[$typeInfo.Type]
    }

    if (-not $gen) {
        # Generate basic value by data type
        $dt = $Column.DataType.ToLower()
        if ($dt -match '^(int|bigint|smallint|tinyint)$') { return (Get-Random -Minimum 1 -Maximum 10000) }
        elseif ($dt -match '^(bit)$') { return [bool](Get-Random -Minimum 0 -Maximum 2) }
        elseif ($dt -match '^(decimal|numeric|float|real|money)$') { return [Math]::Round((Get-Random -Minimum 1 -Maximum 10000) / 100.0, 2) }
        elseif ($dt -match '^(date|datetime|datetime2|smalldatetime|datetimeoffset)$') { return (Get-Date -Date ((Get-Date).AddDays(-(Get-Random -Minimum 1 -Maximum 1000))) -Format 'yyyy-MM-dd') }
        elseif ($dt -eq 'time') { return [timespan]::FromMinutes((Get-Random -Minimum 0 -Maximum 1440)) }
        elseif ($dt -eq 'uniqueidentifier') { return [guid]::NewGuid() }
        elseif ($dt -match '^(binary|varbinary|image)$') {
            $len = if ($Column.MaxLength -and $Column.MaxLength -gt 0) { [Math]::Min($Column.MaxLength, 16) } else { 16 }
            $bytes = [byte[]]::new($len); (New-Object System.Random).NextBytes($bytes); return $bytes
        }
        else { return "Value_$(Get-Random -Minimum 1 -Maximum 9999)" }
    }

    $params = @{} + $gen.Params

    # Apply MaxLength constraint for text generators
    if ($Column.MaxLength -and $Column.MaxLength -gt 0 -and $params.ContainsKey('Type') -and $gen.Function -eq 'New-SldgText') {
        $params['MaxLength'] = $Column.MaxLength
    }

    # Apply CHECK constraint ranges to numeric generators
    if ($Column.CheckConstraints -and $Column.CheckConstraints.Count -gt 0) {
        foreach ($check in $Column.CheckConstraints) {
            # Split on AND to handle compound constraints like 'Qty >= 1 AND Qty <= 999'
            $clauses = $check -split '\bAND\b'
            foreach ($clause in $clauses) {
                if ($clause -match '\[?\w+\]?\s*>=\s*([-+]?\d+\.?\d*)') {
                    $val = [double]$Matches[1]
                    if (-not $params.ContainsKey('Minimum') -or $val -gt $params['Minimum']) { $params['Minimum'] = $val }
                }
                if ($clause -match '\[?\w+\]?\s*<=\s*([-+]?\d+\.?\d*)') {
                    $val = [double]$Matches[1]
                    if (-not $params.ContainsKey('Maximum') -or $val -lt $params['Maximum']) { $params['Maximum'] = $val }
                }
                if ($clause -match '\[?\w+\]?\s*>\s*([-+]?\d+\.?\d*)') {
                    $val = [double]$Matches[1] + 1
                    if (-not $params.ContainsKey('Minimum') -or $val -gt $params['Minimum']) { $params['Minimum'] = $val }
                }
                if ($clause -match '\[?\w+\]?\s*<\s*([-+]?\d+\.?\d*)') {
                    $val = [double]$Matches[1] - 1
                    if (-not $params.ContainsKey('Maximum') -or $val -lt $params['Maximum']) { $params['Maximum'] = $val }
                }
            }
        }
    }

    # Pass column/table context to structured data generators (JSON/XML)
    if ($gen.Function -eq 'New-SldgStructuredData') {
        $params['ColumnName'] = $Column.ColumnName
        if ($Column.TableName) { $params['TableName'] = $Column.TableName }
        if ($Column.SchemaHint) { $params['SchemaHint'] = $Column.SchemaHint }
        if ($Column.MaxLength -and $Column.MaxLength -gt 0) { $params['MaxLength'] = $Column.MaxLength }

        # AI generation hint from rule or column metadata
        $hint = if ($CustomRule -and $CustomRule.AIGenerationHint) { $CustomRule.AIGenerationHint } else { $null }
        if ($hint) { $params['AIGenerationHint'] = $hint }

        # Value examples from rule or column metadata
        $examples = if ($CustomRule -and $CustomRule.ValueExamples) { $CustomRule.ValueExamples } else { $null }
        if ($examples) { $params['ValueExamples'] = $examples }

        # Cross-column dependency: pass context from already-generated columns in this row
        $depCol = if ($CustomRule -and $CustomRule.CrossColumnDependency) { $CustomRule.CrossColumnDependency } else { $null }
        if ($depCol -and $RowContext -and $RowContext.ContainsKey($depCol)) {
            $params['ContextColumn'] = $depCol
            $params['ContextValue'] = [string]$RowContext[$depCol]
        }
    }

    & $gen.Function @params
}