Processors/UrlProcessor.ps1

# Module-level cache for control mappings
$script:ControlMappingsCache = $null
$script:ConfigPath = $null

function Initialize-UrlProcessor {
    <#
    .SYNOPSIS
        Initializes the URL processor with configuration.

    .DESCRIPTION
        Loads control URL mappings from JSON configuration file.

    .PARAMETER ConfigPath
        Path to the control-mappings.json configuration file.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$ConfigPath
    )

    if (-not (Test-Path $ConfigPath)) {
        throw "Control mappings configuration file not found: $ConfigPath"
    }

    try {
        $script:ConfigPath = $ConfigPath
        $configContent = Get-Content -Path $ConfigPath -Raw -Encoding UTF8
        $script:ControlMappingsCache = $configContent | ConvertFrom-Json
        Write-Verbose "Loaded control mappings from $ConfigPath"
    }
    catch {
        throw "Failed to load control mappings configuration: $_"
    }
}

function Get-ControlMapping {
    <#
    .SYNOPSIS
        Gets the URL mapping for a specific control name.

    .DESCRIPTION
        Searches through all category mappings to find a matching control.

    .PARAMETER ControlName
        Name of the control to find mapping for.

    .OUTPUTS
        String URL if mapping found, otherwise $null.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$ControlName
    )

    if (-not $script:ControlMappingsCache) {
        Write-Warning "URL processor not initialized. Call Initialize-UrlProcessor first."
        return $null
    }

    # Search through all category mappings
    foreach ($category in $script:ControlMappingsCache.controlMappings.PSObject.Properties) {
        foreach ($mapping in $category.Value.PSObject.Properties) {
            if ($ControlName -match [regex]::Escape($mapping.Name)) {
                Write-Verbose "Found exact mapping for '$ControlName': $($mapping.Value)"
                return $mapping.Value
            }
        }
    }

    return $null
}

function Get-FallbackUrl {
    <#
    .SYNOPSIS
        Gets a fallback URL based on control name keywords.

    .DESCRIPTION
        Analyzes control name for keywords and returns appropriate portal URL.

    .PARAMETER ControlName
        Name of the control.

    .OUTPUTS
        String URL for the appropriate portal.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$ControlName
    )

    if (-not $script:ControlMappingsCache) {
        return $null
    }

    # Check each fallback rule
    foreach ($rule in $script:ControlMappingsCache.fallbackRules.PSObject.Properties) {
        $keywords = $rule.Value.keywords
        $url = $rule.Value.url

        foreach ($keyword in $keywords) {
            if ($ControlName -match $keyword) {
                Write-Verbose "Using fallback URL for '$ControlName' based on keyword '$keyword': $url"
                return $url
            }
        }
    }

    return $null
}

function Update-LegacyPortalUrl {
    <#
    .SYNOPSIS
        Updates legacy portal URLs to current equivalents.

    .DESCRIPTION
        Replaces old portal URLs (portal.office.com, aad.portal.azure.com) with new ones.

    .PARAMETER Url
        URL to update.

    .OUTPUTS
        Updated URL string.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Url
    )

    if (-not $script:ControlMappingsCache) {
        return $Url
    }

    $updatedUrl = $Url

    # Apply URL replacements from config
    foreach ($replacement in $script:ControlMappingsCache.urlReplacements.PSObject.Properties) {
        $oldUrl = $replacement.Name
        $newUrl = $replacement.Value
        $updatedUrl = $updatedUrl -replace [regex]::Escape($oldUrl), $newUrl
    }

    # Additional Entra ID specific updates
    if ($updatedUrl -match 'Microsoft_AAD' -and $updatedUrl -notmatch 'entra\.microsoft\.com') {
        $updatedUrl = $updatedUrl -replace 'https://portal\.azure\.com', 'https://entra.microsoft.com'
    }

    return $updatedUrl
}

function Add-TenantContext {
    <#
    .SYNOPSIS
        Adds tenant ID context to a URL.

    .DESCRIPTION
        Injects tenant ID parameter into portal URLs for proper tenant scoping.

    .PARAMETER Url
        URL to add tenant context to.

    .PARAMETER TenantId
        Tenant identifier to inject.

    .OUTPUTS
        URL with tenant context added.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Url,

        [Parameter(Mandatory = $false)]
        [string]$TenantId
    )

    if ([string]::IsNullOrEmpty($TenantId)) {
        return $Url
    }

    $updatedUrl = $Url

    # Replace existing tenant ID if present
    if ($updatedUrl -match 'tid=') {
        $updatedUrl = $updatedUrl -replace 'tid=[a-f0-9-]+', "tid=$TenantId"
    }
    # Add tenant ID to Entra and Azure portal URLs that don't have it
    elseif ($updatedUrl -match '^https://(portal\.azure\.com|aad\.portal\.azure\.com|entra\.microsoft\.com)') {
        if ($updatedUrl -match '\?') {
            $updatedUrl = $updatedUrl -replace '\?', "?tid=$TenantId&"
        }
        elseif ($updatedUrl -match '#') {
            $updatedUrl = $updatedUrl -replace '#', "?tid=$TenantId#"
        }
        else {
            $updatedUrl += "?tid=$TenantId"
        }
    }

    return $updatedUrl
}

function Optimize-ControlUrl {
    <#
    .SYNOPSIS
        Optimizes and enhances a control's action URL.

    .DESCRIPTION
        Performs full URL optimization including:
        - Exact control mapping lookup
        - Documentation URL fallback routing
        - Legacy URL updates
        - Tenant context injection

    .PARAMETER Url
        Original URL from API.

    .PARAMETER ControlName
        Name of the control.

    .PARAMETER TenantId
        Tenant identifier for context injection.

    .OUTPUTS
        Optimized URL string.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)]
        [string]$Url,

        [Parameter(Mandatory = $true)]
        [string]$ControlName,

        [Parameter(Mandatory = $false)]
        [string]$TenantId
    )

    if ([string]::IsNullOrEmpty($Url)) {
        return $Url
    }

    $optimizedUrl = $Url

    # Step 1: Check for exact control mapping
    $exactMapping = Get-ControlMapping -ControlName $ControlName
    if ($exactMapping) {
        $optimizedUrl = $exactMapping
        Write-Verbose "Using exact mapping for '$ControlName'"
    }
    # Step 2: If URL points to documentation, find a better config URL
    elseif ($optimizedUrl -match 'learn\.microsoft\.com') {
        $fallbackUrl = Get-FallbackUrl -ControlName $ControlName
        if ($fallbackUrl) {
            $optimizedUrl = $fallbackUrl
            Write-Verbose "Replaced documentation URL with portal fallback for '$ControlName'"
        }
    }

    # Step 3: Update legacy portal URLs
    $optimizedUrl = Update-LegacyPortalUrl -Url $optimizedUrl

    # Step 4: Add tenant context
    $optimizedUrl = Add-TenantContext -Url $optimizedUrl -TenantId $TenantId

    return $optimizedUrl
}

function Test-UrlProcessorInitialized {
    <#
    .SYNOPSIS
        Checks if URL processor is initialized.

    .OUTPUTS
        Boolean indicating initialization status.
    #>

    [CmdletBinding()]
    param()

    return ($null -ne $script:ControlMappingsCache)
}

Export-ModuleMember -Function @(
    'Initialize-UrlProcessor',
    'Get-ControlMapping',
    'Get-FallbackUrl',
    'Update-LegacyPortalUrl',
    'Add-TenantContext',
    'Optimize-ControlUrl',
    'Test-UrlProcessorInitialized'
)