PSAliasFinder.psm1

# PSAliasFinder — PowerShell-side public surface.
# Backend (feedback provider + alias cache + config I/O) lives in the
# PSAliasFinder.dll loaded as RootModule.

function CountActualPipes {
    [CmdletBinding()]
    param([string]$Command)

    try {
        $ast = [System.Management.Automation.Language.Parser]::ParseInput(
            $Command, [ref]$null, [ref]$null)
        $pipelines = $ast.FindAll({
            param($node) $node -is [System.Management.Automation.Language.PipelineAst]
        }, $true)
        if ($pipelines.Count -gt 0) {
            return ($pipelines[0].PipelineElements.Count - 1)
        }
        return 0
    } catch {
        return 0
    }
}

function ShouldShowAliasSuggestion {
    [CmdletBinding()]
    param(
        [string]$OriginalCommand,
        [PSCustomObject]$Alias
    )

    $cfg = [PSAliasFinder.ProviderConfig]::Current
    $firstCommand = ($OriginalCommand -split '\|')[0].Trim()

    if ($firstCommand.Length -lt $cfg.MinCommandLength) { return $false }

    $pipeCount = CountActualPipes $OriginalCommand
    $argumentCount = ($OriginalCommand -split '\s+').Count
    if ($pipeCount -gt $cfg.MaxPipes -or $argumentCount -gt $cfg.MaxArguments) { return $false }

    $absoluteSaving = $firstCommand.Length - $Alias.Name.Length
    if ($absoluteSaving -lt $cfg.MinCharsSaved) { return $false }

    return $true
}

function Find-Alias {
    <#
    .SYNOPSIS
        Finds aliases for a given PowerShell command.
 
    .DESCRIPTION
        Explicit lookup surface preserved from 1.0.0. Independent of the IFeedbackProvider
        subsystem: searches the current session's alias table directly and applies the
        same default filter as the feedback provider (reading PSAliasFinder config for
        thresholds). Use -Force to bypass the filter.
 
    .PARAMETER Command
        The command to search aliases for. Accepts multiple tokens.
 
    .PARAMETER Exact
        Only exact matches.
 
    .PARAMETER Longer
        Include aliases whose definition contains the command (broader than Exact).
 
    .PARAMETER Cheaper
        Only aliases strictly shorter than the full command.
 
    .PARAMETER Quiet
        Suppress console output; emit objects only.
 
    .PARAMETER Force
        Bypass the default filter (MinCommandLength / MaxPipes / MaxArguments / MinCharsSaved).
 
    .EXAMPLE
        Find-Alias Get-ChildItem
    #>

    [CmdletBinding()]
    [Alias('af','alias-finder')]
    param (
        [Parameter(Mandatory=$true, ValueFromRemainingArguments=$true)]
        [string[]]$Command,
        [switch]$Exact,
        [switch]$Longer,
        [switch]$Cheaper,
        [switch]$Quiet,
        [switch]$Force
    )

    $fullCommand = ($Command -join ' ').Trim()
    if ([string]::IsNullOrWhiteSpace($fullCommand)) { return @() }

    $foundAliases = @()
    $currentCmd = $fullCommand

    while (-not [string]::IsNullOrWhiteSpace($currentCmd)) {
        $matchingAliases = Get-Alias | Where-Object {
            if ($Exact) {
                $_.Definition -eq $currentCmd
            } elseif ($Longer) {
                $_.Definition -like "*$currentCmd*"
            } else {
                $_.Definition -eq $currentCmd -or
                ($currentCmd.StartsWith($_.Definition) -and
                 $currentCmd.Length -gt $_.Definition.Length -and
                 $currentCmd[$_.Definition.Length] -match '\s')
            }
        } | ForEach-Object {
            [PSCustomObject]@{
                Name = $_.Name
                Definition = $_.Definition
            }
        }

        if ($Cheaper) {
            $matchingAliases = $matchingAliases | Where-Object {
                $_.Name.Length -lt $fullCommand.Length
            }
        }

        foreach ($alias in $matchingAliases) {
            if ($foundAliases.Name -notcontains $alias.Name) {
                $foundAliases += $alias
            }
        }

        if ($Exact -or $Longer) { break }

        $words = $currentCmd.Trim() -split '\s+'
        if ($words.Count -le 1) { break }
        $currentCmd = ($words[0..($words.Count-2)] -join ' ').Trim()
    }

    if (-not $Force) {
        $foundAliases = $foundAliases | Where-Object { ShouldShowAliasSuggestion $fullCommand $_ }
    }

    if (-not $Quiet -and $foundAliases.Count -gt 0) {
        foreach ($alias in $foundAliases) {
            Write-Host "$($alias.Name) -> $($alias.Definition)" -ForegroundColor Green
        }
    }

    return $foundAliases
}

