# Samy.Printers.ps1
# Printer config, driver, and HTTP handlers

function Get-SamyDriverRootFolder {
    [CmdletBinding()]
    param()

    $root = Join-Path $env:ProgramData 'SVS\Samy\Drivers'

    if (-not (Test-Path $root)) {
        try {
            New-Item -Path $root -ItemType Directory -Force | Out-Null
            Write-LogHybrid "Created driver root folder '$root'." Info Printers -LogToEvent
        }
        catch {
            Write-LogHybrid "Failed to create driver root folder '$root': $($_.Exception.Message)" Warning Printers -LogToEvent
        }
    }

    return $root
}

function Get-SamyDriverFolderForProfile {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][pscustomobject]$Profile
    )

    $root = Get-SamyDriverRootFolder

    if ($Profile.PSObject.Properties.Name -contains 'DriverFolderName' -and $Profile.DriverFolderName) {
        $folderName = $Profile.DriverFolderName
    }
    else {
        $folderName = "$($Profile.ClientCode)_$($Profile.ProfileName)"
    }

    $dest = Join-Path $root $folderName

    if (-not (Test-Path $dest)) {
        try {
            New-Item -Path $dest -ItemType Directory -Force | Out-Null
            Write-LogHybrid "Created driver folder '$dest'." Info Printers -LogToEvent
        }
        catch {
            Write-LogHybrid "Failed to create driver folder '$dest': $($_.Exception.Message)" Warning Printers -LogToEvent
        }
    }

    return $dest
}

function Get-SamyDriverPackageUrl {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][pscustomobject]$Profile
    )

    if ($Profile.PSObject.Properties.Name -contains 'DriverPackageUrl' -and $Profile.DriverPackageUrl) {
        return $Profile.DriverPackageUrl
    }

    if ($Profile.PSObject.Properties.Name -contains 'DriverPackagePath' -and $Profile.DriverPackagePath) {
        return "$Script:SamyRepoBase/$Script:SamyBranch/$($Profile.DriverPackagePath)?raw=1"
    }

    return $null
}

function Get-SamyClientListFromServer {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Uri,

        [Parameter(Mandatory)]
        [string]$Password
    )

    try {
        Write-LogHybrid "Calling client list service at $Uri" Info Printers -LogToEvent

        $headers = @{
            SAMYPW = $Password
        }

        $resp = Invoke-RestMethod -Uri $Uri `
                                  -Method Post `
                                  -Headers $headers `
                                  -ContentType 'application/json' `
                                  -ErrorAction Stop

        if (-not $resp) {
            Write-LogHybrid "Client list service returned no data." Warning Printers -LogToEvent
            return @()
        }

        if ($resp -is [System.Collections.IEnumerable] -and -not ($resp -is [string])) {
            return @($resp)
        }
        else {
            return ,$resp
        }
    }
    catch {
        Write-LogHybrid ("Get-SamyClientListFromServer failed for {0}: {1}" -f $Uri, $_.Exception.Message) Error Printers -LogToEvent
        return @()
    }
}

function Invoke-GetPrinters {
    param($Context)

    try {
        if ($Context.Request.HttpMethod -ne 'POST') {
            $Context.Response.StatusCode = 405
            Send-Text $Context 'Use POST'
            return
        }

        $rawBody = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd()
        if (-not $rawBody) {
            $Context.Response.StatusCode = 400
            Send-Text $Context 'Missing request body.'
            return
        }

        try {
            $body = $rawBody | ConvertFrom-Json
        }
        catch {
            $Context.Response.StatusCode = 400
            Send-Text $Context 'Invalid JSON body.'
            return
        }

        $password = $body.password
        if (-not $password) {
            $Context.Response.StatusCode = 400
            Send-Text $Context 'Password is required.'
            return
        }

        $uri = 'https://bananas.svstools.ca/getprinters'
        Write-LogHybrid "Fetching printers from $uri" Info Printers -LogToEvent

        $printers = Get-SamyClientListFromServer -Uri $uri -Password $password

        if ($null -eq $printers) {
            Write-LogHybrid "Get-SamyClientListFromServer returned `$null; sending empty JSON array." Warning Printers -LogToEvent
            $printers = @()
        }

        try {
            Update-SamyPrinterConfig -PrinterProfiles $printers -SkipIfEmpty
        }
        catch {
            Write-LogHybrid "Update-SamyPrinterConfig failed: $($_.Exception.Message)" Warning Printers -LogToEvent
        }

        Send-JSON $Context $printers
    }
    catch {
        Write-LogHybrid "Invoke-GetPrinters error: $($_.Exception.Message)" Error Printers -LogToEvent
        $Context.Response.StatusCode = 500
        Send-Text $Context "Internal server error fetching printers."
    }
}

