DebugURL.psm1

# DebugURL PowerShell Module
# Author: Naveed Khan
# Version: 1.0.4
# Description: Advanced URL debugging and testing module with comprehensive network analysis capabilities
# License: MIT License
# GitHub: https://github.com/khannaveed2020/DebugURL

#region Module Documentation
<#
.SYNOPSIS
    Advanced URL debugging and testing module with comprehensive network analysis capabilities.
 
.DESCRIPTION
    The DebugURL module provides comprehensive URL testing and debugging capabilities, including:
    - DNS resolution details
    - Request header information
    - SSL/TLS certificate details
    - Response headers
    - Content preview
    - Proxy support
    - Custom timeout settings
    - Certificate validation skip option
    - Custom user agent setting
    - HTTP methods support
    - Custom headers
    - Concurrent requests for performance testing
 
.NOTES
    Version: 1.0.4
    Author: Naveed Khan
    License: MIT License
    GitHub: https://github.com/khannaveed2020/DebugURL
 
.EXAMPLE
    DebugURL -URL "https://example.com"
    Performs a basic GET request to example.com and displays detailed information.
 
.EXAMPLE
    DebugURL -URL "https://example.com" -ConcurrentRequests 5
    Performs 5 concurrent requests to example.com for performance testing.
 
.EXAMPLE
    DebugURL -URL "https://example.com" -SkipCertCheck
    Performs a request while skipping SSL certificate validation.
 
.LINK
    https://github.com/khannaveed2020/DebugURL
#>

#endregion

# Add error handling for module loading
$ErrorActionPreference = 'Stop'
$WarningPreference = 'Continue'

function Format-Size {
    param (
        [long]$Bytes
    )

    $sizes = @('B', 'KB', 'MB', 'GB')
    $index = 0
    $size = $Bytes

    while ($size -gt 1024 -and $index -lt $sizes.Count - 1) {
        $size = $size / 1024
        $index++
    }

    return "$([math]::Round($size, 2)) $($sizes[$index])"
}

function Get-ErrorClassification {
    param (
        [string]$ErrorMessage
    )

    $errorTypes = @{
        'timeout' = 'Connection Timeout'
        'dns' = 'DNS Resolution Error'
        'ssl' = 'SSL/TLS Error'
        'connection refused' = 'Connection Refused'
        'not found' = 'Resource Not Found'
        'unauthorized' = 'Authentication Required'
        'forbidden' = 'Access Forbidden'
    }

    foreach ($type in $errorTypes.Keys) {
        if ($ErrorMessage -match $type) {
            return $errorTypes[$type]
        }
    }

    return 'General Error'
}

function Get-ResponseAnalysis {
    param (
        [Parameter(Mandatory=$true)]
        [object]$Response
    )

    $analysis = @{
        'ContentType' = $null
        'CharacterSet' = $null
        'IsCompressed' = $false
        'IsChunked' = $false
        'CacheControl' = $null
    }

    try {
        if ($PSVersionTable.PSVersion.Major -ge 6) {
            # PowerShell 7+ handling
            $analysis['ContentType'] = $Response.Headers['Content-Type']
            $analysis['CharacterSet'] = $Response.Headers['Character-Set']
            $analysis['IsCompressed'] = $Response.Headers['Content-Encoding'] -match 'gzip|deflate'
            $analysis['IsChunked'] = $Response.Headers['Transfer-Encoding'] -eq 'chunked'
            $analysis['CacheControl'] = $Response.Headers['Cache-Control']
        } else {
            # PowerShell 5.1 handling
            $analysis['ContentType'] = $Response.ContentType
            $analysis['CharacterSet'] = $Response.CharacterSet
            $analysis['IsCompressed'] = $Response.Headers['Content-Encoding'] -match 'gzip|deflate'
            $analysis['IsChunked'] = $Response.Headers['Transfer-Encoding'] -eq 'chunked'
            $analysis['CacheControl'] = $Response.Headers['Cache-Control']
        }
    } catch {
        Write-Debug "Error analyzing response: $($_.Exception.Message)"
    }

    return $analysis
}

function Get-HTTPStatusInfo {
    param (
        [int]$StatusCode
    )

    $statusInfo = @{
        'Category' = switch ($StatusCode) {
            { $_ -ge 100 -and $_ -lt 200 } { 'Informational' }
            { $_ -ge 200 -and $_ -lt 300 } { 'Success' }
            { $_ -ge 300 -and $_ -lt 400 } { 'Redirection' }
            { $_ -ge 400 -and $_ -lt 500 } { 'Client Error' }
            { $_ -ge 500 -and $_ -lt 600 } { 'Server Error' }
            default { 'Unknown' }
        }
        'Description' = switch ($StatusCode) {
            200 { 'OK - The request has succeeded' }
            201 { 'Created - The request has succeeded and a new resource has been created' }
            204 { 'No Content - The server successfully processed the request but returns no content' }
            301 { 'Moved Permanently - The requested resource has been permanently moved' }
            302 { 'Found - The requested resource has been temporarily moved' }
            304 { 'Not Modified - The resource has not been modified since the last request' }
            400 { 'Bad Request - The server cannot process the request due to client error' }
            401 { 'Unauthorized - Authentication is required' }
            403 { 'Forbidden - Server refuses to authorize the request' }
            404 { 'Not Found - The requested resource does not exist' }
            405 { 'Method Not Allowed - The HTTP method is not supported for this resource' }
            408 { 'Request Timeout - The server timed out waiting for the request' }
            409 { 'Conflict - The request conflicts with the current state of the server' }
            413 { 'Payload Too Large - The request entity is larger than the server is willing to process' }
            415 { 'Unsupported Media Type - The server does not support the media type of the request' }
            429 { 'Too Many Requests - Rate limit exceeded' }
            500 { 'Internal Server Error - Server encountered an unexpected condition' }
            501 { 'Not Implemented - The server does not support the functionality required' }
            502 { 'Bad Gateway - Server received an invalid response from upstream' }
            503 { 'Service Unavailable - Server is temporarily unable to handle the request' }
            504 { 'Gateway Timeout - Server did not receive a timely response from upstream' }
            505 { 'HTTP Version Not Supported - The server does not support the HTTP version used' }
            default { 'Unknown status code' }
        }
        'SuggestedAction' = switch ($StatusCode) {
            405 { 'Verify the HTTP method is supported by the endpoint and check API documentation' }
            { $_ -ge 400 -and $_ -lt 500 } { 'Check request parameters, authentication, and client configuration' }
            { $_ -ge 500 -and $_ -lt 600 } { 'Contact server administrator or try again later' }
            default { 'No specific action required' }
        }
    }

    return $statusInfo
}

