@echo off
setlocal EnableExtensions
title Linkly Port Change
cd /d "%~dp0"

:: One-click: self-elevates, then runs the PowerShell embedded below.
:: Windows 7 (PowerShell 2.0) through Windows 11.

net session >nul 2>&1
if %errorlevel% neq 0 (
    echo Requesting Administrator rights...
    powershell.exe -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs"
    if errorlevel 1 (
        echo Failed to elevate. Right-click this file and choose "Run as administrator".
        pause
    )
    exit /b
)

set "LINKLY_SELF=%~f0"
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "& {$p=$env:LINKLY_SELF;$t=[System.IO.File]::ReadAllText($p);$m=':::'+'LINKLY_PORT_CHANGE_PS'+':::' ;$i=$t.LastIndexOf($m);if($i -lt 0){throw 'Embedded script marker not found.'};Invoke-Expression $t.Substring($i+$m.Length)}"
set ERR=%ERRORLEVEL%
if %ERR% neq 0 (
    echo.
    echo Script exited with error %ERR%.
    pause
)
exit /b %ERR%

:::LINKLY_PORT_CHANGE_PS:::
# Compatible with Windows 7 (PowerShell 2.0 / .NET 3.5) through Windows 11.
Set-StrictMode -Version 2
$ErrorActionPreference = 'Stop'

function Test-Blank {
    param($Value)
    if ($null -eq $Value) { return $true }
    return [string]::IsNullOrEmpty($Value.ToString().Trim())
}

function Wait-AnyKey {
    param([string]$Message = 'Press any key to exit...')
    Write-Host ''
    Write-Host $Message
    try {
        $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
    } catch {
        Read-Host 'Press Enter to exit'
    }
}

function Test-IsAdministrator {
    $id = [Security.Principal.WindowsIdentity]::GetCurrent()
    $principal = New-Object Security.Principal.WindowsPrincipal($id)
    return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}

function Write-Step {
    param([string]$Text)
    Write-Host ''
    Write-Host "=== $Text ===" -ForegroundColor Cyan
}

function Write-Ok {
    param([string]$Text)
    Write-Host "  [OK] $Text" -ForegroundColor Green
}

function Write-Warn {
    param([string]$Text)
    Write-Host "  [WARN] $Text" -ForegroundColor Yellow
}

function Write-Fail {
    param([string]$Text)
    Write-Host "  [FAIL] $Text" -ForegroundColor Red
}

function Convert-RegistryPortToDecimal {
    param($RawValue)
    if ($null -eq $RawValue) { return $null }
    if ($RawValue -is [int] -or $RawValue -is [long]) {
        return [int]$RawValue
    }
    $text = [string]$RawValue
    $text = $text.Trim()
    if (Test-Blank $text) { return $null }
    if ($text -match '^0x') {
        return [Convert]::ToInt32($text, 16)
    }
    $parsed = 0
    if ([int]::TryParse($text, [ref]$parsed)) {
        return $parsed
    }
    throw "IP_INTERFACE_PORT value '$text' is not a number."
}

function Get-ProcessIds {
    param($ProcessList)
    $ids = @()
    foreach ($p in @($ProcessList)) {
        if ($null -ne $p) { $ids += $p.Id }
    }
    return $ids
}