function Invoke-InstallPrinters {
    param($Context)

    try {
        if ($Context.Request.HttpMethod -ne 'POST') {
            $Context.Response.StatusCode = 405
            Send-Text $Context 'Use POST'
            return
        }

        $rawBody = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd()
        if (-not $rawBody) {
            $Context.Response.StatusCode = 400
            Send-Text $Context 'Missing request body.'
            return
        }

        try {
            $body = $rawBody | ConvertFrom-Json
        }
        catch {
            $Context.Response.StatusCode = 400
            Send-Text $Context 'Invalid JSON body.'
            return
        }

        $printers = $body.printers
        if (-not $printers -or $printers.Count -eq 0) {
            $Context.Response.StatusCode = 400
            Send-Text $Context 'No printers specified.'
            return
        }

        Write-LogHybrid "Received request to install $($printers.Count) printers." Info Printers -LogToEvent

        $successCount = 0
        $failures     = @()

        foreach ($p in $printers) {
            $clientCode   = $p.ClientCode
            $profileName  = $p.ProfileName
            $setDefault   = $false

            if ($p.PSObject.Properties.Name -contains 'SetAsDefault' -and $p.SetAsDefault) {
                $setDefault = $true
            }

            if (-not $clientCode -or -not $profileName) {
                $msg = "Skipping printer entry because ClientCode or ProfileName is missing."
                Write-LogHybrid $msg Warning Printers -LogToEvent
                $failures += $msg
                continue
            }

            $summary = "ClientCode=$clientCode ProfileName=$profileName DisplayName=$($p.DisplayName) Location=$($p.Location) SetAsDefault=$setDefault"
            Write-LogHybrid "Installing printer ($summary)" Info Printers -LogToEvent

            try {
                Invoke-SamyPrinterInstall `
                    -ClientCode  $clientCode `
                    -ProfileName $profileName `
                    -SetAsDefault:$setDefault `
                    #-WhatIf

                $successCount++
            }
            catch {
                $errMsg = "Failed to install printer ($summary): $($_.Exception.Message)"
                Write-LogHybrid $errMsg Error Printers -LogToEvent
                $failures += $errMsg
            }
        }

        $result = @{
            SuccessCount = $successCount
            FailureCount = $failures.Count
            Failures     = $failures
            Message      = "Printer install (WHATIF) processed. Check SAMY logs for detail."
        }

        Send-JSON $Context $result
    }
    catch {
        Write-LogHybrid "Invoke-InstallPrinters error: $($_.Exception.Message)" Error Printers -LogToEvent
        $Context.Response.StatusCode = 500
        Send-Text $Context "Internal server error installing printers."
    }
}

function Get-SamyPrinterLocalConfigPath {
    [CmdletBinding()]
    param()

    $configDir = Join-Path $env:ProgramData 'SVS\Samy\Printers'

    if (-not (Test-Path $configDir)) {
        try {
            New-Item -Path $configDir -ItemType Directory -Force | Out-Null
            Write-LogHybrid "Created printer config folder at '$configDir'." Info Printers -LogToEvent
        }
        catch {
            Write-LogHybrid "Failed to create printer config folder '$configDir': $($_.Exception.Message)" Warning Printers -LogToEvent
        }
    }

    return (Join-Path $configDir 'printers.json')
}

function Get-SamyPrinterConfigFromFile {
    [CmdletBinding()]
    param()

    $path = Get-SamyPrinterLocalConfigPath

    if (-not (Test-Path $path)) {
        throw "Local printer config file not found at '$path'. Create or update printers.json first."
    }

    $json     = Get-Content -Path $path -Raw -ErrorAction Stop
    $profiles = $json | ConvertFrom-Json

    if (-not $profiles) {
        throw "Printer config file '$path' is empty or invalid JSON."
    }

    return $profiles
}

$Script:Samy_PrinterProfiles = $null