function Get-SSLCertificate {
    <#
    .SYNOPSIS
        Retrieves SSL certificate information for a specified hostname and port.
 
    .DESCRIPTION
        Gets detailed SSL certificate information including TLS version, cipher algorithms, and certificate details.
 
    .PARAMETER Hostname
        The hostname to check the SSL certificate for.
 
    .PARAMETER Port
        The port number to connect to. Default is 443.
 
    .PARAMETER SkipValidation
        Whether to skip certificate validation. Default is false.
 
    .EXAMPLE
        Get-SSLCertificate -Hostname "example.com"
        Gets SSL certificate information for example.com on port 443.
 
    .EXAMPLE
        Get-SSLCertificate -Hostname "example.com" -Port 8443 -SkipValidation
        Gets SSL certificate information for example.com on port 8443, skipping validation.
 
    .OUTPUTS
        [hashtable] containing certificate details.
    #>

    [CmdletBinding()]
    [OutputType([hashtable])]
    param (
        [Parameter(Mandatory=$true)]
        [string]$Hostname,

        [Parameter(Mandatory=$false)]
        [int]$Port = 443,

        [Parameter(Mandatory=$false)]
        [bool]$SkipValidation = $false
    )

    try {
        Write-Verbose "Connecting to $Hostname on port $Port"
        $tcpClient = New-Object System.Net.Sockets.TcpClient
        $tcpClient.Connect($Hostname, $Port)

        Write-Verbose "Creating SSL stream"
        $sslStream = New-Object System.Net.Security.SslStream(
            $tcpClient.GetStream(),
            $false,
            {
                param($senderObj, $cert, $certChain, $errors)
                if ($SkipValidation) {
                    Write-Verbose "Skipping certificate validation"
                    return $true
                }
                return [System.Net.Security.SslPolicyErrors]::None -eq $errors
            }
        )

        Write-Verbose "Authenticating SSL stream"
        $sslStream.AuthenticateAsClient($Hostname)

        # Get the actual TLS version
        $tlsVersion = switch ($sslStream.SslProtocol) {
            'Tls' { 'TLS 1.0' }
            'Tls11' { 'TLS 1.1' }
            'Tls12' { 'TLS 1.2' }
            'Tls13' { 'TLS 1.3' }
            default { $sslStream.SslProtocol.ToString() }
        }

        Write-Verbose "TLS Version: $tlsVersion"
        return @{
            Certificate = $sslStream.RemoteCertificate
            TLSVersion = $tlsVersion
            CipherAlgorithm = $sslStream.CipherAlgorithm.ToString()
            HashAlgorithm = $sslStream.HashAlgorithm.ToString()
            KeyExchangeAlgorithm = $sslStream.KeyExchangeAlgorithm.ToString()
        }
    }
    catch {
        Write-Verbose "SSL Certificate Error: $($_.Exception.Message)"
        Write-Debug "Detailed error: $($_.Exception | ConvertTo-Json -Depth 10)"
        return $null
    }
    finally {
        if ($sslStream) {
            Write-Verbose "Disposing SSL stream"
            $sslStream.Dispose()
        }
        if ($tcpClient) {
            Write-Verbose "Disposing TCP client"
            $tcpClient.Dispose()
        }
    }
}

function Get-ResponseContent {
    param (
        [Parameter(Mandatory=$true)]
        [System.Net.WebResponse]$Response
    )

    try {
        $stream = $Response.GetResponseStream()
        $reader = New-Object System.IO.StreamReader($stream)
        return $reader.ReadToEnd()
    }
    finally {
        if ($reader) { $reader.Dispose() }
        if ($stream) { $stream.Dispose() }
    }
}

function Format-ResponseTime {
    param (
        [double]$Seconds
    )

    if ($Seconds -lt 1) {
        return "$([math]::Round($Seconds * 1000, 2)) ms"
    }
    return "$([math]::Round($Seconds, 3)) seconds"
}