function Get-AliasFinderConfig {
    <#
    .SYNOPSIS
        Returns the current PSAliasFinder configuration.
 
    .DESCRIPTION
        Emits the live ProviderConfig snapshot as a PSCustomObject, including the
        on-disk config file path. Reflects the state the feedback provider is
        actually using — after Set-AliasFinderConfig, the in-memory copy is
        updated without reimporting.
    #>

    [CmdletBinding()]
    param()

    $c = [PSAliasFinder.ProviderConfig]::Current
    [pscustomobject]@{
        Enabled          = $c.Enabled
        MinCommandLength = $c.MinCommandLength
        MaxPipes         = $c.MaxPipes
        MaxArguments     = $c.MaxArguments
        MinCharsSaved    = $c.MinCharsSaved
        CooldownSeconds  = $c.CooldownSeconds
        MaxSuggestions   = $c.MaxSuggestions
        IgnoredCommands  = $c.IgnoredCommands
        ConfigFile       = [PSAliasFinder.ProviderConfig]::ConfigFile
    }
}

function Set-AliasFinderConfig {
    <#
    .SYNOPSIS
        Updates and persists PSAliasFinder configuration.
 
    .DESCRIPTION
        Changes apply live: the JSON file at $env:APPDATA\PSAliasFinder\config.json
        is rewritten and the in-memory ProviderConfig.Current is swapped, so the
        feedback provider uses the new values on the next command without
        reimporting the module.
 
    .PARAMETER Reset
        Restore all defaults before applying any other parameters.
 
    .PARAMETER PassThru
        Emit the resulting config object after saving.
    #>

    [CmdletBinding()]
    param(
        [bool]$Enabled,
        [int]$MinCommandLength,
        [int]$MaxPipes,
        [int]$MaxArguments,
        [int]$MinCharsSaved,
        [int]$CooldownSeconds,
        [int]$MaxSuggestions,
        [string[]]$IgnoredCommands,
        [string[]]$AddIgnored,
        [string[]]$RemoveIgnored,
        [switch]$Reset,
        [switch]$PassThru
    )

    $current = [PSAliasFinder.ProviderConfig]::Current

    if ($Reset) {
        $config = [PSAliasFinder.ProviderConfig]::new()
    } else {
        $config = [PSAliasFinder.ProviderConfig]::new()
        $config.Enabled          = $current.Enabled
        $config.MinCommandLength = $current.MinCommandLength
        $config.MaxPipes         = $current.MaxPipes
        $config.MaxArguments     = $current.MaxArguments
        $config.MinCharsSaved    = $current.MinCharsSaved
        $config.CooldownSeconds  = $current.CooldownSeconds
        $config.MaxSuggestions   = $current.MaxSuggestions
        $config.IgnoredCommands  = $current.IgnoredCommands
    }

    if ($PSBoundParameters.ContainsKey('Enabled'))          { $config.Enabled          = $Enabled }
    if ($PSBoundParameters.ContainsKey('MinCommandLength')) { $config.MinCommandLength = $MinCommandLength }
    if ($PSBoundParameters.ContainsKey('MaxPipes'))         { $config.MaxPipes         = $MaxPipes }
    if ($PSBoundParameters.ContainsKey('MaxArguments'))     { $config.MaxArguments     = $MaxArguments }
    if ($PSBoundParameters.ContainsKey('MinCharsSaved'))    { $config.MinCharsSaved    = $MinCharsSaved }
    if ($PSBoundParameters.ContainsKey('CooldownSeconds'))  { $config.CooldownSeconds  = $CooldownSeconds }
    if ($PSBoundParameters.ContainsKey('MaxSuggestions'))   { $config.MaxSuggestions   = $MaxSuggestions }
    if ($PSBoundParameters.ContainsKey('IgnoredCommands'))  { $config.IgnoredCommands  = $IgnoredCommands }

    if ($PSBoundParameters.ContainsKey('AddIgnored')) {
        $merged = [System.Collections.Generic.HashSet[string]]::new(
            [string[]]$config.IgnoredCommands,
            [System.StringComparer]::OrdinalIgnoreCase)
        foreach ($item in $AddIgnored) { [void]$merged.Add($item) }
        $config.IgnoredCommands = [string[]]@($merged)
    }

    if ($PSBoundParameters.ContainsKey('RemoveIgnored')) {
        $toRemove = [System.Collections.Generic.HashSet[string]]::new(
            [string[]]$RemoveIgnored,
            [System.StringComparer]::OrdinalIgnoreCase)
        $config.IgnoredCommands = [string[]]@($config.IgnoredCommands | Where-Object { -not $toRemove.Contains($_) })
    }

    $config.Save()

    if ($PassThru) {
        Get-AliasFinderConfig
    }
}

Export-ModuleMember -Function Find-Alias, Get-AliasFinderConfig, Set-AliasFinderConfig -Alias af, alias-finder