function Get-SamyPrinterProfiles {
    [CmdletBinding()]
    param(
        [string]$ClientCode
    )

    if (-not $Script:Samy_PrinterProfiles) {
        $Script:Samy_PrinterProfiles = Get-SamyPrinterConfigFromFile
    }

    $result = $Script:Samy_PrinterProfiles

    if ($PSBoundParameters.ContainsKey('ClientCode') -and $ClientCode) {
        $result = $result | Where-Object { $_.ClientCode -eq $ClientCode }
    }

    return $result
}

function Get-SamyPrinterProfile {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$ClientCode,
        [Parameter(Mandatory)][string]$ProfileName
    )

    $profiles = Get-SamyPrinterProfiles -ClientCode $ClientCode
    $match    = $profiles | Where-Object { $_.ProfileName -eq $ProfileName }

    if (-not $match) {
        throw "No printer profile found for ClientCode '$ClientCode' and ProfileName '$ProfileName'."
    }

    if ($match.Count -gt 1) {
        throw "Multiple printer profiles found for ClientCode '$ClientCode' and ProfileName '$ProfileName'. De-duplicate in printers.json."
    }

    return $match
}

function Ensure-SamyPrinterDriver {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [pscustomobject]$Profile
    )

    $driverName = $Profile.DriverName
    if (-not $driverName) {
        throw "Profile '$($Profile.ProfileName)' has no DriverName defined in printer config."
    }

    $existingDriver = Get-PrinterDriver -Name $driverName -ErrorAction SilentlyContinue
    if ($existingDriver) {
        Write-LogHybrid "Printer driver '$driverName' already installed." Info Printers -LogToEvent
        return
    }

    Write-LogHybrid "Printer driver '$driverName' not found. Preparing to install." Info Printers -LogToEvent

    $localDriverRoot = Get-SamyDriverFolderForProfile -Profile $Profile

    $infPath = $null
    if ($Profile.PSObject.Properties.Name -contains 'DriverInfPath' -and $Profile.DriverInfPath) {
        if (Test-Path $Profile.DriverInfPath) {
            $infPath = $Profile.DriverInfPath
            Write-LogHybrid "Using existing INF path '$infPath' for driver '$driverName'." Info Printers -LogToEvent
        }
        else {
            Write-LogHybrid "Configured DriverInfPath '$($Profile.DriverInfPath)' does not exist, will try repo download." Warning Printers -LogToEvent
        }
    }

    $packageDownloaded = $false

    if ($Profile.PSObject.Properties.Name -contains 'DriverPackagePath' -and $Profile.DriverPackagePath) {
        $driverPackageUrl = "$Script:SamyRepoBase/$Script:SamyBranch/$($Profile.DriverPackagePath)?raw=1"
        $localZip         = Join-Path $localDriverRoot "package.zip"

        Write-LogHybrid "Attempting to download driver package from $driverPackageUrl." Info Printers -LogToEvent

        try {
            Invoke-WebRequest -Uri $driverPackageUrl -OutFile $localZip -UseBasicParsing -ErrorAction Stop
            Write-LogHybrid "Downloaded driver package from $driverPackageUrl to $localZip." Success Printers -LogToEvent
            $packageDownloaded = $true
        }
        catch [System.Net.WebException] {
            $response   = $_.Exception.Response
            $statusCode = $null
            if ($response -and $response.StatusCode) {
                $statusCode = [int]$response.StatusCode
            }

            if ($statusCode -eq 404) {
                Write-LogHybrid "Driver package not found at $driverPackageUrl (404). Falling back to INF-only install for '$($Profile.DisplayName)'." Warning Printers -LogToEvent
            }
            else {
                Write-LogHybrid "Driver package download failed ($statusCode) from $($driverPackageUrl): $($_.Exception.Message)" Error Printers -LogToEvent
                throw "Failed to download driver package from $($driverPackageUrl): $($_.Exception.Message)"
            }
        }
        catch {
            Write-LogHybrid "Driver package download failed from $($driverPackageUrl): $($_.Exception.Message)" Error Printers -LogToEvent
            throw "Failed to download driver package from $($driverPackageUrl): $($_.Exception.Message)"
        }
    }
    else {
        Write-LogHybrid "No DriverPackagePath defined for '$($Profile.DisplayName)'; will rely on local INF." Info Printers -LogToEvent
    }

    if ($packageDownloaded) {
        try {
            Expand-Archive -Path $localZip -DestinationPath $localDriverRoot -Force
            Write-LogHybrid "Expanded driver package to '$localDriverRoot'." Info Printers -LogToEvent
        }
        catch {
            Write-LogHybrid "Failed to expand driver package '$localZip': $($_.Exception.Message)" Error Printers -LogToEvent
            throw "Failed to expand driver package '$localZip': $($_.Exception.Message)"
        }

        if (-not $infPath) {
            if ($Profile.PSObject.Properties.Name -contains 'DriverInfName' -and $Profile.DriverInfName) {

                $candidateInf = Join-Path $localDriverRoot $Profile.DriverInfName
                if (Test-Path $candidateInf) {
                    $infPath = $candidateInf
                    Write-LogHybrid "Resolved INF from package as '$infPath' using DriverInfName '$($Profile.DriverInfName)' at root." Info Printers -LogToEvent
                }
                else {
                    Write-LogHybrid "Expected INF '$candidateInf' not found at root; searching recursively..." Warning Printers -LogToEvent

                    $found = Get-ChildItem -Path $localDriverRoot -Recurse -Filter $Profile.DriverInfName -File -ErrorAction SilentlyContinue |
                             Select-Object -First 1

                    if ($found) {
                        $infPath = $found.FullName
                        Write-LogHybrid "Resolved INF from package as '$infPath' (found by recursive search for '$($Profile.DriverInfName)')." Info Printers -LogToEvent
                    }
                    else {
                        Write-LogHybrid "Could not find any '$($Profile.DriverInfName)' under '$localDriverRoot' after expanding package." Error Printers -LogToEvent
                    }
                }
            }
            else {
                Write-LogHybrid "DriverInfName not defined for profile '$($Profile.ProfileName)'; cannot auto-resolve INF from expanded package." Warning Printers -LogToEvent
            }
        }
    }

    if (-not $infPath -or -not (Test-Path $infPath)) {
        throw "Driver '$driverName' is not installed and no valid DriverInfPath or usable driver package is available for profile '$($Profile.ProfileName)'."
    }

    Write-LogHybrid "Installing printer driver '$driverName' from '$infPath'." Info Printers -LogToEvent

    $pnputilCmd = "pnputil.exe /add-driver `"$infPath`" /install"
    Write-LogHybrid "Running: $pnputilCmd" Info Printers -LogToEvent

    $pnputilOutput = & pnputil.exe /add-driver "`"$infPath`"" /install 2>&1
    $exitCode      = $LASTEXITCODE

    Write-LogHybrid "pnputil exit code: $exitCode. Output:`n$pnputilOutput" Info Printers -LogToEvent

    if ($exitCode -ne 0) {
        throw "pnputil failed with exit code $exitCode installing '$driverName' from '$infPath'."
    }

    $existingDriver = Get-PrinterDriver -ErrorAction SilentlyContinue | Where-Object {
        $_.Name -eq $driverName -or
        $_.Name -like "*$driverName*" -or
        $driverName -like "*$($_.Name)*"
    }

    if (-not $existingDriver) {
        $sharpDrivers = Get-PrinterDriver -ErrorAction SilentlyContinue |
                        Where-Object { $_.Name -like "SHARP*" }

        $sharpList = if ($sharpDrivers) {
            ($sharpDrivers | Select-Object -ExpandProperty Name) -join ', '
        }
        else {
            '(none)'
        }

        Write-LogHybrid "After pnputil, driver '$driverName' not found. Existing SHARP drivers: $sharpList" Warning Printers -LogToEvent
    }
    else {
        Write-LogHybrid "Printer driver '$($existingDriver.Name)' is present after pnputil (requested '$driverName')." Success Printers -LogToEvent
    }
}

