RelaxedIT.Tools.psm1

function Get-BasePath {
    param (
        [Parameter(Mandatory = $true)]
        [string]$Path
    )

    # Resolve the base directory
    $BasePath = Split-Path -Path $Path -Parent
    return $BasePath
}


function Start-ElevatedPwsh {
    # Check if the current session is running as Administrator
    if (-Not ([Security.Principal.WindowsPrincipal]([Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))) {
        Write-RelaxedIT -logtext "Starting an elevated PowerShell session..." -ForegroundColor Yellow

        # Start an elevated PowerShell session (empty)
        Start-Process -FilePath "pwsh.exe" -Verb RunAs

        Write-RelaxedIT -logtext "An elevated PowerShell session has been started." -ForegroundColor Green
    } else {
        Write-RelaxedIT -logtext "This session is already running with Administrator privileges." -ForegroundColor Cyan
    }
}

function Test-AndCreatePath {
    param (
        [Parameter(Mandatory = $true)]
        [string]$Path
    )

    # Check if the path exists
    if (Test-Path -Path $Path) {
        Write-RelaxedIT -logtext "Path '$Path' already exists." -ForegroundColor Green -level 3
    } else {
        # Create the path
        Write-RelaxedIT -logtext "Path '$Path' does not exist. Creating it now..." -ForegroundColor Yellow
        New-Item -ItemType Directory -Path $Path | Out-Null
        Write-RelaxedIT -logtext "Path '$Path' has been created." -ForegroundColor Cyan
    }
}

function Update-InFileContent {
    param (
        [Parameter(Mandatory = $true)]
        [string]$FilePath,   # The file to modify
        [Parameter(Mandatory = $true)]
        [string]$OldText,    # Text to be replaced
        [Parameter(Mandatory = $true)]
        [string]$NewText     # Replacement text
    )

    # Check if file exists
    if (-Not (Test-Path -Path $FilePath)) {
        Write-RelaxedIT -logtext "[ERR] File not found: $FilePath" -ForegroundColor Red
        return
    }

    # Read the content of the file
    $Content = Get-Content -Path $FilePath

    # Replace the specified text
    $UpdatedContent = $Content -replace [regex]::Escape($OldText), $NewText

    # Write the updated content back to the file
    Set-Content -Path $FilePath -Value $UpdatedContent -Encoding utf8BOM

    Write-RelaxedIT -logtext "Replaced ""$OldText"" with ""$NewText"" in ""$FilePath""" -ForegroundColor Green -level 2
}


Function Get-HwInfo
{
    <#
    .SYNOPSIS
    Queries essential hardware, OS, and disk information from local or remote computers.
    .PARAMETER ComputerName
    Specifies the target computer(s) for which to retrieve the information.
    Defaults to the local computer.
    .EXAMPLE
    Get-HwInfo -ComputerName "Server01", "PC05"
    #>

    [CmdletBinding(SupportsShouldProcess = $True)]
    Param(
        [Parameter(Mandatory = $False, Position = 1)]
        [string[]]$ComputerName = $Env:COMPUTERNAME
    )

    $HWInfoArray = @()

    foreach($Computer in $ComputerName)
    {
        # Helper variable for conditional CIM calls (Splatting)
        $CimParams = @{ ErrorAction = 'Stop' }
        if ($Computer -ne $Env:COMPUTERNAME) {
            $CimParams.Add('ComputerName', $Computer)
        }

        # Assuming Write-RelaxedIT is a custom function for logging
        Write-RelaxedIT -logtext "Query Hardware and OS Infos for ""$Computer""..."

        try
        {
            # --- 1. Basic HW Info ---
            $BIOS = Get-CimInstance -ClassName Win32_BIOS @CimParams
            $System = Get-CimInstance -ClassName Win32_ComputerSystem @CimParams

            $ObjectOutput = [PSCustomObject]@{
                ComputerName       = $Computer.ToUpper()
                BIOSVersion        = $BIOS.SMBIOSBIOSVersion
                SerialNumber       = $BIOS.SerialNumber
                Manufacturer       = $System.Manufacturer
                Model              = $System.Model
                SystemFamily       = $System.SystemFamily
            }

            # --- 2. CPU Info ---
            $Processor = Get-CimInstance -ClassName Win32_Processor @CimParams |
                         Select-Object -Property Name, NumberOfCores, NumberOfLogicalProcessors |
                         ConvertTo-Json -Compress

            $ObjectOutput | Add-Member -MemberType NoteProperty -Name CPUJSON -Value $Processor

            # --- 3. RAM Info ---
            $RAMInfo = Get-CimInstance -ClassName Win32_PhysicalMemory @CimParams
            $TotalRAMBytes = ($RAMInfo | Measure-Object -Property Capacity -Sum).Sum
            $RAM_GB = [math]::Round($TotalRAMBytes / 1GB, 2)

            $ObjectOutput | Add-Member -MemberType NoteProperty -Name RAM_GB -Value $RAM_GB

            # --- 4. OS Details ---
            $OS = Get-CimInstance -ClassName Win32_OperatingSystem @CimParams
            $ObjectOutput | Add-Member -MemberType NoteProperty -Name ProductName -Value $OS.Caption
            $ObjectOutput | Add-Member -MemberType NoteProperty -Name CurrentBuildNumber -Value $OS.BuildNumber

            $DisplayVersion = $null
            if ($CimParams.ContainsKey('ComputerName'))
            {
                $DisplayVersion = Invoke-Command -ComputerName $Computer -ScriptBlock {
                    (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion').DisplayVersion
                } -ErrorAction SilentlyContinue
            }
            else
            {
                $DisplayVersion = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion').DisplayVersion
            }
            $ObjectOutput | Add-Member -MemberType NoteProperty -Name DisplayVersion -Value $DisplayVersion

            # --- 5. DISK INFO INTEGRATION (Refactored to use Get-CimInstance) ---

            # Get all physical disks
            $disks = Get-CimInstance -ClassName Win32_DiskDrive @CimParams

            # Get logical disk info for size and free space (DriveType=3 is Local Disk)
            $logicalDisks = Get-CimInstance -ClassName Win32_LogicalDisk @CimParams -Filter "DriveType=3"

            $diskInfo = @()
            $totalSizeSum = 0
            $freeSizeSum = 0

            foreach ($disk in $disks) {
                # Get the partitions on the current physical disk
                $partitions = Get-CimInstance -ClassName Win32_DiskPartition @CimParams | Where-Object { $_.DiskIndex -eq $disk.Index }

                foreach ($partition in $partitions) {
                    # Get the link between the partition and the logical disk (volume)
                    # Note: We can often use Get-CimAssociatedInstance here, but for simplicity
                    # we will stick to the WMI chaining logic, ensuring CIM is used.
                    $link = Get-CimInstance -ClassName Win32_LogicalDiskToPartition @CimParams |
                            Where-Object { $_.Antecedent -like "*$($partition.DeviceID)*" }

                    if ($link) {
                        # Extract drive letter (DeviceID property from Dependent string)
                        $driveLetter = ($link.Dependent -split '"')[1]
                        $logical = $logicalDisks | Where-Object { $_.DeviceID -eq $driveLetter }

                        if ($logical) {
                            $diskObj = [PSCustomObject]@{
                                SerialNumber = $disk.SerialNumber
                                DiskModel    = $disk.Model
                                DriveLetter  = $logical.DeviceID
                                TotalSizeGB  = [math]::Round($logical.Size / 1GB, 2)
                                FreeSpaceGB  = [math]::Round($logical.FreeSpace / 1GB, 2)
                            }

                            $diskInfo += $diskObj
                            $totalSizeSum += [long]$logical.Size
                            $freeSizeSum += [long]$logical.FreeSpace
                        }
                    }
                }
            }

            # Add summary properties to the output object
            $ObjectOutput | Add-Member -MemberType NoteProperty -Name DisksJSON -Value ($diskInfo | ConvertTo-Json -Compress)
            $ObjectOutput | Add-Member -MemberType NoteProperty -Name DisksTotalSizeGB -Value ([math]::Round($totalSizeSum / 1GB, 2))
            $ObjectOutput | Add-Member -MemberType NoteProperty -Name DisksTotalFreeGB -Value ([math]::Round($freeSizeSum / 1GB, 2))


            # --- 6. Finalizing Loop ---
            $HWInfoArray += $ObjectOutput
        }
        catch
        {
            # Log the error and move to the next computer
            Write-RelaxedIT -logtext "Error while querying info for ""$Computer"": $($_.Exception.Message)" -Color Red
            # Add an object to the array indicating failure
            $ErrorObject = [PSCustomObject]@{
                ComputerName = $Computer.ToUpper()
                Status       = "Error: $($_.Exception.Message)"
                # Ensure all expected properties are present
                BIOSVersion = $null; SerialNumber = $null; Manufacturer = $null; Model = $null;
                SystemFamily = $null; CPUJSON = $null; RAM_GB = $null; ProductName = $null;
                CurrentBuildNumber = $null; DisplayVersion = $null;
                DisksJSON = $null; DisksTotalSizeGB = $null; DisksTotalFreeGB = $null
            }
            $HWInfoArray += $ErrorObject
        }
    }

    return $HWInfoArray
}

function Convert-IpRangeToCidr {
    param(
        [Parameter(Mandatory=$true)][string]$StartIP,
        [Parameter(Mandatory=$true)][string]$EndIP
    )

    # Convert IPv4 to UInt32
    function IPToUInt32([string]$ip) {
        $bytes = [System.Net.IPAddress]::Parse($ip).GetAddressBytes()
        [Array]::Reverse($bytes) # little-endian to match UInt32
        return [BitConverter]::ToUInt32($bytes,0)
    }

    # Convert UInt32 to IPv4 string
    function UInt32ToIP([uint32]$int) {
        $bytes = [BitConverter]::GetBytes($int)
        [Array]::Reverse($bytes)
        return ([System.Net.IPAddress]::new($bytes)).ToString()
    }

    # Count trailing zero bits (0..32)
    function GetTrailingZeroCount([uint32]$value) {
        if ($value -eq 0) { return 32 }
        $count = 0
        while ((($value -shr $count) -band 1) -eq 0) { $count++ }
        return $count
    }

    # Highest power-of-two <= n, returns exponent (log2)
    function FloorLog2([uint64]$n) {
        $k = 0
        while ((1 -shl ($k+1)) -le $n) { $k++ }
        return $k
    }

    $start = IPToUInt32 $StartIP
    $end   = IPToUInt32 $EndIP

    if ($start -gt $end) { throw "StartIP must be <= EndIP." }

    $cidrs = @()

    while ($start -le $end) {
        # Alignment-constrained prefix
        $tz = GetTrailingZeroCount $start           # alignment in bits
        $prefixAlign = 32 - $tz

        # Remaining-size-constrained prefix
        $remaining = [uint64]($end - $start + 1)
        $exp = FloorLog2 $remaining                 # block size exponent
        $prefixSize = 32 - [int]$exp

        # Take the stricter (larger) prefix length
        $prefix = [Math]::Max($prefixAlign, $prefixSize)

        # Emit block
        $cidrs += "$(UInt32ToIP $start)/$prefix"

        # Advance by block size
        $blockSize = [uint32](1 -shl (32 - $prefix))
        $start = $start + $blockSize
    }

    return $cidrs
}

function New-AustriaFirewallRules {
<#
.SYNOPSIS
Creates Windows Firewall rules to allow the Minecraft Bedrock Server.
 
.PARAMETER Name
The base name for the firewall rules (e.g., "Minecraft").
 
.PARAMETER MinecraftExePath
The full path to the server executable file.
 
.EXAMPLE
    New-MinecraftFirewallRules -Name "BedrockServer" -MinecraftExePath "D:\Servers\bedrock_server.etxe" -Ports 25565
    New-MinecraftFirewallRules -Name "Minecraft" -MinecraftExePath "C:\MineCraft\bedrock-server-latest\bedrock_server.exe" -Ports @(19132, 19133)
    New-MinecraftFirewallRules -Name "MinecraftJAVA" -MinecraftExePath "C:\MineCraft\java-server\bedrock_server.exe" -Ports @(25565, 25575, 19132, 19133)
#>

    param(
        [Parameter(Mandatory=$true)]
        [string]$Name = "Minecraft",

        [Parameter(Mandatory=$true)]
        [string]$MinecraftExePath = "C:\MineCraft\bedrock-server\bedrock_server.exe",

        [Parameter(Mandatory=$true)]
        [int[]]$Ports = @(25565, 25575, 19132, 19133),
        [Parameter(Mandatory=$false)]
        [string]$ipfile ="at.csv"
        # https://www.nirsoft.net/countryip/at.html
    )


    $AustriaIPs = @()
    # Path to at.csv is assumed to be relative to where the script is run
    $CsvPath = Join-Path (Split-Path $MyInvocation.MyCommand.Path) $ipfile

    if (-not (Test-Path $CsvPath)) {
        Write-RelaxedIT -logtext "ERROR: The required IP CSV file '$ipfile' was not found at '$CsvPath'. Returning without creating rules."
        return
    }

    Import-Csv $CsvPath -Header StartIP,EndIP,Count,Date,Provider | ForEach-Object {
        $AustriaIPs += Convert-IpRangeToCidr -StartIP $_.StartIP -EndIP $_.EndIP
    }

    Write-RelaxedIT -logtext "ℹ️ Found $($AustriaIPs.count) CIDR blocks for Austria IPs."

    # Internal/private ranges (RFC1918)
    $InternalIPs = @(
        "10.0.0.0/8",
        "172.16.0.0/12",
        "192.168.0.0/16"
    )

    # Cleanup old rules
    Write-RelaxedIT -logtext "Cleaning up old firewall rules prefixed with '$Name'..."
    Get-NetFirewallRule -DisplayName "$Name Austria Public TCP" -ErrorAction SilentlyContinue | Remove-NetFirewallRule -Confirm:$false
    Get-NetFirewallRule -DisplayName "$Name Austria Public UDP" -ErrorAction SilentlyContinue | Remove-NetFirewallRule -Confirm:$false
    Get-NetFirewallRule -DisplayName "$Name Internal TCP" -ErrorAction SilentlyContinue | Remove-NetFirewallRule -Confirm:$false
    Get-NetFirewallRule -DisplayName "$Name Internal UDP" -ErrorAction SilentlyContinue | Remove-NetFirewallRule -Confirm:$false
    Write-RelaxedIT -logtext "Cleanup complete."

    # Austria rules (Public profile only)
    Write-RelaxedIT -logtext "Adding rules for Public Profile (Austria IPs)..."
    New-NetFirewallRule `
        -DisplayName "$Name Austria Public TCP" `
        -Direction Inbound `
        -Program $MinecraftExePath `
        -Action Allow `
        -Profile Public `
        -RemoteAddress $AustriaIPs `
        -Protocol TCP `
        -LocalPort $Ports

    New-NetFirewallRule `
        -DisplayName "$Name Austria Public UDP" `
        -Direction Inbound `
        -Program $MinecraftExePath `
        -Action Allow `
        -Profile Public `
        -RemoteAddress $AustriaIPs `
        -Protocol UDP `
        -LocalPort $Ports

    # Internal rules (Private + Domain profiles)
    Write-RelaxedIT -logtext "Adding rules for Private/Domain Profiles (Internal IPs)..."
    New-NetFirewallRule `
        -DisplayName "$Name Internal TCP" `
        -Direction Inbound `
        -Program $MinecraftExePath `
        -Action Allow `
        -Profile Private,Domain `
        -RemoteAddress $InternalIPs `
        -Protocol TCP `
        -LocalPort $Ports

    New-NetFirewallRule `
        -DisplayName "$Name Internal UDP" `
        -Direction Inbound `
        -Program $MinecraftExePath `
        -Action Allow `
        -Profile Private,Domain `
        -RemoteAddress $InternalIPs `
        -Protocol UDP `
        -LocalPort $Ports

    Write-RelaxedIT -logtext "✅ Firewall rules for $Name Server created successfully for ports $($Ports -join ', ')."
}