Functions/GenXdev.Coding.PowerShell.Modules/New-ModuleXmlHelp.ps1

###############################################################################
<#
.SYNOPSIS
Generates MAML XML help files for any PowerShell module.

.DESCRIPTION
Generates MAML XML help files for any PowerShell module by extracting
metadata from all cmdlets in a module using Get-CmdletMetaData and
producing both ModuleName.dll-Help.xml (for C# cmdlets) and
ModuleName-help.xml (for .ps1 script cmdlets) in the output directory
under <ModuleRoot>\<Language>\.

Each help file includes all 9 MAML sections: command details,
description, syntax, parameters, input types, return values, alerts,
examples, and related links. Works with any module that has
discoverable cmdlets — GenXdev modules, community modules, or your
own custom modules.

Cmdlet sources are resolved using the same two-path strategy as
Get-CmdletMetaData:

- Script cmdlets (.ps1): source files under
  Functions\<(Sub)ModuleName>\FileName.ps1, dot-sourced by .psm1 files
  via . "$PSScriptRoot\Functions\<(Sub)ModuleName>\<FileName>".
- Compiled cmdlets (.cs / .dll): identified by their
  [Cmdlet("Verb", "Noun")] attribute and assigned to sub-modules via
  ImplementationType.Namespace (e.g., GenXdev.FileSystem). C# files
  are named Verb-Noun.cs or Verb-Noun.Cmdlet.cs (the .Cmdlet suffix
  is for partial classes sharing a source file with helper types).
  Help text lives in a [System.ComponentModel.Description(@"...")]
  attribute using the same .KEYWORD syntax as comment-based help
  (.SYNOPSIS, .DESCRIPTION, .PARAMETER, .EXAMPLE, .NOTES,
  .LINK). The  .EXAMPLE keyword uses fenced ```powershell blocks
  (description below the fence) vs. unfenced code in .ps1 help.

To see how a C# cmdlet receives its metadata through attributes only,
visit:
  https://github.com/genXdev/GenXdev.PowerShell/blob/main/Functions/GenXdev.FileSystem/Find-Item.Cmdlet.cs

.LICENSE
Copyright (C) 2026 René Vaessen / GenXdev

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.

.PARAMETER ModuleName
The name of the PowerShell module to generate help for (e.g.,
'Microsoft.WinGet.Client', 'PSReadLine', 'GenXdev.AI').

.PARAMETER Language
The language code for the generated help files. Defaults to 'en' for English.
Uses the language dictionary from Get-WebLanguageDictionary for validation.
Supports 140+ languages.

.PARAMETER Force
Overwrite existing help XML files without prompting.

.PARAMETER SkipTranslation
Skip LLM-based translation even when Language is not 'en'. Help content
remains in the source language (English).

.PARAMETER Model
The model identifier or pattern to use for AI translations.

.PARAMETER ApiEndpoint
The API endpoint URL for AI translations.

.PARAMETER ApiKey
The API key for authenticated AI operations.

.PARAMETER PromptForSettings
When specified, launches an interactive prompt to configure the LLM settings

.PARAMETER LinkPrefix
URL prefix for the \"Online Version:\" link in each cmdlet's related links.
When specified, each cmdlet's -Online URL will be
${LinkPrefix}<CmdletName>.md (e.g.,
`https://github.com/genXdev/GenXdev.PowerShell/blob/main/Docs/en-US/` becomes
`https://.../Docs/en-US/Find-Item.md`). When omitted, a placeholder URL
is used for the Online Version link.

.PARAMETER WhatIf
Shows what would happen if the cmdlet runs without actually making changes.

.PARAMETER Confirm
Prompts for confirmation before making changes.

.EXAMPLE
New-ModuleXmlHelp -ModuleName 'Microsoft.WinGet.Client'

Generates English help XML for the WinGet client module in en-US\.

.EXAMPLE
New-ModuleXmlHelp -ModuleName 'GenXdev' -Language 'nl-NL' -Force -Model 'deepseek-v4-pro' -ApiKey 'your-api-key' -ApiEndpoint 'https://api.deepseek.com/chat/completions'

Generates Dutch help XML for GenXdev, overwriting existing files.

.EXAMPLE
New-ModuleXmlHelp -ModuleName 'PSReadLine' -WhatIf

Shows what files would be created without actually writing them.

