internal/functions/utility/Resolve-SldgPromptTemplate.ps1

function Resolve-SldgPromptTemplate {
    <#
    .SYNOPSIS
        Resolves and renders a prompt template file with variable substitution.
    .DESCRIPTION
        Loads a .prompt template file for the given purpose, resolving the best
        variant match based on the active AI provider and configuration.

        Search order (first match wins):
          1. Custom path / {purpose}.{effectiveVariant}.prompt
          2. Custom path / {purpose}.default.prompt
          3. Built-in / {purpose}.{effectiveVariant}.prompt
          4. Built-in / {purpose}.default.prompt

        The effective variant is determined by:
          - AI.PromptVariant config (if not 'default')
          - Otherwise auto-detected from AI.Provider (AzureOpenAI maps to 'openai')

        Template files use YAML front matter for metadata and {{Variable}} placeholders
        in the body that are replaced from the -Variables hashtable.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param (
        [Parameter(Mandatory)]
        [string]$Purpose,

        [hashtable]$Variables = @{}
    )

    # Determine effective variant
    $configVariant = Get-PSFConfigValue -FullName 'SqlLabDataGenerator.AI.PromptVariant'
    if ($configVariant -and $configVariant -ne 'default') {
        $effectiveVariant = $configVariant
    }
    else {
        $provider = Get-PSFConfigValue -FullName 'SqlLabDataGenerator.AI.Provider'
        $effectiveVariant = switch ($provider) {
            'AzureOpenAI' { 'openai' }
            'OpenAI'      { 'openai' }
            'Ollama'      { 'ollama' }
            default       { 'default' }
        }
    }

    $builtInPath = Join-Path -Path (Join-Path -Path $script:ModuleRoot -ChildPath 'internal') -ChildPath 'prompts'
    $customPath = Get-PSFConfigValue -FullName 'SqlLabDataGenerator.AI.PromptPath'

    # Build search candidates in priority order
    $candidates = @()
    if ($customPath -and (Test-Path $customPath)) {
        $candidates += Join-Path $customPath "$Purpose.$effectiveVariant.prompt"
        if ($effectiveVariant -ne 'default') {
            $candidates += Join-Path $customPath "$Purpose.default.prompt"
        }
    }
    $candidates += Join-Path $builtInPath "$Purpose.$effectiveVariant.prompt"
    if ($effectiveVariant -ne 'default') {
        $candidates += Join-Path $builtInPath "$Purpose.default.prompt"
    }

    $resolvedFile = $null
    foreach ($candidate in $candidates) {
        if (Test-Path $candidate) {
            $resolvedFile = $candidate
            break
        }
    }

    if (-not $resolvedFile) {
        Write-PSFMessage -Level Warning -Message ($script:strings.'Prompt.TemplateNotFound' -f $Purpose, $effectiveVariant, ($candidates -join ', '))
        return $null
    }

    Write-PSFMessage -Level Verbose -Message ($script:strings.'Prompt.TemplateResolved' -f $resolvedFile)

    $raw = Get-Content -Path $resolvedFile -Raw -Encoding UTF8

    # Parse YAML front matter: strip the --- header block, keep the body
    $body = $raw
    if ($raw -match '^---\s*\r?\n([\s\S]*?)\r?\n---\s*\r?\n([\s\S]*)$') {
        $body = $Matches[2]
    }

    # Substitute {{Variable}} placeholders (use literal .Replace to avoid regex backreference injection via $0/$1/$&)
    foreach ($key in $Variables.Keys) {
        $val = $Variables[$key]
        $strVal = if ($val -is [hashtable] -or $val -is [System.Collections.IDictionary] -or $val -is [System.Array] -or $val -is [psobject] -and $val -isnot [string] -and $val -isnot [ValueType]) {
            $val | ConvertTo-Json -Depth 5 -Compress
        } else {
            [string]$val
        }
        $body = $body.Replace("{{$key}}", $strVal)
    }

    $body.TrimEnd()
}