function Get-CullenHive {
    $is64Process = [IntPtr]::Size -eq 8
    if ($is64Process) {
        $candidates = @(
            @{ SubKey = 'SOFTWARE\WOW6432Node\CullenSoftwareDesign'; Label = 'WOW6432Node (32-bit client)'; Display = 'HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\CullenSoftwareDesign' },
            @{ SubKey = 'SOFTWARE\CullenSoftwareDesign';            Label = 'native 64-bit';              Display = 'HKEY_LOCAL_MACHINE\SOFTWARE\CullenSoftwareDesign' }
        )
    } else {
        $candidates = @(
            @{ SubKey = 'SOFTWARE\CullenSoftwareDesign';            Label = '32-bit SOFTWARE (WOW64 redirected)'; Display = 'HKEY_LOCAL_MACHINE\SOFTWARE\CullenSoftwareDesign' },
            @{ SubKey = 'SOFTWARE\WOW6432Node\CullenSoftwareDesign'; Label = 'WOW6432Node';                         Display = 'HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\CullenSoftwareDesign' }
        )
    }

    $hklm = [Microsoft.Win32.Registry]::LocalMachine
    foreach ($c in $candidates) {
        $key = $null
        try {
            $key = $hklm.OpenSubKey($c.SubKey, $true)
        } catch {
            $key = $null
        }
        if ($null -eq $key) { continue }

        $obj = New-Object PSObject
        $obj | Add-Member NoteProperty Key     $key
        $obj | Add-Member NoteProperty SubKey  $c.SubKey
        $obj | Add-Member NoteProperty Label   $c.Label
        $obj | Add-Member NoteProperty Display $c.Display
        return $obj
    }
    return $null
}