function DebugURL {
    <#
    .SYNOPSIS
        Performs comprehensive URL testing and debugging with detailed network analysis.
 
    .DESCRIPTION
        The DebugURL function provides detailed analysis of URL connectivity, including:
        - DNS resolution details
        - SSL/TLS certificate information
        - Request and response headers
        - HTTP status code analysis
        - Performance metrics
        - Concurrent request testing
 
    .PARAMETER URL
        The URL to test. This parameter is mandatory.
 
    .PARAMETER Method
        The HTTP method to use. Valid values are: GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH.
        Default is GET.
 
    .PARAMETER Headers
        A hashtable of custom headers to include in the request.
 
    .PARAMETER Body
        The request body content.
 
    .PARAMETER SkipCertCheck
        Skip SSL certificate validation.
 
    .PARAMETER UserAgent
        Custom User-Agent string. Default is "DebugURL-PowerShell-Module/1.0".
 
    .PARAMETER Timeout
        Request timeout in seconds. Default is 30.
 
    .PARAMETER Proxy
        Proxy server URL.
 
    .PARAMETER ConcurrentRequests
        Number of concurrent requests for performance testing. Default is 1.
 
    .PARAMETER LogPath
        Path to save detailed logs of the debugging process.
 
    .EXAMPLE
        DebugURL -URL "https://example.com"
        Performs a basic GET request to example.com with detailed analysis.
 
    .EXAMPLE
        DebugURL -URL "https://api.example.com" -Method POST -Headers @{"Authorization"="Bearer token"}
        Performs a POST request with custom headers.
 
    .OUTPUTS
        None. Outputs detailed analysis to the console.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, Position=0)]
        [string]$URL,

        [Parameter(Mandatory=$false)]
        [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS', 'PATCH')]
        [string]$Method = 'GET',

        [Parameter(Mandatory=$false)]
        [hashtable]$Headers = @{},

        [Parameter(Mandatory=$false)]
        [string]$Body,

        [Parameter(Mandatory=$false)]
        [switch]$SkipCertCheck,

        [Parameter(Mandatory=$false)]
        [string]$UserAgent = "DebugURL-PowerShell-Module/1.0",

        [Parameter(Mandatory=$false)]
        [int]$Timeout = 30,

        [Parameter(Mandatory=$false)]
        [string]$Proxy,

        [Parameter(Mandatory=$false)]
        [int]$ConcurrentRequests = 1,

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

    try {
        # Initialize logging if LogPath is provided
        $logStream = $null
        if ($LogPath) {
            try {
                # Create log directory if it doesn't exist
                $logDir = Split-Path -Path $LogPath -Parent
                if (-not (Test-Path $logDir)) {
                    New-Item -ItemType Directory -Path $logDir -Force | Out-Null
                }
                
                # Create log file with timestamp
                $timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
                $logFile = Join-Path $LogPath "DebugURL_$timestamp.log"
                
                # Ensure the log file directory exists
                $logFileDir = Split-Path -Path $logFile -Parent
                if (-not (Test-Path $logFileDir)) {
                    New-Item -ItemType Directory -Path $logFileDir -Force | Out-Null
                }
                
                $logStream = [System.IO.StreamWriter]::new($logFile)
                
                # Write initial log information
                $logStream.WriteLine("DebugURL Log - Started at $(Get-Date)")
                $logStream.WriteLine("URL: $URL")
                $logStream.WriteLine("Method: $Method")
                
                # Convert headers to a serializable format
                $headerObj = @{}
                foreach ($key in $Headers.Keys) {
                    $headerObj[$key.ToString()] = $Headers[$key].ToString()
                }
                $logStream.WriteLine("Headers: $($headerObj | ConvertTo-Json -Compress)")
                
                $logStream.WriteLine("Timeout: $Timeout seconds")
                $logStream.WriteLine("SkipCertCheck: $SkipCertCheck")
                $logStream.WriteLine("UserAgent: $UserAgent")
                if ($Proxy) { $logStream.WriteLine("Proxy: $Proxy") }
                if ($Body) { $logStream.WriteLine("Body: $Body") }
                $logStream.WriteLine("ConcurrentRequests: $ConcurrentRequests")
                $logStream.WriteLine("----------------------------------------")
            }
            catch {
                Write-Warning "Failed to initialize logging: $($_.Exception.Message)"
                Write-Debug "Log initialization error: $($_.Exception | ConvertTo-Json -Depth 3)"
            }
        }

        Write-Debug "Starting DebugURL function with URL: $URL"
        Write-Verbose "Initializing request with Method: $Method, Timeout: $Timeout seconds"

        # Initialize timeline tracking
        $timeline = [ordered]@{
            'DNS Resolution' = 0
            'TCP Connection' = 0
            'SSL Handshake' = 0
            'Request Send' = 0
            'Response Wait' = 0
            'Total Time' = 0
        }

        $startTime = Get-Date

        # Parse URL
        $uri = [System.Uri]$URL
        Write-Debug "Parsed URL - Scheme: $($uri.Scheme), Host: $($uri.Host), Port: $($uri.Port)"
        if ($logStream) {
            $logStream.WriteLine("URL Details:")
            $logStream.WriteLine(" Scheme: $($uri.Scheme)")
            $logStream.WriteLine(" Host: $($uri.Host)")
            $logStream.WriteLine(" Port: $($uri.Port)")
            $logStream.WriteLine(" Path: $($uri.PathAndQuery)")
        }

        # DNS Resolution
        Write-Verbose "Performing DNS resolution for $($uri.Host)"
        $dnsStartTime = Get-Date
        try {
            $dnsResult = Resolve-DnsName -Name $uri.Host -ErrorAction Stop
            $dnsEndTime = Get-Date
            $timeline['DNS Resolution'] = ($dnsEndTime - $dnsStartTime).TotalSeconds
            if ($logStream) {
                $logStream.WriteLine("DNS Resolution Results:")
                # Convert DNS results to a serializable format
                $dnsObj = @{}
                foreach ($record in $dnsResult) {
                    $dnsObj[$record.Name] = @{
                        'Type' = $record.Type
                        'IPAddress' = $record.IPAddress
                        'TTL' = $record.TTL
                    }
                }
                $dnsJson = $dnsObj | ConvertTo-Json -Compress
                $logStream.WriteLine($dnsJson)
                $logStream.WriteLine("DNS Resolution Time: $($timeline['DNS Resolution']) seconds")
            }
            $dnsTable = $dnsResult | Format-Table Name, Type, IPAddress, QueryType, Section | Out-String
            Write-Output "==================== DNS Resolution ===================="
            Write-Output $dnsTable.TrimEnd()
            Write-Output ""
        }
        catch {
            $errorDetails = @{
                'Hostname' = $uri.Host
                'Error' = $_.Exception.Message
                'Type' = Get-ErrorClassification -ErrorMessage $_.Exception.Message
            }

            if ($logStream) {
                $logStream.WriteLine("DNS Resolution Error:")
                $logStream.WriteLine(" Error: $($_.Exception.Message)")
                $logStream.WriteLine(" Type: $(Get-ErrorClassification -ErrorMessage $_.Exception.Message)")
            }

            if ($PSVersionTable.PSVersion.Major -ge 6) {
                Write-Error "DNS Resolution Error: Unable to resolve hostname '$($uri.Host)'"
                Write-Debug ($errorDetails | ConvertTo-Json -Depth 3)
            }
            else {
                Write-Error "DNS Resolution Error: Unable to resolve hostname '$($uri.Host)'"
                Write-Debug "Error Details: $($errorDetails | ConvertTo-Json -Depth 3)"
            }
            return
        }

        # Get SSL Certificate Info for HTTPS
        $actualTLSVersion = "N/A"
        $certInfo = $null
        if ($uri.Scheme -eq 'https') {
            $sslStartTime = Get-Date
            Write-Verbose "Retrieving SSL certificate information"
            try {
                $certInfo = Get-SSLCertificate -Hostname $uri.Host -Port $uri.Port -SkipValidation:$SkipCertCheck
                $sslEndTime = Get-Date
                $timeline['SSL Handshake'] = ($sslEndTime - $sslStartTime).TotalSeconds
                if ($certInfo) {
                    $actualTLSVersion = $certInfo.TLSVersion
                    Write-Debug "SSL Certificate Info - TLS Version: $actualTLSVersion, Cipher: $($certInfo.CipherAlgorithm)"
                }
            } catch {
                $errorDetails = @{
                    'Hostname' = $uri.Host
                    'Error' = $_.Exception.Message
                    'Type' = Get-ErrorClassification -ErrorMessage $_.Exception.Message
                }

                if ($PSVersionTable.PSVersion.Major -ge 6) {
                    Write-Warning "SSL Certificate Error: Unable to retrieve certificate for '$($uri.Host)'"
                    Write-Debug ($errorDetails | ConvertTo-Json -Depth 3)
                } else {
                    Write-Warning "SSL Certificate Error: Unable to retrieve certificate for '$($uri.Host)'"
                    Write-Debug "Error Details: $($errorDetails | ConvertTo-Json -Depth 3)"
                }
            }
        }

        # Create Request Headers Output
        Write-Verbose "Preparing request headers"
        $requestHeaders = [ordered]@{
            'Host' = $uri.Host
            'Method' = $Method
            'Port' = $uri.Port
            'TLS Version' = if ($uri.Scheme -eq 'https') { $actualTLSVersion } else { "N/A (HTTP)" }
            'User-Agent' = $UserAgent
        }

        # Add custom headers
        foreach ($header in $Headers.GetEnumerator()) {
            Write-Debug "Adding custom header: $($header.Key) = $($header.Value)"
            $requestHeaders[$header.Key] = $header.Value
        }

        Write-Output "==================== Request Headers ==================="
        $requestHeadersObj = New-Object PSObject -Property $requestHeaders
        Write-Output ($requestHeadersObj | Format-List | Out-String).TrimEnd()
        Write-Output ""

        # Display SSL Certificate Info for HTTPS
        if ($uri.Scheme -eq 'https') {
            Write-Verbose "Displaying certificate details"
            Write-Output "================== Certificate Details ================="

            if ($certInfo) {
                $cert = $certInfo.Certificate
                Write-Debug "Certificate Details - Subject: $($cert.Subject), Expires: $($cert.GetExpirationDateString())"
                $certDetails = [ordered]@{
                    'Thumbprint' = $cert.GetCertHashString()
                    'Subject' = $cert.Subject
                    'Issuer' = $cert.Issuer
                    'NotAfter' = $cert.GetExpirationDateString()
                    'TLS Version' = $certInfo.TLSVersion
                    'Cipher Algorithm' = $certInfo.CipherAlgorithm
                    'Hash Algorithm' = $certInfo.HashAlgorithm
                    'Key Exchange Algorithm' = $certInfo.KeyExchangeAlgorithm
                }
                $certObj = New-Object PSObject -Property $certDetails
                Write-Output ($certObj | Format-List | Out-String).TrimEnd()
            }
            else {
                Write-Warning "Unable to retrieve certificate details"
            }
            Write-Output ""
        }

        # Handle concurrent requests
        if ($ConcurrentRequests -gt 1) {
            Write-Verbose "Processing $ConcurrentRequests concurrent requests"
            Write-Debug "Concurrent request parameters - URL: $URL, Method: $Method, Timeout: $Timeout"
            Write-Output "`n================== Concurrent Requests Summary =================="
            Write-Output "Concurrent Requests: $ConcurrentRequests"

            $jobs = @()
            $startTime = Get-Date

            # Create and start jobs
            for ($i = 1; $i -le $ConcurrentRequests; $i++) {
                Write-Debug "Starting job $i of $ConcurrentRequests"
                $jobScript = {
                    param($URL, $Method, $Headers, $UserAgent, $Timeout, $SkipCertCheck, $Proxy, $Body, $PSVersion)

                    $ErrorActionPreference = 'Stop'
                    $result = @{
                        Success = $false
                        Time = 0
                        Status = $null
                        Error = $null
                        Content = $null
                    }

                    try {
                        Write-Debug "Job: Preparing request parameters"
                        # Create a clean copy of headers without invalid characters
                        $cleanHeaders = @{}
                        foreach ($key in $Headers.Keys) {
                            if ($key -notmatch '[^a-zA-Z0-9\-]') {
                                $cleanHeaders[$key] = $Headers[$key]
                            }
                        }

                        $requestParams = @{
                            Uri = $URL
                            Method = $Method
                            Headers = $cleanHeaders
                            UserAgent = $UserAgent
                            TimeoutSec = $Timeout
                            ErrorAction = 'Stop'
                        }

                        if ($SkipCertCheck) {
                            Write-Debug "Job: Skipping certificate validation"
                            $requestParams['SkipCertificateCheck'] = $true
                        }

                        if ($Proxy) {
                            Write-Debug "Job: Using proxy: $Proxy"
                            $requestParams['Proxy'] = $Proxy
                        }

                        if ($Body) {
                            Write-Debug "Job: Adding request body"
                            $requestParams['Body'] = $Body
                        }

                        Write-Debug "Job: Sending request"
                        $jobStartTime = Get-Date
                        $response = Invoke-WebRequest @requestParams
                        $jobEndTime = Get-Date

                        $result.Success = $true
                        $result.Time = ($jobEndTime - $jobStartTime).TotalSeconds
                        $result.Status = $response.StatusCode
                        $result.Content = if ($response.Content) { $response.Content } else { "" }
                        Write-Debug "Job: Request completed successfully in $($result.Time) seconds"
                    }
                    catch {
                        Write-Debug "Job: Request failed with error: $($_.Exception.Message)"
                        $result.Error = $_.Exception.Message
                    }

                    return $result
                }

                $jobs += Start-Job -ScriptBlock $jobScript -ArgumentList $URL, $Method, $requestHeaders, $UserAgent, $Timeout, $SkipCertCheck, $Proxy, $Body, $PSVersionTable.PSVersion.Major
            }

            Write-Verbose "Waiting for all jobs to complete"
            $jobResults = $jobs | Wait-Job | Receive-Job
            $jobs | Remove-Job

            # Process results
            $successCount = 0
            $totalTime = 0
            $requestNumber = 1

            foreach ($result in $jobResults) {
                if ($result.Success) {
                    $successCount++
                    $totalTime += $result.Time
                    Write-Output "Request $requestNumber`: Success, Time: $(Format-ResponseTime -Seconds $result.Time), Status: $($result.Status)"
                    Write-Debug "Request $requestNumber details - Time: $($result.Time)s, Status: $($result.Status)"
                }
                else {
                    Write-Output "Request $requestNumber`: Fail, Time: $(Format-ResponseTime -Seconds $result.Time), Status: N/A - $($result.Error)"
                    Write-Debug "Request $requestNumber failed - Error: $($result.Error)"
                }
                $requestNumber++
            }

            if ($successCount -gt 0) {
                $avgTime = $totalTime / $successCount
                # Create array of times from successful requests
                $successTimes = @()
                foreach ($result in $jobResults) {
                    if ($result.Success) {
                        $successTimes += $result.Time
                    }
                }
                # Calculate min and max manually
                $minTime = [double]::MaxValue
                $maxTime = [double]::MinValue
                foreach ($time in $successTimes) {
                    if ($time -lt $minTime) { $minTime = $time }
                    if ($time -gt $maxTime) { $maxTime = $time }
                }
                Write-Output "Response Time Statistics:"
                Write-Output " Average: $(Format-ResponseTime -Seconds $avgTime)"
                Write-Output " Minimum: $(Format-ResponseTime -Seconds $minTime)"
                Write-Output " Maximum: $(Format-ResponseTime -Seconds $maxTime)"
                Write-Debug "Concurrent requests summary - Success: $successCount/$ConcurrentRequests, Avg Time: $avgTime s"
            }
            Write-Output "$successCount of $ConcurrentRequests requests succeeded."
            return
        } else {
            Write-Verbose "Processing single request"
            if ($PSVersionTable.PSVersion.Major -ge 6) {
                Write-Debug "Using PowerShell 6+ request handling"
                $requestStartTime = Get-Date
                $webRequestParams = @{
                    Uri = $URL
                    Method = $Method
                    Headers = $Headers
                    UserAgent = $UserAgent
                    TimeoutSec = $Timeout
                    UseBasicParsing = $true
                    SkipCertificateCheck = $SkipCertCheck
                    ErrorAction = 'Stop'
                }
                if ($Proxy) {
                    Write-Debug "Adding proxy: $Proxy"
                    $webRequestParams['Proxy'] = $Proxy
                }
                if ($Body) {
                    Write-Debug "Adding request body"
                    $webRequestParams['Body'] = $Body
                }
                Write-Verbose "Sending request with parameters: $($webRequestParams | ConvertTo-Json)"
                try {
                    $tcpStartTime = Get-Date
                    $response = Invoke-WebRequest @webRequestParams
                    $tcpEndTime = Get-Date
                    $timeline['TCP Connection'] = ($tcpEndTime - $tcpStartTime).TotalSeconds
                    $timeline['Response Wait'] = ($tcpEndTime - $tcpStartTime).TotalSeconds
                }
                catch {
                    if ($_.Exception.Response) {
                        $statusCode = [int]$_.Exception.Response.StatusCode
                        $statusInfo = Get-HTTPStatusInfo -StatusCode $statusCode

                        Write-Output "=================== HTTP Error Details ==================="
                        Write-Output "Status Code: $statusCode"
                        Write-Output "Category: $($statusInfo.Category)"
                        Write-Output "Description: $($statusInfo.Description)"
                        Write-Output "Suggested Action: $($statusInfo.SuggestedAction)"
                        Write-Output ""

                        # Get response headers if available
                        if ($_.Exception.Response) {
                            Write-Output "Response Headers:"
                            try {
                                if ($PSVersionTable.PSVersion.Major -ge 6) {
                                    $response = $_.Exception.Response
                                    $headers = @{}
                                    foreach ($header in $response.Headers.GetEnumerator()) {
                                        $headers[$header.Key] = $header.Value
                                    }
                                    foreach ($key in $headers.Keys) {
                                        Write-Output " $key : $($headers[$key])"
                                    }
                                } else {
                                    $response = $_.Exception.Response
                                    $headers = @{}
                                    foreach ($key in $response.Headers.AllKeys) {
                                        $headers[$key] = $response.Headers[$key]
                                    }
                                    foreach ($key in $headers.Keys) {
                                        Write-Output " $key : $($headers[$key])"
                                    }
                                }
                            }
                            catch {
                                Write-Debug "Error accessing response headers: $($_.Exception.Message)"
                                Write-Debug "Exception type: $($_.Exception.GetType().FullName)"
                                Write-Debug "Stack trace: $($_.ScriptStackTrace)"

                                # Try alternative method
                                try {
                                    $response = $_.Exception.Response
                                    Write-Output " Status: $($response.StatusCode) $($response.StatusDescription)"
                                    Write-Output " Content-Type: $($response.ContentType)"
                                    Write-Output " Content-Length: $($response.ContentLength)"
                                    Write-Output " Server: $($response.Server)"
                                    Write-Output " Last-Modified: $($response.LastModified)"
                                }
                                catch {
                                    Write-Debug "Alternative header access also failed: $($_.Exception.Message)"
                                }
                            }
                            Write-Output ""
                        } else {
                            Write-Debug "No response object available"
                        }

                        # Update timeline before error
                        $requestEndTime = Get-Date
                        $timeline['Request Send'] = ($requestEndTime - $requestStartTime).TotalSeconds
                        $timeTaken = ($requestEndTime - $startTime).TotalSeconds
                        $timeline['Total Time'] = $timeTaken

                        # Display timeline before throwing the error
                        Write-Output "=================== Request Timeline ==================="
                        $timelineObj = New-Object PSObject -Property $timeline
                        Write-Output ($timelineObj | Format-List | Out-String).TrimEnd()
                        Write-Output ""

                        Write-Error "HTTP Error $statusCode - $($statusInfo.Description)" -ErrorAction Stop
                    }
                    else {
                        # Update timeline before error
                        $requestEndTime = Get-Date
                        $timeline['Request Send'] = ($requestEndTime - $requestStartTime).TotalSeconds
                        $timeTaken = ($requestEndTime - $startTime).TotalSeconds
                        $timeline['Total Time'] = $timeTaken

                        # Display timeline before throwing the error
                        Write-Output "=================== Request Timeline ==================="
                        $timelineObj = New-Object PSObject -Property $timeline
                        Write-Output ($timelineObj | Format-List | Out-String).TrimEnd()
                        Write-Output ""
                        
                        throw
                    }
                }
                $requestEndTime = Get-Date
                $timeline['Request Send'] = ($requestEndTime - $requestStartTime).TotalSeconds
                $timeTaken = ($requestEndTime - $startTime).TotalSeconds
                $timeline['Total Time'] = $timeTaken
                Write-Debug "Request completed in $timeTaken seconds"
            }
            else {
                Write-Debug "Using PowerShell 5.1 request handling"
                if ($SkipCertCheck) {
                    Write-Debug "Skipping certificate validation"
                    [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
                }
                $request = [System.Net.HttpWebRequest]::Create($URL)
                $request.Method = $Method
                $request.UserAgent = $UserAgent
                $request.Timeout = $Timeout * 1000
                $request.AllowAutoRedirect = $false

                Write-Debug "Setting up request headers"
                if ($Headers.ContainsKey('Content-Type')) { $request.ContentType = $Headers['Content-Type']; $Headers.Remove('Content-Type') }
                if ($Headers.ContainsKey('Accept')) { $request.Accept = $Headers['Accept']; $Headers.Remove('Accept') }
                if ($Headers.ContainsKey('User-Agent')) { $request.UserAgent = $Headers['User-Agent']; $Headers.Remove('User-Agent') }
                if ($Headers.ContainsKey('Referer')) { $request.Referer = $Headers['Referer']; $Headers.Remove('Referer') }
                if ($Headers.ContainsKey('Date')) { $request.Date = [DateTime]::Parse($Headers['Date']); $Headers.Remove('Date') }
                if ($Headers.ContainsKey('Host')) { $request.Host = $Headers['Host']; $Headers.Remove('Host') }
                foreach ($header in $Headers.GetEnumerator()) {
                    Write-Debug "Adding header: $($header.Key) = $($header.Value)"
                    $request.Headers.Add($header.Key, $header.Value)
                }
                if ($Proxy) {
                    Write-Debug "Setting up proxy: $Proxy"
                    $request.Proxy = New-Object System.Net.WebProxy($Proxy)
                }
                if ($Body) {
                    Write-Debug "Adding request body"
                    $bodyBytes = [System.Text.Encoding]::UTF8.GetBytes($Body)
                    $request.ContentLength = $bodyBytes.Length
                    $requestStream = $request.GetRequestStream()
                    $requestStream.Write($bodyBytes, 0, $bodyBytes.Length)
                    $requestStream.Close()
                }
                Write-Verbose "Sending request"
                $requestStartTime = Get-Date
                try {
                    $tcpStartTime = Get-Date
                    $response = $request.GetResponse()
                    $tcpEndTime = Get-Date
                    $timeline['TCP Connection'] = ($tcpEndTime - $tcpStartTime).TotalSeconds
                    $timeline['Response Wait'] = ($tcpEndTime - $tcpStartTime).TotalSeconds
                }
                catch [System.Net.WebException] {
                    if ($_.Exception.Response) {
                        $statusCode = [int]$_.Exception.Response.StatusCode
                        $statusInfo = Get-HTTPStatusInfo -StatusCode $statusCode

                        Write-Output "=================== HTTP Error Details ==================="
                        Write-Output "Status Code: $statusCode"
                        Write-Output "Category: $($statusInfo.Category)"
                        Write-Output "Description: $($statusInfo.Description)"
                        Write-Output "Suggested Action: $($statusInfo.SuggestedAction)"
                        Write-Output ""

                        # Get response headers if available
                        if ($_.Exception.Response) {
                            Write-Output "Response Headers:"
                            try {
                                if ($PSVersionTable.PSVersion.Major -ge 6) {
                                    $response = $_.Exception.Response
                                    $headers = @{}
                                    foreach ($header in $response.Headers.GetEnumerator()) {
                                        $headers[$header.Key] = $header.Value
                                    }
                                    foreach ($key in $headers.Keys) {
                                        Write-Output " $key : $($headers[$key])"
                                    }
                                } else {
                                    $response = $_.Exception.Response
                                    $headers = @{}
                                    foreach ($key in $response.Headers.AllKeys) {
                                        $headers[$key] = $response.Headers[$key]
                                    }
                                    foreach ($key in $headers.Keys) {
                                        Write-Output " $key : $($headers[$key])"
                                    }
                                }
                            }
                            catch {
                                Write-Debug "Error accessing response headers: $($_.Exception.Message)"
                                Write-Debug "Exception type: $($_.Exception.GetType().FullName)"
                                Write-Debug "Stack trace: $($_.ScriptStackTrace)"

                                # Try alternative method
                                try {
                                    $response = $_.Exception.Response
                                    Write-Output " Status: $($response.StatusCode) $($response.StatusDescription)"
                                    Write-Output " Content-Type: $($response.ContentType)"
                                    Write-Output " Content-Length: $($response.ContentLength)"
                                    Write-Output " Server: $($response.Server)"
                                    Write-Output " Last-Modified: $($response.LastModified)"
                                }
                                catch {
                                    Write-Debug "Alternative header access also failed: $($_.Exception.Message)"
                                }
                            }
                            Write-Output ""
                        } else {
                            Write-Debug "No response object available"
                        }

                        # Update timeline before error
                        $requestEndTime = Get-Date
                        $timeline['Request Send'] = ($requestEndTime - $requestStartTime).TotalSeconds
                        $timeTaken = ($requestEndTime - $startTime).TotalSeconds
                        $timeline['Total Time'] = $timeTaken

                        # Display timeline before throwing the error
                        Write-Output "=================== Request Timeline ==================="
                        $timelineObj = New-Object PSObject -Property $timeline
                        Write-Output ($timelineObj | Format-List | Out-String).TrimEnd()
                        Write-Output ""

                        Write-Error "HTTP Error $statusCode - $($statusInfo.Description)" -ErrorAction Stop
                    }
                    throw
                }
                $requestEndTime = Get-Date
                $timeline['Request Send'] = ($requestEndTime - $requestStartTime).TotalSeconds
                $timeTaken = ($requestEndTime - $startTime).TotalSeconds
                $timeline['Total Time'] = $timeTaken
                Write-Debug "Request completed in $timeTaken seconds"
            }

            # Response Headers
            Write-Output "=================== Response Headers ==================="
            Write-Debug "Processing response headers"

            # Get response analysis
            $responseAnalysis = if ($response) { Get-ResponseAnalysis -Response $response } else { @{} }

            # Calculate response size
            $responseSize = 0
            if ($response) {
                try {
                    if ($PSVersionTable.PSVersion.Major -ge 6) {
                        # PowerShell 7+ handling
                        $responseSize = if ($response.RawContentLength -gt 0) {
                            $response.RawContentLength
                        } else {
                            [System.Text.Encoding]::UTF8.GetByteCount($response.Content)
                        }
                    } else {
                        # PowerShell 5.1 handling
                        $responseSize = if ($response.ContentLength -gt 0) {
                            $response.ContentLength
                        } else {
                            $content = Get-ResponseContent -Response $response
                            [System.Text.Encoding]::UTF8.GetByteCount($content)
                        }
                    }
                } catch {
                    Write-Debug "Error calculating response size: $($_.Exception.Message)"
                }
            }

            $responseHeaders = [ordered]@{
                'Request-URI' = if ($response -and $response.ResponseUri) { $response.ResponseUri.ToString() } else { $URL }
                'ResponseHeaders' = "Response Headers"
                'ResponseTime' = Format-ResponseTime -Seconds $timeTaken
                'ResponseSize' = Format-Size -Bytes $responseSize
                'ContentType' = $responseAnalysis.ContentType
                'CharacterSet' = $responseAnalysis.CharacterSet
                'IsCompressed' = $responseAnalysis.IsCompressed
                'IsChunked' = $responseAnalysis.IsChunked
                'CacheControl' = $responseAnalysis.CacheControl
            }

            if ($response) {
                try {
                    if ($PSVersionTable.PSVersion.Major -ge 6) {
                        Write-Debug "Processing PowerShell 7+ response"
                        $responseHeaders['StatusCode'] = $response.StatusCode
                        $responseHeaders['StatusDescription'] = $response.StatusDescription

                        # Process response headers
                        if ($response.Headers) {
                            foreach ($header in $response.Headers.GetEnumerator()) {
                                Write-Debug "Response header: $($header.Key) = $($header.Value)"
                                $responseHeaders[$header.Key] = $header.Value
                            }
                        }

                        # Process content
                        $content = if ($response.Content) { $response.Content } else { "" }
                        if ($content) {
                            $responseHeaders['Content'] = $content.Substring(0, [Math]::Min(150, $content.Length))
                        } else {
                            $responseHeaders['Content'] = ""
                        }
                    } else {
                        Write-Debug "Processing PowerShell 5.1 response"
                        $responseHeaders['StatusCode'] = [int]$response.StatusCode
                        $responseHeaders['StatusDescription'] = $response.StatusDescription

                        # Process response headers
                        if ($response.Headers) {
                            foreach ($key in $response.Headers.AllKeys) {
                                Write-Debug "Response header: $key = $($response.Headers[$key])"
                                $responseHeaders[$key] = $response.Headers[$key]
                            }
                        }

                        # Process content
                        try {
                            $content = Get-ResponseContent -Response $response
                            if ($content) {
                                $responseHeaders['Content'] = $content.Substring(0, [Math]::Min(150, $content.Length))
                            } else {
                                $responseHeaders['Content'] = ""
                            }
                        } catch {
                            Write-Debug "Error getting response content: $($_.Exception.Message)"
                            $responseHeaders['Content'] = ""
                        }
                    }
                } catch {
                    Write-Debug "Error processing response: $($_.Exception.Message)"
                    $responseHeaders['Error'] = $_.Exception.Message
                }
            }

            $responseObj = New-Object PSObject -Property $responseHeaders
            Write-Output ($responseObj | Format-List | Out-String).TrimEnd()
            Write-Output ""

            # Display Timeline
            Write-Output "=================== Request Timeline ==================="
            $timelineObj = New-Object PSObject -Property $timeline
            Write-Output ($timelineObj | Format-List | Out-String).TrimEnd()
            Write-Output ""
        }
    }
    catch {
        if ($logStream) {
            $logStream.WriteLine("Error Occurred:")
            $logStream.WriteLine(" Message: $($_.Exception.Message)")
            $logStream.WriteLine(" Type: $(Get-ErrorClassification -ErrorMessage $_.Exception.Message)")
            $logStream.WriteLine(" Stack Trace: $($_.ScriptStackTrace)")
        }
        if ($_.Exception.Message -match 'HTTP Error \d+') {
            # Error already handled and displayed, just return
            return
        }

        # Update timeline before error
        $requestEndTime = Get-Date
        $timeline['Request Send'] = ($requestEndTime - $requestStartTime).TotalSeconds
        $timeTaken = ($requestEndTime - $startTime).TotalSeconds
        $timeline['Total Time'] = $timeTaken

        # Display timeline before throwing the error
        Write-Output "=================== Request Timeline ==================="
        $timelineObj = New-Object PSObject -Property $timeline
        Write-Output ($timelineObj | Format-List | Out-String).TrimEnd()
        Write-Output ""

        $errorDetails = @{
            'Hostname' = $uri.Host
            'Error' = $_.Exception.Message
            'Type' = Get-ErrorClassification -ErrorMessage $_.Exception.Message
        }

        if ($PSVersionTable.PSVersion.Major -ge 6) {
            Write-Error "Error: $($errorDetails.Type) - $($errorDetails.Error)"
            Write-Debug ($errorDetails | ConvertTo-Json -Depth 3)
        } else {
            Write-Error "Error: $($errorDetails.Type) - $($errorDetails.Error)"
            Write-Debug "Error Details: $($errorDetails | ConvertTo-Json -Depth 3)"
        }
        return
    } finally {
        if ($logStream) {
            $logStream.WriteLine("----------------------------------------")
            $logStream.WriteLine("DebugURL Log - Ended at $(Get-Date)")
            $logStream.Dispose()
        }
        # Cleanup any resources
        if ($response -and -not $PSVersionTable.PSVersion.Major -ge 6) {
            $response.Close()
        }
        if ($PSVersionTable.PSVersion.Major -lt 6 -and $SkipCertCheck) {
            [System.Net.ServicePointManager]::ServerCertificateValidationCallback = $null
        }
    }
}

function Get-DNSCache {
    <#
    .SYNOPSIS
        Retrieves and displays the current DNS cache entries.
 
    .DESCRIPTION
        The Get-DNSCache function displays the contents of the local DNS cache,
        including hostnames, IP addresses, and record types. This is useful for
        troubleshooting DNS resolution issues and verifying cached entries.
 
    .EXAMPLE
        Get-DNSCache
        Displays all entries in the DNS cache.
 
    .EXAMPLE
        Get-DNSCache | Where-Object { $_.Name -like "*.example.com" }
        Filters DNS cache entries for a specific domain pattern.
 
    .OUTPUTS
        System.Object[]
        Returns an array of objects containing DNS cache entries with the following properties:
        - Name: The hostname
        - Type: The record type (A, AAAA, CNAME, etc.)
        - IPAddress: The resolved IP address
        - TTL: Time to live in seconds
    #>

    [CmdletBinding()]
    param()

    try {
        Write-Debug "Retrieving DNS cache entries"
        $dnsCache = Get-DnsClientCache
        if ($dnsCache) {
            Write-Output "==================== DNS Cache Entries ===================="
            $dnsCache | Format-Table Name, Type, IPAddress, TTL | Out-String
            Write-Debug "Successfully retrieved $($dnsCache.Count) DNS cache entries"
        } else {
            Write-Output "No entries found in DNS cache."
            Write-Debug "DNS cache is empty"
        }
    }
    catch {
        Write-Error "Failed to retrieve DNS cache: $($_.Exception.Message)"
        Write-Debug "Error retrieving DNS cache: $($_.Exception.Message)"
    }
}

Export-ModuleMember -Function DebugURL, Get-DNSCache