function Install-SamyTcpIpPrinter {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [pscustomobject]$Profile,

        [switch]$SetAsDefault
    )

    $portName    = $Profile.Address
    $printerName = $Profile.DisplayName

    if (-not $portName) {
        throw "TCP/IP printer profile '$($Profile.ProfileName)' is missing Address in printer config."
    }

    if (-not (Get-PrinterPort -Name $portName -ErrorAction SilentlyContinue)) {
        Write-Verbose "Creating TCP/IP port '$portName'."
        Add-PrinterPort -Name $portName -PrinterHostAddress $Profile.Address
    }
    else {
        Write-Verbose "TCP/IP port '$portName' already exists."
    }

    $existingPrinter = Get-Printer -Name $printerName -ErrorAction SilentlyContinue
    if ($existingPrinter) {
        Write-Verbose "Printer '$printerName' already exists. Skipping creation."
    }
    else {
        Write-Verbose "Creating printer '$printerName' on port '$portName' using driver '$($Profile.DriverName)'."
        Add-Printer -Name $printerName -PortName $portName -DriverName $Profile.DriverName
    }

    if ($SetAsDefault -or $Profile.IsDefault) {
        Write-Verbose "Setting '$printerName' as default printer."
        (New-Object -ComObject WScript.Network).SetDefaultPrinter($printerName)
    }
}