function Get-InstallDirCandidates {
    param([string]$FromRegistry)

    $list = @()
    if (-not (Test-Blank $FromRegistry)) {
        $list += $FromRegistry.Trim().TrimEnd([char]'\')
    }

    $roots = @(
        'C:\PC_EFT',
        'C:\PC-EFT',
        'C:\PC-EFTPOS',
        'C:\Linkly',
        'C:\Program Files\PC-EFTPOS',
        'C:\Program Files\Linkly',
        'C:\Program Files\PC_EFT',
        'C:\Program Files\PC-EFT',
        'C:\Program Files (x86)\PC-EFTPOS',
        'C:\Program Files (x86)\Linkly',
        'C:\Program Files (x86)\PC_EFT',
        'C:\Program Files (x86)\PC-EFT'
    )
    foreach ($r in $roots) { $list += $r }
    return $list
}

function Find-EftClntUi {
    param([string]$InstallDir)

    foreach ($dir in (Get-InstallDirCandidates -FromRegistry $InstallDir)) {
        if (Test-Blank $dir) { continue }
        $direct = Join-Path $dir 'EftClntUI.exe'
        if (Test-Path -LiteralPath $direct) {
            return (Get-Item -LiteralPath $direct).FullName
        }
    }

    foreach ($dir in (Get-InstallDirCandidates -FromRegistry $InstallDir)) {
        if (-not (Test-Path -LiteralPath $dir)) { continue }
        $found = Get-ChildItem -LiteralPath $dir -Filter 'EftClntUI.exe' -Recurse -ErrorAction SilentlyContinue |
            Where-Object { -not $_.PSIsContainer } |
            Select-Object -First 1
        if ($found) { return $found.FullName }
    }
    return $null
}

function Get-LinklyService {
    $wantedDisplay = @('PC-EFTPOS Client', 'Linkly Client')
    $services = @(Get-WmiObject -Class Win32_Service -ErrorAction Stop)
    foreach ($want in $wantedDisplay) {
        foreach ($svc in $services) {
            if ($null -eq $svc) { continue }
            $dn = [string]$svc.DisplayName
            if ($dn.Trim() -eq $want) { return $svc }
        }
    }
    return $null
}

function Get-ExeDirectoryFromServicePath {
    param([string]$PathName)
    if (Test-Blank $PathName) { return $null }
    $exe = $null
    if ($PathName -match '^"([^"]+)"') {
        $exe = $Matches[1]
    } else {
        $exe = ($PathName -split '\s+')[0]
    }
    if (Test-Blank $exe) { return $null }
    return Split-Path -Parent $exe
}

function Stop-ClientGui {
    $killed = @()
    $procs = @(Get-WmiObject -Class Win32_Process -Filter "Name = 'EftClntUI.exe'" -ErrorAction SilentlyContinue)

    foreach ($p in $procs) {
        if ($null -eq $p) { continue }
        Write-Host "  Stopping GUI PID $($p.ProcessId) ($($p.Name))"
        $result = $p.Terminate()
        $code = 0
        if ($null -ne $result) { $code = [int]$result.ReturnValue }
        if ($code -eq 0) {
            $killed += $p.ProcessId
        } else {
            Write-Warn "Terminate returned $code for PID $($p.ProcessId); trying Stop-Process."
            Stop-Process -Id $p.ProcessId -Force -ErrorAction SilentlyContinue
            $killed += $p.ProcessId
        }
    }

    $deadline = (Get-Date).AddSeconds(10)
    do {
        $still = @(Get-Process -Name 'EftClntUI' -ErrorAction SilentlyContinue)
        if ($still.Count -eq 0) { break }
        Start-Sleep -Milliseconds 250
    } while ((Get-Date) -lt $deadline)

    $leftover = @(Get-Process -Name 'EftClntUI' -ErrorAction SilentlyContinue)
    if ($leftover.Count -gt 0) {
        $ids = Get-ProcessIds $leftover
        throw "GUI process still running: $($ids -join ', ')"
    }
    return $killed
}

function Wait-ServiceStatus {
    param(
        [string]$Name,
        [string]$Status,
        [int]$TimeoutSeconds = 45
    )
    $deadline = (Get-Date).AddSeconds($TimeoutSeconds)
    do {
        $svc = Get-Service -Name $Name -ErrorAction Stop
        if ($svc.Status -eq $Status) { return $svc }
        Start-Sleep -Milliseconds 400
    } while ((Get-Date) -lt $deadline)
    $svc = Get-Service -Name $Name -ErrorAction Stop
    throw "Service '$Name' did not reach $Status (current: $($svc.Status))."
}

function Restart-LinklyService {
    param($WmiService)

    $name = $WmiService.Name
    $display = $WmiService.DisplayName
    Write-Host "  Restarting Windows service: $display  (internal name: $name)"

    $svc = Get-Service -Name $name -ErrorAction Stop
    if ($svc.Status -eq 'Running') {
        Write-Host '  Stopping service...'
        Stop-Service -Name $name -Force -ErrorAction Stop
        Wait-ServiceStatus -Name $name -Status Stopped | Out-Null
        Write-Ok 'Service stopped.'
    } else {
        Write-Host "  Service is currently $($svc.Status)."
    }

    Write-Host '  Starting service...'
    Start-Service -Name $name -ErrorAction Stop
    Wait-ServiceStatus -Name $name -Status Running | Out-Null
    Write-Ok 'Service is running.'
}

function Add-FirewallPortRule {
    param(
        [int]$Port = 2012,
        [string]$RuleName = 'Linkly / PC-EFTPOS TCP 2012 Inbound'
    )
    
    # PowerShell 3.0+ check
    if (Get-Command -Name 'Get-NetFirewallRule' -ErrorAction SilentlyContinue) {
        $existing = Get-NetFirewallRule -DisplayName $RuleName -ErrorAction SilentlyContinue
        if ($null -eq $existing) {
            New-NetFirewallRule -DisplayName $RuleName -Direction Inbound -Action Allow -Protocol TCP -LocalPort $Port | Out-Null
            Write-Ok "Created Windows Firewall inbound rule '$RuleName' for TCP port $Port."
        } else {
            Enable-NetFirewallRule -DisplayName $RuleName | Out-Null
            Write-Ok "Inbound firewall rule '$RuleName' already exists. Verified enabled."
        }
    } else {
        # Fallback to netsh for PowerShell 2.0 / Windows 7
        $checkNetsh = netsh advfirewall firewall show rule name="$RuleName" 2>&1
        if ($checkNetsh -like "*No rules match*") {
            netsh advfirewall firewall add rule name="$RuleName" dir=in action=allow protocol=TCP localport=$Port | Out-Null
            Write-Ok "Created Windows Firewall inbound rule '$RuleName' for TCP port $Port (netsh)."
        } else {
            Write-Ok "Inbound firewall rule '$RuleName' already exists."
        }
    }
}

if (-not (Test-IsAdministrator)) {
    $self = $env:LINKLY_SELF
    if (Test-Blank $self) {
        Write-Fail 'Please right-click this file and choose Run as administrator.'
        Wait-AnyKey
        exit 1
    }
    Start-Process -FilePath $self -Verb RunAs
    exit
}

try { $Host.UI.RawUI.WindowTitle = 'Linkly Port Change' } catch { }
Write-Host 'Linkly / PC-EFTPOS  -  PINPAD PORT = TCPIP' -ForegroundColor White
Write-Host ('-' * 52)
Write-Host ("  Windows  " + [Environment]::OSVersion.Version.ToString() + "   PowerShell  " + $PSVersionTable.PSVersion.ToString())

$hive = $null
$exitCode = 0
$missingLinkly = $false

try {
    Write-Step '1. Read IP_INTERFACE_PORT'

    $hive = Get-CullenHive
    if ($null -eq $hive) {
        Write-Fail 'CullenSoftwareDesign was not found under:'
        Write-Host '    HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\CullenSoftwareDesign'
        Write-Host '    HKEY_LOCAL_MACHINE\SOFTWARE\CullenSoftwareDesign'
        Write-Host ''
        Write-Host 'install linkly first' -ForegroundColor Yellow
        $missingLinkly = $true
        $exitCode = 1
    }

    if (-not $missingLinkly) {
        Write-Ok ("Found " + $hive.Display + "  [" + $hive.Label + "]")

        $clientKey = $null
        try { $clientKey = $hive.Key.OpenSubKey('EFTCLIENT\CLIENT', $false) } catch { $clientKey = $null }

        $portSource = 'default (IP_INTERFACE_PORT not present)'
        $port = 2011
        $installDir = $null
        $rawPort = $null

        if ($null -eq $clientKey) {
            Write-Warn 'CLIENT key not found. Using default port 2011.'
        } else {
            $valueNames = @($clientKey.GetValueNames())
            if ($valueNames -contains 'IP_INTERFACE_PORT') {
                $rawPort = $clientKey.GetValue('IP_INTERFACE_PORT')
                $kind = $clientKey.GetValueKind('IP_INTERFACE_PORT')
                $port = Convert-RegistryPortToDecimal -RawValue $rawPort
                $portSource = "registry $kind"
            } else {
                Write-Warn 'IP_INTERFACE_PORT does not exist. Using default 2011.'
            }
            $installDir = [string]$clientKey.GetValue('INSTALL_DIR')
            $clientKey.Close()
        }

        Write-Host ''
        Write-Host "  port number = $port (dec)" -ForegroundColor Green
        Write-Host "  source      = $portSource"
        if ($rawPort -is [int] -or $rawPort -is [long]) {
            Write-Host ("  registry    = 0x{0:X}" -f [int]$rawPort)
        }
        if (-not (Test-Blank $installDir)) {
            Write-Host "  INSTALL_DIR = $installDir"
        }

        Write-Step '2. Set PINPAD PORT = TCPIP'

        $pinpadKey = $hive.Key.CreateSubKey('EFTCLIENT\PINPAD')
        if ($null -eq $pinpadKey) {
            throw "Could not create or open $($hive.Display)\EFTCLIENT\PINPAD"
        }

        $existing = $pinpadKey.GetValue('PORT')
        $pinpadKey.SetValue('PORT', 'TCPIP', [Microsoft.Win32.RegistryValueKind]::String)
        $pinpadKey.Flush()
        $written = [string]$pinpadKey.GetValue('PORT')
        $pinpadKey.Close()

        if ($written -ne 'TCPIP') {
            throw "Write verification failed. PORT is '$written'."
        }

        $pinpadDisplay = $hive.Display + '\EFTCLIENT\PINPAD'
        if (-not (Test-Blank $existing)) {
            Write-Ok "Updated $pinpadDisplay  PORT: '$existing' -> 'TCPIP'"
        } else {
            Write-Ok "Created $pinpadDisplay  PORT = 'TCPIP' (REG_SZ)"
        }

        Write-Step '3. Configure Firewall Rule (Allow TCP 2012 Inbound)'

        Add-FirewallPortRule -Port 2012

        Write-Step '4. Find PC_EFT / Linkly (EftClntUI.exe)'

        $wmiSvc = $null
        try {
            $wmiSvc = Get-LinklyService
        } catch {
            Write-Warn $_.Exception.Message
        }

        if ($null -ne $wmiSvc -and -not (Test-Blank $wmiSvc.PathName)) {
            $svcDir = Get-ExeDirectoryFromServicePath -PathName $wmiSvc.PathName
            if ((-not (Test-Blank $svcDir)) -and (Test-Blank $installDir)) { $installDir = $svcDir }
            Write-Host "  Service binary folder: $svcDir"
        }

        $uiExe = Find-EftClntUi -InstallDir $installDir
        if (Test-Blank $uiExe) {
            Write-Fail 'EftClntUI.exe was not found in INSTALL_DIR or the known folders:'
            foreach ($d in (Get-InstallDirCandidates -FromRegistry $installDir)) {
                Write-Host "    $d"
            }
            throw 'Cannot restart the GUI without EftClntUI.exe.'
        }

        $uiDir = Split-Path -Parent $uiExe
        Write-Ok "Found $uiExe"

        Write-Step '5. Kill GUI (EftClntUI.exe) and restart service'

        $killed = @(Stop-ClientGui)
        if ($killed.Count -gt 0) {
            Write-Ok "Stopped GUI process(es): $($killed -join ', ')"
        } else {
            Write-Host '  GUI was not running.'
        }

        if ($null -eq $wmiSvc) {
            throw 'Windows service "PC-EFTPOS Client" / "Linkly Client" was not found.'
        }
        Restart-LinklyService -WmiService $wmiSvc

        Write-Step '6. Start EftClntUI.exe'

        Start-Sleep -Seconds 3
        $already = @(Get-Process -Name 'EftClntUI' -ErrorAction SilentlyContinue)
        if ($already.Count -gt 0) {
            $ids = Get-ProcessIds $already
            Write-Ok "GUI already running after service start (PID $($ids -join ', '))."
        } else {
            Write-Host "  Launching: $uiExe"
            Start-Process -FilePath $uiExe -WorkingDirectory $uiDir
            Start-Sleep -Seconds 2
            $started = @(Get-Process -Name 'EftClntUI' -ErrorAction SilentlyContinue)
            if ($started.Count -gt 0) {
                $ids = Get-ProcessIds $started
                Write-Ok "GUI started (PID $($ids -join ', '))."
            } else {
                Write-Warn 'EftClntUI.exe was launched but the process is not visible yet.'
            }
        }

        Write-Host ''
        Write-Host 'Done.' -ForegroundColor Green
        Write-Host "  port number   = $port (dec)"
        Write-Host '  PINPAD PORT   = TCPIP'
        Write-Host '  Firewall Rule = Allowed TCP 2012 Inbound'
    }
}
catch {
    $exitCode = 1
    Write-Host ''
    Write-Fail $_.Exception.Message
    if ($null -ne $_.InvocationInfo -and $null -ne $_.InvocationInfo.PositionMessage) {
        Write-Host $_.InvocationInfo.PositionMessage -ForegroundColor DarkGray
    }
}
finally {
    if ($null -ne $hive -and $null -ne $hive.Key) {
        try { $hive.Key.Close() } catch { }
    }
}

Wait-AnyKey
exit $exitCode