.EXAMPLE
New-ModuleXmlHelp -ModuleName 'GenXdev' -LinkPrefix `
    'https://github.com/genXdev/GenXdev.PowerShell/blob/main/Docs/en-US/' -Force

Generates English help XML with per-cmdlet Online Version URLs pointing
to GitHub Markdown docs. Get-Help <cmdlet> -Online opens the browser.
#>

function New-ModuleXmlHelp {
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
    [OutputType([System.IO.FileInfo[]])]
    param (
        #######################################################################
        [Parameter(
            Position = 0,
            Mandatory = $true,
            HelpMessage = 'The name of the PowerShell module to generate help for'
        )]
        [ValidateScript({
                $m = Microsoft.PowerShell.Core\Get-Module -Name $_ `
                    -ErrorAction SilentlyContinue
                if (-not $m) {
                    $m = Microsoft.PowerShell.Core\Get-Module -Name $_ `
                        -ListAvailable -ErrorAction SilentlyContinue
                }
                if (-not $m) {
                    throw "Module '$_' not found. " +
                    'Provide a valid module name.'
                }
                $true
            })]
        [string] $ModuleName,
        #######################################################################
        [ValidateSet(
            'ar-SA',
            'bg-BG',
            'bn-IN',
            'ca-ES',
            'cs-CZ',
            'da-DK',
            'de-DE',
            'el-GR',
            'en-US',
            'es-ES',
            'et-EE',
            'fi-FI',
            'fr-FR',
            'gu-IN',
            'he-IL',
            'hi-IN',
            'hr-HR',
            'hu-HU',
            'id-ID',
            'it-IT',
            'ja-JP',
            'kn-IN',
            'ko-KR',
            'lt-LT',
            'lv-LV',
            'ml-IN',
            'mr-IN',
            'ms-MY',
            'nb-NO',
            'nl-NL',
            'pa-IN',
            'pl-PL',
            'pt-BR',
            'ro-RO',
            'ru-RU',
            'sk-SK',
            'sl-SI',
            'sr-RS',
            'sv-SE',
            'ta-IN',
            'te-IN',
            'th-TH',
            'tr-TR',
            'uk-UA',
            'vi-VN',
            'zh-Hans-CN'
        )]
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'BCP 47 language tag for the generated help ' +
            '(e.g., en-US, nl-NL, de-DE)'
        )]
        [string] $Language = 'en-US',
        #######################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Overwrite existing help XML files without prompting'
        )]
        [switch] $Force,
        #######################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Skip LLM translation; keep help in source language'
        )]
        [switch] $SkipTranslation,
        #######################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('The model identifier or pattern to use for AI translations')
        )]
        [string] $Model,
        ###########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'The API endpoint URL for AI translations'
        )]
        [string] $ApiEndpoint,
        ###########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'The API key for authenticated AI translations'
        )]
        [string] $ApiKey,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Launch interactive prompt to configure LLM settings')
        )]
        [switch] $PromptForSettings,
        ###########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Indicates that LLM has no support for JSON schemas'
        )]
        [switch] $NoSupportForJsonSchema,
        ###########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('URL prefix for the Online Version link in each ' +
                'cmdlet related links (e.g., ' +
                'https://github.com/genXdev/GenXdev.PowerShell/blob/main/Docs/en-US/)')
        )]
        [string] $LinkPrefix,
        ###########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Custom instructions for the LLM translation ' +
            '(passed to Get-TextTranslation)'
        )]
        [string] $TranslationInstructions

    )

    begin {
        ###############################################################################
        #region MAML XML Generation
        function GenerateMamlXml {
            param(
                [array]$CmdletMetaList,
                [string]$LinkPrefix = '',
                [hashtable]$SubModuleLinkMap = $null
            )

            $ns = @{
                maml    = 'http://schemas.microsoft.com/maml/2004/10'
                command = 'http://schemas.microsoft.com/maml/dev/command/2004/10'
                dev     = 'http://schemas.microsoft.com/maml/dev/2004/10'
                MSHelp  = 'http://msdn.microsoft.com/mshelp'
            }

            # Build a lookup: cmdlet name -> sub-module name
            $cmdletToSubModule = @{}
            if ($SubModuleLinkMap) {
                foreach ($subModule in $SubModuleLinkMap.Keys) {
                    foreach ($cmdletName in $SubModuleLinkMap[$subModule]) {
                        $cmdletToSubModule[$cmdletName] = $subModule
                    }
                }
            }

            # Use XmlWriter for precise formatting
            $settings = [System.Xml.XmlWriterSettings]::new()
            $settings.Indent = $true
            $settings.IndentChars = ' '
            $settings.Encoding = [System.Text.Encoding]::UTF8
            $settings.OmitXmlDeclaration = $false
            $settings.ConformanceLevel = [System.Xml.ConformanceLevel]::Document

            $stream = [System.IO.MemoryStream]::new()
            $writer = [System.Xml.XmlWriter]::Create($stream, $settings)

            $writer.WriteStartDocument()
            $writer.WriteStartElement('helpItems', 'http://msh')
            $writer.WriteAttributeString('schema', 'maml')
            $writer.WriteAttributeString('xmlns', 'http://msh')

            $i = 0;
            foreach ($meta in $CmdletMetaList) {
                Microsoft.PowerShell.Utility\Write-Progress -Id 426 -Activity "Generating XML Help" -Status "Processing $($meta.CmdletName)" -PercentComplete ([math]::Round((($i + 1) / $CmdletMetaList.Count) * 100, 2))
                $i++;

                $subModule = $cmdletToSubModule[$meta.CmdletName]
                $siblingCmdlets = if ($subModule -and $SubModuleLinkMap.ContainsKey($subModule)) {
                    @($SubModuleLinkMap[$subModule] |
                            Microsoft.PowerShell.Core\Where-Object { $_ -ne $meta.CmdletName } |
                            Microsoft.PowerShell.Utility\Sort-Object)
                }
                else { @() }

                WriteCmdletCommandElement -Writer $writer -Meta $meta -Ns $ns `
                    -SiblingCmdlets $siblingCmdlets -LinkPrefix $LinkPrefix
            }

            $writer.WriteEndElement() # helpItems
            $writer.WriteEndDocument()
            $writer.Flush()

            # Reset stream and load as XDocument
            $stream.Position = 0
            $reader = [System.IO.StreamReader]::new($stream)
            $xml = $reader.ReadToEnd()
            $reader.Dispose()
            $writer.Dispose()
            $stream.Dispose()

            return [xml]$xml
        }

        function WriteCmdletCommandElement {
            param(
                [System.Xml.XmlWriter]$Writer,
                $Meta,
                [hashtable]$Ns,
                [string[]]$SiblingCmdlets = @(),
                [string]$LinkPrefix = ''
            )

            # Derive verb and noun from cmdlet name
            $dashPos = $Meta.CmdletName.IndexOf('-')
            $verb = if ($dashPos -gt 0) { $Meta.CmdletName.Substring(0, $dashPos) } else { $Meta.CmdletName }
            $noun = if ($dashPos -gt 0) { $Meta.CmdletName.Substring($dashPos + 1) } else { $Meta.CmdletName }

            $Writer.WriteStartElement('command', 'command', $Ns['command'])
            $Writer.WriteAttributeString('xmlns', 'maml', $null, $Ns['maml'])
            $Writer.WriteAttributeString('xmlns', 'command', $null, $Ns['command'])
            $Writer.WriteAttributeString('xmlns', 'dev', $null, $Ns['dev'])
            $Writer.WriteAttributeString('xmlns', 'MSHelp', $null, $Ns['MSHelp'])

            WriteDetailsElement -Writer $Writer -CmdletName $Meta.CmdletName -Verb $verb -Noun $noun `
                -Synopsis $Meta.Synopsis -Ns $Ns

            WriteFullDescriptionElement -Writer $Writer -Description $Meta.Description -Ns $Ns

            WriteSyntaxElement -Writer $Writer -CmdletName $Meta.CmdletName -Parameters $Meta.Parameters -Ns $Ns

            WriteParametersElement -Writer $Writer -Parameters $Meta.Parameters -Ns $Ns

            WriteInputTypesElement -Writer $Writer -Parameters $Meta.Parameters -Ns $Ns

            WriteReturnValuesElement -Writer $Writer -OutputType $Meta.OutputType -Ns $Ns

            WriteExamplesElement -Writer $Writer -Examples $Meta.Examples -Ns $Ns

            WriteRelatedLinksElement -Writer $writer -CmdletName $Meta.CmdletName `
                -Ns $ns -SiblingCmdlets $SiblingCmdlets -LinkPrefix $LinkPrefix

            $Writer.WriteEndElement() # command:command
        }
        ###############################################################################
        #region Section Writers

        function WriteDetailsElement {
            param(
                [System.Xml.XmlWriter]$Writer,
                [string]$CmdletName, [string]$Verb, [string]$Noun,
                [string]$Synopsis, [hashtable]$Ns
            )
            $Writer.WriteStartElement('command', 'details', $Ns['command'])
            $Writer.WriteElementString('command', 'name', $Ns['command'], $CmdletName)
            $Writer.WriteElementString('command', 'verb', $Ns['command'], $Verb)
            $Writer.WriteElementString('command', 'noun', $Ns['command'], $Noun)
            $Writer.WriteStartElement('maml', 'description', $Ns['maml'])
            $Writer.WriteElementString('maml', 'para', $Ns['maml'], (Get-SynopsisText $Synopsis))
            $Writer.WriteEndElement() # maml:description
            $Writer.WriteEndElement() # command:details
        }

        function WriteFullDescriptionElement {
            param(
                [System.Xml.XmlWriter]$Writer,
                [string]$Description, [hashtable]$Ns
            )

            $Writer.WriteStartElement('maml', 'description', $Ns['maml'])

            $text = if ([string]::IsNullOrWhiteSpace($Description)) {
                '{{ Fill in the Description }}'
            }
            else { $Description }

            # Split description into paragraphs on double-newlines or sentences
            $paras = $text -split '\r?\n\s*\r?\n' | Microsoft.PowerShell.Core\Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
            if ($paras.Count -eq 0) {
                $Writer.WriteElementString('maml', 'para', $Ns['maml'], $text)
            }
            else {
                foreach ($p in $paras) {
                    $Writer.WriteElementString('maml', 'para', $Ns['maml'], $p.Trim())
                }
            }

            $Writer.WriteEndElement() # maml:description
        }

        function WriteSyntaxElement {
            param(
                [System.Xml.XmlWriter]$Writer,
                [string]$CmdletName,
                [array]$Parameters,
                [hashtable]$Ns
            )

            $Writer.WriteStartElement('command', 'syntax', $Ns['command'])
            $Writer.WriteStartElement('command', 'syntaxItem', $Ns['command'])
            $Writer.WriteElementString('maml', 'name', $Ns['maml'], $CmdletName)

            if ($Parameters -and $Parameters.Count -gt 0) {
                # Group by ParameterSetName; if all are same set, just list all
                $sortedParams = $Parameters |
                    Microsoft.PowerShell.Utility\Sort-Object { if ($_.Position -match '^\d+$') { [int]$_.Position } else { 2147483647 } },
                    @{ Expression = 'Name'; Descending = $false }

                foreach ($p in $sortedParams) {
                    WriteSyntaxParameterElement -Writer $Writer -Param $p -Ns $Ns
                }
            }

            $Writer.WriteEndElement() # command:syntaxItem
            $Writer.WriteEndElement() # command:syntax
        }

        function WriteSyntaxParameterElement {
            param(
                [System.Xml.XmlWriter]$Writer,
                $Param, [hashtable]$Ns
            )

            $aliasesText = if ($Param.Aliases -and $Param.Aliases.Count -gt 0) {
                ($Param.Aliases -join ', ')
            }
            else { 'none' }

            $pipelineText = if ($Param.ValueFromPipeline) {
                if ($Param.ValueFromPipelineByPropertyName) { 'True (ByValue, ByPropertyName)' }
                else { 'True (ByValue)' }
            }
            elseif ($Param.ValueFromPipelineByPropertyName) {
                'True (ByPropertyName)'
            }
            else { 'False' }

            $pos = if ($Param.Position -match '^\d+$') { $Param.Position } else { 'named' }
            $required = if ($Param.Mandatory) { 'true' } else { 'false' }
            $varLen = if ($Param.ValueFromRemainingArguments) { 'true' } else { 'true' }
            $globbing = if ($Param.SupportsWildcards) { 'true' } else { 'false' }

            $Writer.WriteStartElement('command', 'parameter', $Ns['command'])
            $Writer.WriteAttributeString('required', $required)
            $Writer.WriteAttributeString('variableLength', $varLen)
            $Writer.WriteAttributeString('globbing', $globbing)
            $Writer.WriteAttributeString('pipelineInput', $pipelineText)
            $Writer.WriteAttributeString('position', $pos)
            $Writer.WriteAttributeString('aliases', $aliasesText)

            $Writer.WriteElementString('maml', 'name', $Ns['maml'], $Param.Name)
            $Writer.WriteStartElement('maml', 'description', $Ns['maml'])
            $Writer.WriteElementString('maml', 'para', $Ns['maml'],
                (Get-ParamDescription $Param))
            $Writer.WriteEndElement() # maml:description

            # parameterValue (only for non-switch with ValueFromRemainingArguments or
            # for mandatory non-switch params)
            if ($Param.ParameterType -ne 'System.Management.Automation.SwitchParameter') {
                $paramValueType = Get-ParameterValueTypeName $Param.ParameterType
                $Writer.WriteStartElement('command', 'parameterValue', $Ns['command'])
                $Writer.WriteAttributeString('required', 'true')
                $Writer.WriteAttributeString('variableLength', 'false')
                $Writer.WriteString($paramValueType)
                $Writer.WriteEndElement() # command:parameterValue
            }

            # dev:type
            $Writer.WriteStartElement('dev', 'type', $Ns['dev'])
            $Writer.WriteElementString('maml', 'name', $Ns['maml'],
                (Get-MamlTypeName $Param.ParameterType))
            $Writer.WriteElementString('maml', 'uri', $Ns['maml'], '')
            $Writer.WriteEndElement() # dev:type

            # dev:defaultValue
            $defVal = if ($null -ne $Param.DefaultValue -and $Param.DefaultValue -ne '') {
                $Param.DefaultValue
            }
            elseif ($Param.ParameterType -eq 'System.Management.Automation.SwitchParameter') {
                'False'
            }
            else { 'None' }
            $Writer.WriteElementString('dev', 'defaultValue', $Ns['dev'], $defVal)

            $Writer.WriteEndElement() # command:parameter
        }

        function WriteParametersElement {
            param(
                [System.Xml.XmlWriter]$Writer,
                [array]$Parameters,
                [hashtable]$Ns
            )

            $Writer.WriteStartElement('command', 'parameters', $Ns['command'])

            if ($Parameters -and $Parameters.Count -gt 0) {
                $sortedParams = $Parameters |
                    Microsoft.PowerShell.Utility\Sort-Object { if ($_.Position -match '^\d+$') { [int]$_.Position } else { 2147483647 } },
                    @{ Expression = 'Name'; Descending = $false }

                foreach ($p in $sortedParams) {
                    WriteParameterDetailElement -Writer $Writer -Param $p -Ns $Ns
                }
            }

            $Writer.WriteEndElement() # command:parameters
        }

        function WriteParameterDetailElement {
            param(
                [System.Xml.XmlWriter]$Writer,
                $Param, [hashtable]$Ns
            )

            $aliasesText = if ($Param.Aliases -and $Param.Aliases.Count -gt 0) {
                ($Param.Aliases -join ', ')
            }
            else { 'none' }

            $pipelineText = if ($Param.ValueFromPipeline) {
                if ($Param.ValueFromPipelineByPropertyName) { 'True (ByValue, ByPropertyName)' }
                else { 'True (ByValue)' }
            }
            elseif ($Param.ValueFromPipelineByPropertyName) {
                'True (ByPropertyName)'
            }
            else { 'False' }

            $pos = if ($Param.Position -match '^\d+$') { $Param.Position } else { 'named' }
            $required = if ($Param.Mandatory) { 'true' } else { 'false' }
            $varLen = if ($Param.ValueFromRemainingArguments) { 'true' } else { 'true' }
            $globbing = if ($Param.SupportsWildcards) { 'true' } else { 'false' }

            $Writer.WriteStartElement('command', 'parameter', $Ns['command'])
            $Writer.WriteAttributeString('required', $required)
            $Writer.WriteAttributeString('variableLength', $varLen)
            $Writer.WriteAttributeString('globbing', $globbing)
            $Writer.WriteAttributeString('pipelineInput', $pipelineText)
            $Writer.WriteAttributeString('position', $pos)
            $Writer.WriteAttributeString('aliases', $aliasesText)

            $Writer.WriteElementString('maml', 'name', $Ns['maml'], $Param.Name)
            $Writer.WriteStartElement('maml', 'description', $Ns['maml'])
            $Writer.WriteElementString('maml', 'para', $Ns['maml'],
                (Get-ParamDescription $Param))
            $Writer.WriteEndElement() # maml:description

            # parameterValue
            if ($Param.ParameterType -ne 'System.Management.Automation.SwitchParameter') {
                $paramValueType = Get-ParameterValueTypeName $Param.ParameterType
                $Writer.WriteStartElement('command', 'parameterValue', $Ns['command'])
                $Writer.WriteAttributeString('required', 'true')
                $Writer.WriteAttributeString('variableLength', 'false')
                $Writer.WriteString($paramValueType)
                $Writer.WriteEndElement() # command:parameterValue
            }
            else {
                $Writer.WriteStartElement('command', 'parameterValue', $Ns['command'])
                $Writer.WriteAttributeString('required', 'false')
                $Writer.WriteAttributeString('variableLength', 'false')
                $Writer.WriteString('SwitchParameter')
                $Writer.WriteEndElement() # command:parameterValue
            }

            # dev:type
            $Writer.WriteStartElement('dev', 'type', $Ns['dev'])
            $Writer.WriteElementString('maml', 'name', $Ns['maml'],
                (Get-MamlTypeName $Param.ParameterType))
            $Writer.WriteElementString('maml', 'uri', $Ns['maml'], '')
            $Writer.WriteEndElement() # dev:type

            # dev:defaultValue
            $defVal = if ($null -ne $Param.DefaultValue -and $Param.DefaultValue -ne '') {
                $Param.DefaultValue
            }
            elseif ($Param.ParameterType -eq 'System.Management.Automation.SwitchParameter') {
                'False'
            }
            else { 'None' }
            $Writer.WriteElementString('dev', 'defaultValue', $Ns['dev'], $defVal)

            $Writer.WriteEndElement() # command:parameter
        }

        function WriteInputTypesElement {
            param(
                [System.Xml.XmlWriter]$Writer,
                [array]$Parameters,
                [hashtable]$Ns
            )

            $Writer.WriteStartElement('command', 'inputTypes', $Ns['command'])

            # Collect types that accept pipeline input
            $pipelineParams = @($Parameters | Microsoft.PowerShell.Core\Where-Object {
                    $_.ValueFromPipeline -or $_.ValueFromPipelineByPropertyName
                })

            if ($pipelineParams.Count -eq 0) {
                # No pipeline input
                $Writer.WriteStartElement('command', 'inputType', $Ns['command'])
                $Writer.WriteStartElement('dev', 'type', $Ns['dev'])
                $Writer.WriteElementString('maml', 'name', $Ns['maml'], 'None')
                $Writer.WriteEndElement() # dev:type
                $Writer.WriteStartElement('maml', 'description', $Ns['maml'])
                $Writer.WriteElementString('maml', 'para', $Ns['maml'],
                    "You can't pipe input to this cmdlet.")
                $Writer.WriteEndElement() # maml:description
                $Writer.WriteEndElement() # command:inputType
            }
            else {
                foreach ($pp in ($pipelineParams | Microsoft.PowerShell.Utility\Sort-Object ParameterType -Unique)) {
                    $Writer.WriteStartElement('command', 'inputType', $Ns['command'])
                    $Writer.WriteStartElement('dev', 'type', $Ns['dev'])
                    $Writer.WriteElementString('maml', 'name', $Ns['maml'], $pp.ParameterType)
                    $Writer.WriteEndElement() # dev:type
                    $Writer.WriteStartElement('maml', 'description', $Ns['maml'])
                    $Writer.WriteElementString('maml', 'para', $Ns['maml'],
                        "Accepts $($pp.ParameterType) objects via pipeline.")
                    $Writer.WriteEndElement() # maml:description
                    $Writer.WriteEndElement() # command:inputType
                }
            }

            $Writer.WriteEndElement() # command:inputTypes
        }

        function WriteReturnValuesElement {
            param(
                [System.Xml.XmlWriter]$Writer,
                [array]$OutputType,
                [hashtable]$Ns
            )

            $Writer.WriteStartElement('command', 'returnValues', $Ns['command'])

            if ($OutputType -and $OutputType.Count -gt 0) {
                foreach ($ot in $OutputType) {
                    $Writer.WriteStartElement('command', 'returnValue', $Ns['command'])
                    $Writer.WriteStartElement('dev', 'type', $Ns['dev'])
                    $Writer.WriteElementString('maml', 'name', $Ns['maml'], $ot)
                    $Writer.WriteEndElement() # dev:type
                    $Writer.WriteStartElement('maml', 'description', $Ns['maml'])
                    $Writer.WriteElementString('maml', 'para', $Ns['maml'],
                        "Returns $ot objects.")
                    $Writer.WriteEndElement() # maml:description
                    $Writer.WriteEndElement() # command:returnValue
                }
            }
            else {
                $Writer.WriteStartElement('command', 'returnValue', $Ns['command'])
                $Writer.WriteStartElement('dev', 'type', $Ns['dev'])
                $Writer.WriteElementString('maml', 'name', $Ns['maml'], 'None')
                $Writer.WriteEndElement() # dev:type
                $Writer.WriteStartElement('maml', 'description', $Ns['maml'])
                $Writer.WriteElementString('maml', 'para', $Ns['maml'], '')
                $Writer.WriteEndElement() # maml:description
                $Writer.WriteEndElement() # command:returnValue
            }

            $Writer.WriteEndElement() # command:returnValues
        }

        function WriteExamplesElement {
            param(
                [System.Xml.XmlWriter]$Writer,
                [array]$Examples,
                [hashtable]$Ns
            )

            $Writer.WriteStartElement('command', 'examples', $Ns['command'])

            if ($Examples -and $Examples.Count -gt 0) {
                $exNum = 1
                foreach ($ex in $Examples) {
                    $exampleText = "$ex"

                    # Parse fenced code block: ```powershell...``` is code,
                    # first non-blank line after ``` is title, rest is remarks.
                    $code = ''
                    $title = "---------- Example ${exNum} ----------"
                    $remarks = ''

                    if ($exampleText -match '(?s)```powershell\s*\r?\n(.*?)```') {
                        $code = $matches[1].TrimEnd()
                        $afterFence = $exampleText.Substring(
                            $exampleText.IndexOf($matches[0]) + $matches[0].Length).Trim()

                        if ($afterFence) {
                            $afterLines = $afterFence -split '\r?\n' |
                                Microsoft.PowerShell.Core\ForEach-Object { $_.TrimEnd() }
                            # First non-blank line is title
                            $titleIndex = -1
                            for ($i = 0; $i -lt $afterLines.Count; $i++) {
                                if ($afterLines[$i] -ne '') {
                                    $titleIndex = $i
                                    $title = $afterLines[$i]
                                    break
                                }
                            }
                            # Remaining lines are remarks
                            if ($titleIndex -ge 0 -and $titleIndex -lt $afterLines.Count - 1) {
                                $remarks = ($afterLines[($titleIndex + 1)..($afterLines.Count - 1)] |
                                        Microsoft.PowerShell.Core\Where-Object { $_ -ne '' }) -join ' '
                            }
                        }
                    }
                    else {
                        # No fenced code block — use the entire text as code
                        $code = $exampleText
                    }

                    $Writer.WriteStartElement('command', 'example', $Ns['command'])
                    $Writer.WriteElementString('maml', 'title', $Ns['maml'], $title)
                    $Writer.WriteElementString('dev', 'code', $Ns['dev'], $code)
                    $Writer.WriteStartElement('dev', 'remarks', $Ns['dev'])
                    $Writer.WriteElementString('maml', 'para', $Ns['maml'], $remarks)
                    $Writer.WriteEndElement() # dev:remarks
                    $Writer.WriteEndElement() # command:example

                    $exNum++
                }
            }
            else {
                # Placeholder example
                $Writer.WriteStartElement('command', 'example', $Ns['command'])
                $Writer.WriteElementString('maml', 'title', $Ns['maml'],
                    '-------------------------- Example 1 --------------------------')
                $Writer.WriteElementString('dev', 'code', $Ns['dev'],
                    'PS C:\> {{ Add example code here }}')
                $Writer.WriteStartElement('dev', 'remarks', $Ns['dev'])
                $Writer.WriteElementString('maml', 'para', $Ns['maml'],
                    '{{ Add example description here }}')
                $Writer.WriteEndElement() # dev:remarks
                $Writer.WriteEndElement() # command:example
            }

            $Writer.WriteEndElement() # command:examples
        }

        function WriteRelatedLinksElement {
            param(
                [System.Xml.XmlWriter]$Writer,
                [string]$CmdletName,
                [hashtable]$Ns,
                [string[]]$SiblingCmdlets = @(),
                [string]$LinkPrefix = ''
            )

            $Writer.WriteStartElement('command', 'relatedLinks', $Ns['command'])

            # 1. "Online Version:" link for Get-Help -Online
            $Writer.WriteStartElement('maml', 'navigationLink', $Ns['maml'])
            $Writer.WriteElementString('maml', 'linkText', $Ns['maml'], 'Online Version:')
            $onlineUri = if ($LinkPrefix) {
                "${LinkPrefix}${CmdletName}.md"
            }
            else {
                "https://docs.github.com/en/rest/search"
            }
            $Writer.WriteElementString('maml', 'uri', $Ns['maml'], $onlineUri)
            $Writer.WriteEndElement() # maml:navigationLink

            # 2. Sibling cmdlet cross-references (empty URIs, PowerShell convention)
            foreach ($sibling in $SiblingCmdlets) {
                $Writer.WriteStartElement('maml', 'navigationLink', $Ns['maml'])
                $Writer.WriteElementString('maml', 'linkText', $Ns['maml'], $sibling)
                $Writer.WriteElementString('maml', 'uri', $Ns['maml'], '')
                $Writer.WriteEndElement() # maml:navigationLink
            }

            $Writer.WriteEndElement() # command:relatedLinks
        }
        #endregion Section Writers
        ###############################################################################
        #region Helper Functions

        function Get-SynopsisText {
            param([string]$Synopsis)

            if ([string]::IsNullOrWhiteSpace($Synopsis)) {
                return ''
            }
            return $Synopsis
        }

        function Get-ParamDescription {
            param($Param)

            if (-not [string]::IsNullOrWhiteSpace($Param.HelpMessage)) {
                return $Param.HelpMessage
            }
            return "The $($Param.Name) parameter."
        }

        function Get-MamlTypeName {
            param([string]$ParameterType)

            switch -Regex ($ParameterType) {
                'System\.String\[\]' { return 'String[]' }
                'System\.String' { return 'String' }
                'System\.Int32' { return 'Int32' }
                'System\.Int64' { return 'Int64' }
                'System\.Boolean' { return 'Boolean' }
                'System\.Double' { return 'Double' }
                'System\.DateTime' { return 'DateTime' }
                'System\.Object' { return 'Object' }
                'System\.Object\[\]' { return 'Object[]' }
                'Hashtable' { return 'Hashtable' }
                'Hashtable\[\]' { return 'Hashtable[]' }
                'SwitchParameter|System\.Management\.Automation\.SwitchParameter' { return 'SwitchParameter' }
                'System\.Collections\.IEnumerable' { return 'IEnumerable' }
                'System\.Char\[\]' { return 'Char[]' }
                'GenXdev\.Helpers\.' { return ($ParameterType -replace 'GenXdev\.Helpers\.', '') }
                default { return ($ParameterType -replace 'System\.', '') }
            }
        }

        function Get-ParameterValueTypeName {
            param([string]$ParameterType)

            switch -Regex ($ParameterType) {
                'System\.String\[\]' { return 'String[]' }
                'System\.String' { return 'String' }
                'System\.Int32' { return 'Int32' }
                'System\.Int64' { return 'Int64' }
                'System\.Boolean' { return 'Boolean' }
                'System\.Double' { return 'Double' }
                'System\.DateTime' { return 'DateTime' }
                'System\.Object' { return 'Object' }
                'System\.Object\[\]' { return 'Object[]' }
                'Hashtable' { return 'Hashtable' }
                'Hashtable\[\]' { return 'Hashtable[]' }
                'SwitchParameter|System\.Management\.Automation\.SwitchParameter' { return 'SwitchParameter' }
                'System\.Collections\.IEnumerable' { return 'IEnumerable' }
                'System\.Char\[\]' { return 'Char[]' }
                'GenXdev\.Helpers\.' { return ($ParameterType -replace 'GenXdev\.Helpers\.', '') }
                default { return ($ParameterType -replace 'System\.', '') }
            }
        }
        #endregion Helper Functions

        # -- Resolve module --------------------------------------------------
        # Prefer the actually-loaded module (has ExportedCommands).
        # Otherwise fall back to the available module with the highest
        # version number.
        $module = @(Microsoft.PowerShell.Core\Get-Module -Name $ModuleName `
                -ErrorAction SilentlyContinue |
                Microsoft.PowerShell.Core\Where-Object {
                    $_.ExportedCommands.Count -gt 0 -and
                    $_.ModuleType -ne 'Binary'
                })[0]

        if (-not $module) {
            $module = @(Microsoft.PowerShell.Core\Get-Module `
                    -Name $ModuleName -ListAvailable `
                    -ErrorAction SilentlyContinue |
                    Microsoft.PowerShell.Utility\Sort-Object Version -Descending)[0]
        }

        if (-not $module) {
            Microsoft.PowerShell.Utility\Write-Error `
                "Module '${ModuleName}' not found."
            return
        }

        $moduleRoot = $module.ModuleBase
        $helpDir = Microsoft.PowerShell.Management\Join-Path $moduleRoot $Language

        Microsoft.PowerShell.Utility\Write-Verbose "Module root: ${moduleRoot}"

        # -- Track generated files for pipeline output -----------------------
        $generatedFiles = [System.Collections.Generic.List[string]]::new()

        # -- Cache module-level metadata params ------------------------------
        $metaModuleParams = GenXdev\Copy-IdenticalParamValues `
            -BoundParameters $PSBoundParameters `
            -FunctionName "GenXdev\Get-ModuleCmdletMetaData" ;
    }

    process {
        # -- Collect metadata for all cmdlets --------------------------------
        if (-not $PSCmdlet.ShouldProcess(
                $ModuleName,
                'Retrieve cmdlet metadata for all cmdlets')) {
            return
        }

        try {
            $cmdletMeta = @(GenXdev\Get-ModuleCmdletMetaData `
                    @metaModuleParams -ErrorAction Stop)
        }
        catch {
            Microsoft.PowerShell.Utility\Write-Error (
                "Failed to get metadata for module '${ModuleName}': $_")
            return
        }

        if ($cmdletMeta.Count -eq 0) {
            Microsoft.PowerShell.Utility\Write-Warning "No cmdlet metadata retrieved for '${ModuleName}'."
            return
        }

        Microsoft.PowerShell.Utility\Write-Verbose "Retrieved metadata for $($cmdletMeta.Count) cmdlet(s)."

        # -- Build sub-module link map for related links --------------------
        $subModuleLinkMap = @{}
        foreach ($meta in $cmdletMeta) {
            $subModule = if ($meta.SubModuleName) { $meta.SubModuleName } else { 'General' }
            if (-not $subModuleLinkMap.ContainsKey($subModule)) {
                $subModuleLinkMap[$subModule] = @()
            }
            $subModuleLinkMap[$subModule] += $meta.CmdletName
        }
        Microsoft.PowerShell.Utility\Write-Verbose (
            "Built link map with $($subModuleLinkMap.Count) sub-module(s).")

        # Separate into C# and script cmdlets based on CmdletType
        $csCmdlets = @($cmdletMeta | Microsoft.PowerShell.Core\Where-Object { $_.CmdletType -eq 'Cmdlet' })
        $scriptCmdlets = @($cmdletMeta | Microsoft.PowerShell.Core\Where-Object { $_.CmdletType -ne 'Cmdlet' })

        Microsoft.PowerShell.Utility\Write-Verbose "C# cmdlets: $($csCmdlets.Count), Script cmdlets: $($scriptCmdlets.Count)"

        # Generate dll-Help.xml for C# cmdlets
        if ($csCmdlets.Count -gt 0) {
            $dllHelpPath = Microsoft.PowerShell.Management\Join-Path $helpDir "${ModuleName}.dll-Help.xml"
            if ($PSCmdlet.ShouldProcess($dllHelpPath, 'Generate C# help XML')) {
                if ((Microsoft.PowerShell.Management\Test-Path $dllHelpPath -PathType Leaf) -and -not $Force) {
                    Microsoft.PowerShell.Utility\Write-Warning "File '${dllHelpPath}' exists. Use -Force to overwrite."
                }
                else {
                    $xml = GenerateMamlXml -CmdletMetaList $csCmdlets `
                        -LinkPrefix $LinkPrefix -SubModuleLinkMap $subModuleLinkMap
                    $null = Microsoft.PowerShell.Management\New-Item -ItemType Directory -Path $helpDir -Force -ErrorAction SilentlyContinue
                    $xml.Save($dllHelpPath)
                    $null = $generatedFiles.Add($dllHelpPath)
                    Microsoft.PowerShell.Utility\Write-Verbose "Generated: ${dllHelpPath}"
                }
            }
        }

        # Generate help.xml for .ps1 script cmdlets
        if ($scriptCmdlets.Count -gt 0) {
            $scriptHelpPath = Microsoft.PowerShell.Management\Join-Path $helpDir "${ModuleName}-help.xml"
            if ($PSCmdlet.ShouldProcess($scriptHelpPath, 'Generate script help XML')) {
                if ((Microsoft.PowerShell.Management\Test-Path $scriptHelpPath -PathType Leaf) -and -not $Force) {
                    Microsoft.PowerShell.Utility\Write-Warning "File '${scriptHelpPath}' exists. Use -Force to overwrite."
                }
                else {
                    $xml = GenerateMamlXml -CmdletMetaList $scriptCmdlets `
                        -LinkPrefix $LinkPrefix -SubModuleLinkMap $subModuleLinkMap
                    $null = Microsoft.PowerShell.Management\New-Item -ItemType Directory -Path $helpDir -Force -ErrorAction SilentlyContinue
                    $xml.Save($scriptHelpPath)
                    $null = $generatedFiles.Add($scriptHelpPath)
                    Microsoft.PowerShell.Utility\Write-Verbose "Generated: ${scriptHelpPath}"
                }
            }
        }

        if ($generatedFiles.Count -gt 0) { Microsoft.PowerShell.Management\Get-ChildItem $generatedFiles }
    }

    end {
        Microsoft.PowerShell.Utility\Write-Progress -Id 426 -Completed
    }
}