function Install-SamySharedPrinter {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [pscustomobject]$Profile,

        [switch]$SetAsDefault
    )

    if (-not $Profile.PrintServer -or -not $Profile.ShareName) {
        throw "Shared printer profile '$($Profile.ProfileName)' is missing PrintServer or ShareName in printer config."
    }

    $connectionName = "\\$($Profile.PrintServer)\$($Profile.ShareName)"

    $existing = Get-Printer -ErrorAction SilentlyContinue |
        Where-Object {
            $_.Name -eq $Profile.DisplayName -or
            $_.ShareName -eq $Profile.ShareName
        }

    $printerName = $null

    if ($existing) {
        Write-Verbose "Shared printer '$($Profile.DisplayName)' already connected as '$($existing.Name)'."
        $printerName = $existing.Name
    }
    else {
        Write-Verbose "Adding shared printer connection '$connectionName'."
        Add-Printer -ConnectionName $connectionName

        $printerName = (Get-Printer |
            Where-Object { $_.Name -like "*$($Profile.ShareName)*" } |
            Select-Object -First 1
        ).Name
    }

    if ($SetAsDefault -or $Profile.IsDefault) {
        Write-Verbose "Setting '$printerName' as default printer."
        (New-Object -ComObject WScript.Network).SetDefaultPrinter($printerName)
    }
}

function Invoke-SamyPrinterInstall {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory)]
        [string]$ClientCode,

        [Parameter(Mandatory)]
        [string]$ProfileName,

        [switch]$SetAsDefault
    )

    try {
        $profile    = Get-SamyPrinterProfile -ClientCode $ClientCode -ProfileName $ProfileName
        $targetName = $profile.DisplayName

        if ($PSCmdlet.ShouldProcess($targetName, "Install printer")) {

            Write-LogHybrid "Installing printer profile '$($profile.ProfileName)' for client '$ClientCode' (WhatIf=$WhatIfPreference)." Info Printers -LogToEvent

            Ensure-SamyPrinterDriver -Profile $profile

            switch ($profile.Type) {
                'TcpIp' {
                    Install-SamyTcpIpPrinter -Profile $profile -SetAsDefault:$SetAsDefault
                }
                'Shared' {
                    Install-SamySharedPrinter -Profile $profile -SetAsDefault:$SetAsDefault
                }
                default {
                    throw "Unsupported printer Type '$($profile.Type)' in profile '$($profile.ProfileName)'. Use 'TcpIp' or 'Shared'."
                }
            }

            Write-LogHybrid "Installed printer '$($profile.DisplayName)' (Client=$ClientCode Profile=$ProfileName)." Info Printers -LogToEvent
        }
    }
    catch {
        Write-LogHybrid (
            "Printer install failed for Client={0} Profile={1}: {2}" -f $ClientCode, $ProfileName, $_.Exception.Message
        ) Error Printers -LogToEvent
        throw
    }
}

function Update-SamyPrinterConfig {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object]$PrinterProfiles,

        [switch]$SkipIfEmpty
    )

    $path = Get-SamyPrinterLocalConfigPath

    $profilesArray = @($PrinterProfiles)

    if ($SkipIfEmpty -and ($null -eq $PrinterProfiles -or $profilesArray.Count -eq 0)) {
        Write-LogHybrid "Update-SamyPrinterConfig: no printer profiles returned; keeping existing printers.json." Warning Printers -LogToEvent
        return
    }

    if ($profilesArray.Count -eq 0) {
        Write-LogHybrid "Update-SamyPrinterConfig: zero profiles; writing empty printers.json." Warning Printers -LogToEvent
    }

    try {
        $profilesArray | ConvertTo-Json -Depth 5 | Set-Content -Path $path -Encoding UTF8
        Write-LogHybrid "Saved $($profilesArray.Count) printer profiles to '$path'." Success Printers -LogToEvent

        $Script:Samy_PrinterProfiles = $null
    }
    catch {
        Write-LogHybrid "Failed to write printers.json to '$path': $($_.Exception.Message)" Error Printers -LogToEvent
    }
}
