// Package runner executes PowerShell deployment scripts and streams log output.
package runner

import (
	"bufio"
	"context"
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"
)

// Step describes a single deployment step.
type Step struct {
	ID         string // e.g. "adminAccount"
	Num        string // display number e.g. "00"
	Name       string
	ScriptName string // e.g. "00-admin-account.ps1"
	Enabled    bool
}

// AllSteps returns the ordered list of deployment steps.
func AllSteps() []Step {
	return []Step{
		{ID: "adminAccount",    Num: "00", Name: "Admin ucet",              ScriptName: "00-admin-account.ps1"},
		{ID: "bloatware",       Num: "01", Name: "Bloatware removal",        ScriptName: "01-bloatware.ps1"},
		{ID: "software",        Num: "02", Name: "Software (winget)",         ScriptName: "02-software.ps1"},
		{ID: "systemRegistry",  Num: "03", Name: "System Registry (HKLM)",   ScriptName: "03-system-registry.ps1"},
		{ID: "defaultProfile",  Num: "04", Name: "Default Profile",          ScriptName: "04-default-profile.ps1"},
		{ID: "personalization", Num: "05", Name: "Personalizace",             ScriptName: "05-personalization.ps1"},
		{ID: "scheduledTasks",  Num: "06", Name: "Scheduled Tasks",           ScriptName: "06-scheduled-tasks.ps1"},
		{ID: "backinfo",        Num: "07", Name: "BackInfo",                  ScriptName: "07-backinfo.ps1"},
		{ID: "activation",      Num: "08", Name: "Windows aktivace",          ScriptName: "08-activation.ps1"},
		{ID: "dellUpdate",      Num: "11", Name: "Dell Command | Update",      ScriptName: "11-dell-update.ps1"},
		{ID: "network",         Num: "09", Name: "Network discovery",         ScriptName: "10-network.ps1"},
		{ID: "pcIdentity",      Num: "10", Name: "PC identita",               ScriptName: "09-pc-identity.ps1"},
	}
}

// Feature is a single toggleable sub-item within a deployment step.
type Feature struct {
	ID    string
	Label string
}

// StepFeatures returns per-step feature lists. Steps absent from this map
// have no sub-features and are controlled at the step level only.
func StepFeatures() map[string][]Feature {
	return map[string][]Feature{
		"software": {
			{ID: "wingetInstalls", Label: "Instalace SW ze seznamu (winget)"},
			{ID: "pdfDefault",     Label: "Adobe Reader jako vychozi PDF"},
			{ID: "ateraAgent",     Label: "Atera RMM agent"},
		},
		"systemRegistry": {
			{ID: "systemTweaks",      Label: "Windows tweaky (Widgets, GameDVR, Recall...)"},
			{ID: "edgePolicies",      Label: "Edge policies (tlacitka, vyhledavac, telemetrie)"},
			{ID: "oneDriveUninstall", Label: "OneDrive uninstall (consumer pre-install)"},
			{ID: "powercfg",          Label: "Nastaveni napajeni (timeout AC/DC)"},
			{ID: "proxyDisable",      Label: "Zakaz WPAD proxy auto-detect"},
		},
		"defaultProfile": {
			{ID: "taskbarTweaks",   Label: "Taskbar – zarovnani, tlacitka, layout XML"},
			{ID: "startMenuTweaks", Label: "Start menu – cisteni pinu, Bing, Copilot"},
			{ID: "explorerTweaks",  Label: "Explorer – pripony, LaunchTo, ShowRecent"},
		},
		"dellUpdate": {
			{ID: "drivers", Label: "Dell drivery + firmware"},
			{ID: "bios",    Label: "Dell BIOS update"},
		},
	}
}

// SelectableItem is a single toggleable row in the TUI checklist.
// It represents either a whole step (FeatureID == "") or a specific feature.
type SelectableItem struct {
	Key       string // "stepID" or "stepID.featureID"
	StepID    string
	FeatureID string // empty for step-level items
	Label     string
	Num       string
}

// AllSelectableItems returns the flat ordered list of all TUI toggle rows.
// Steps with features are expanded to individual feature rows.
// Steps without features appear as a single step-level row.
func AllSelectableItems() []SelectableItem {
	steps    := AllSteps()
	features := StepFeatures()
	var items []SelectableItem
	for _, s := range steps {
		feats, hasFeatures := features[s.ID]
		if !hasFeatures {
			items = append(items, SelectableItem{
				Key:    s.ID,
				StepID: s.ID,
				Label:  s.Num + " – " + s.Name,
				Num:    s.Num,
			})
		} else {
			for _, f := range feats {
				items = append(items, SelectableItem{
					Key:       s.ID + "." + f.ID,
					StepID:    s.ID,
					FeatureID: f.ID,
					Label:     s.Num + " – " + f.Label,
					Num:       s.Num,
				})
			}
		}
	}
	return items
}

// RunConfig holds runtime parameters passed to each script.
type RunConfig struct {
	ScriptsDir  string
	ConfigPath  string
	LogFile     string
	ProfileType string
}

// Result is the outcome of a single step.
type Result struct {
	Step    Step
	Status  string // "OK", "ERROR", "SKIPPED"
	Elapsed time.Duration
}

// LogLine is a single output line from a running script.
type LogLine struct {
	StepID string
	Text   string
	Level  string // INFO, OK, ERROR, WARN, STEP - parsed from [LEVEL] prefix
}

// Runner executes deployment steps sequentially.
type Runner struct {
	cfg      RunConfig
	onLog    func(LogLine)
	onResult func(Result)
	cancel   context.CancelFunc
}

// New creates a Runner. onLog is called for each output line, onResult after each step.
func New(cfg RunConfig, onLog func(LogLine), onResult func(Result)) *Runner {
	return &Runner{cfg: cfg, onLog: onLog, onResult: onResult}
}

// Run executes enabled steps sequentially. Blocks until done or context cancelled.
func (r *Runner) Run(ctx context.Context, steps []Step) []Result {
	ctx, cancel := context.WithCancel(ctx)
	r.cancel = cancel
	defer cancel()

	// Write config JSON to temp file so scripts can read it
	cfgArg := r.cfg.ConfigPath

	var results []Result
	for _, step := range steps {
		if !step.Enabled {
			res := Result{Step: step, Status: "SKIPPED"}
			r.onResult(res)
			results = append(results, res)
			continue
		}

		start := time.Now()
		err := r.runScript(ctx, step, cfgArg)
		elapsed := time.Since(start)

		status := "OK"
		if err != nil {
			if ctx.Err() != nil {
				status = "CANCELLED"
			} else {
				status = "ERROR"
			}
		}

		res := Result{Step: step, Status: status, Elapsed: elapsed}
		r.onResult(res)
		results = append(results, res)

		if ctx.Err() != nil {
			break
		}
	}
	return results
}

// Stop cancels the running deployment.
func (r *Runner) Stop() {
	if r.cancel != nil {
		r.cancel()
	}
}

func (r *Runner) runScript(ctx context.Context, step Step, cfgArg string) error {
	scriptPath := filepath.Join(r.cfg.ScriptsDir, step.ScriptName)

	// Build argument list
	args := []string{
		"-NonInteractive",
		"-ExecutionPolicy", "Bypass",
		"-File", scriptPath,
		"-LogFile", r.cfg.LogFile,
	}

	// Pass config object as JSON string (script reads it inline)
	if cfgArg != "" {
		args = append(args, "-Config", fmt.Sprintf("(Get-Content '%s' | ConvertFrom-Json)", cfgArg))
	}

	// ProfileType for step 04
	if step.ID == "defaultProfile" && r.cfg.ProfileType != "" {
		args = append(args, "-ProfileType", r.cfg.ProfileType)
	}

	cmd := exec.CommandContext(ctx, "powershell.exe", args...)

	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return err
	}
	cmd.Stderr = cmd.Stdout // merge stderr into stdout

	if err := cmd.Start(); err != nil {
		return err
	}

	scanner := bufio.NewScanner(stdout)
	for scanner.Scan() {
		line := scanner.Text()
		r.onLog(LogLine{
			StepID: step.ID,
			Text:   line,
			Level:  parseLevel(line),
		})
	}

	return cmd.Wait()
}

// parseLevel extracts the log level from lines formatted as "[HH:mm:ss] [LEVEL] message".
func parseLevel(line string) string {
	if strings.Contains(line, "] [OK]") {
		return "OK"
	}
	if strings.Contains(line, "] [ERROR]") {
		return "ERROR"
	}
	if strings.Contains(line, "] [WARN]") {
		return "WARN"
	}
	if strings.Contains(line, "] [STEP]") {
		return "STEP"
	}
	return "INFO"
}

// ExtractScripts unpacks embedded scripts to a temp directory.
// Returns the directory path. Caller is responsible for cleanup.
func ExtractScripts(fs interface{ ReadDir(string) ([]os.DirEntry, error); ReadFile(string) ([]byte, error) }, tmpDir string) error {
	entries, err := fs.ReadDir("scripts")
	if err != nil {
		return fmt.Errorf("read embedded scripts: %w", err)
	}
	scriptsDir := filepath.Join(tmpDir, "scripts")
	if err := os.MkdirAll(scriptsDir, 0755); err != nil {
		return err
	}
	for _, e := range entries {
		if e.IsDir() {
			continue
		}
		data, err := fs.ReadFile(filepath.Join("scripts", e.Name()))
		if err != nil {
			return err
		}
		if err := os.WriteFile(filepath.Join(scriptsDir, e.Name()), data, 0644); err != nil {
			return err
		}
	}
	return nil
}

// ExtractAssets unpacks embedded assets to tmpDir/assets.
func ExtractAssets(fs interface{ ReadDir(string) ([]os.DirEntry, error); ReadFile(string) ([]byte, error) }, tmpDir string) error {
	return extractDir(fs, "assets", tmpDir)
}

func extractDir(fs interface{ ReadDir(string) ([]os.DirEntry, error); ReadFile(string) ([]byte, error) }, src, dstBase string) error {
	entries, err := fs.ReadDir(src)
	if err != nil {
		return err
	}
	dst := filepath.Join(dstBase, src)
	if err := os.MkdirAll(dst, 0755); err != nil {
		return err
	}
	for _, e := range entries {
		srcPath := filepath.Join(src, e.Name())
		dstPath := filepath.Join(dstBase, srcPath)
		if e.IsDir() {
			if err := extractDir(fs, srcPath, dstBase); err != nil {
				return err
			}
			continue
		}
		data, err := fs.ReadFile(srcPath)
		if err != nil {
			return err
		}
		if err := os.WriteFile(dstPath, data, 0644); err != nil {
			return err
		}
	}
	return nil
}

// WriteConfig serialises cfg to a temp JSON file and returns its path.
func WriteConfig(cfg interface{}, tmpDir string) (string, error) {
	path := filepath.Join(tmpDir, "config-runtime.json")
	data, err := json.MarshalIndent(cfg, "", "  ")
	if err != nil {
		return "", err
	}
	return path, os.WriteFile(path, data, 0644)
}


AskUserQuestion CronCreate CronDelete CronList EnterPlanMode EnterWorktree ExitPlanMode ExitWorktree Monitor NotebookEdit RemoteTrigger TaskCreate TaskGet TaskList TaskOutput TaskStop TaskUpdate WebFetch WebSearch


Edit

# -----------------------------------------------------------------------
# Install packages from config
# -----------------------------------------------------------------------
if (-not $Config -or -not $Config.software -or -not $Config.software.install) {
    Write-Log "No software list in config - skipping installs" -Level WARN
} else {
    foreach ($pkg in $Config.software.install) {
        Write-Log "Installing $($pkg.name) ($($pkg.wingetId))" -Level INFO
        $result = & winget install --id $pkg.wingetId `
            --silent `
            --accept-package-agreements `
            --accept-source-agreements `
            --disable-interactivity `
            2>&1

        $exitCode = $LASTEXITCODE
        if ($exitCode -eq 0) {
            Write-Log "  Installed OK: $($pkg.name)" -Level OK
        } elseif ($exitCode -eq -1978335189) {
            # 0x8A150011 = already installed
            Write-Log "  Already installed: $($pkg.name)" -Level OK
        } else {
            Write-Log "  Failed: $($pkg.name) (exit $exitCode)" -Level ERROR
            Write-Log "  Output: $($result -join ' ')" -Level ERROR
        }
    }
}

# -----------------------------------------------------------------------
# Install packages from config
# -----------------------------------------------------------------------
if (Get-Feature $Config "software" "wingetInstalls") {
    if (-not $Config -or -not $Config.software -or -not $Config.software.install) {
        Write-Log "No software list in config - skipping installs" -Level WARN
    } else {
        foreach ($pkg in $Config.software.install) {
            Write-Log "Installing $($pkg.name) ($($pkg.wingetId))" -Level INFO
            $result = & winget install --id $pkg.wingetId `
                --silent `
                --accept-package-agreements `
                --accept-source-agreements `
                --disable-interactivity `
                2>&1

            $exitCode = $LASTEXITCODE
            if ($exitCode -eq 0) {
                Write-Log "  Installed OK: $($pkg.name)" -Level OK
            } elseif ($exitCode -eq -1978335189) {
                # 0x8A150011 = already installed
                Write-Log "  Already installed: $($pkg.name)" -Level OK
            } else {
                Write-Log "  Failed: $($pkg.name) (exit $exitCode)" -Level ERROR
                Write-Log "  Output: $($result -join ' ')" -Level ERROR
            }
        }
    }
} else {
    Write-Log "wingetInstalls feature disabled - skipping" -Level INFO
}


The file /opt/xetup/scripts/02-software.ps1 has been updated successfully.

# -----------------------------------------------------------------------
# Install packages from config
# -----------------------------------------------------------------------
if (Get-Feature $Config "software" "wingetInstalls") {
    if (-not $Config -or -not $Config.software -or -not $Config.software.install) {
        Write-Log "No software list in config - skipping installs" -Level WARN
    } else {
        foreach ($pkg in $Config.software.install) {
            Write-Log "Installing $($pkg.name) ($($pkg.wingetId))" -Level INFO
            $result = & winget install --id $pkg.wingetId `
                --silent `
                --accept-package-agreements `
                --accept-source-agreements `
                --disable-interactivity `
                2>&1

            $exitCode = $LASTEXITCODE
            if ($exitCode -eq 0) {
                Write-Log "  Installed OK: $($pkg.name)" -Level OK
            } elseif ($exitCode -eq -1978335189) {
                # 0x8A150011 = already installed
                Write-Log "  Already installed: $($pkg.name)" -Level OK
            } else {
                Write-Log "  Failed: $($pkg.name) (exit $exitCode)" -Level ERROR
                Write-Log "  Output: $($result -join ' ')" -Level ERROR
            }
        }
    }
} else {
    Write-Log "wingetInstalls feature disabled - skipping" -Level INFO
}

<#
.SYNOPSIS
    Installs standard business software via winget, sets Adobe PDF default, and installs Atera RMM agent.

.DESCRIPTION
    Uses winget to install the standard X9.cz MSP software bundle. Checks winget
    availability before running. Each install is logged. After Adobe Acrobat Reader,
    temporarily stops the UCPD driver (User Choice Protection Driver, present since
    Win11 23H2 / Win10 22H2 update) to allow the HKCR file association write, sets
    .pdf -> AcroRd32, then restarts UCPD. Atera RMM agent is installed for MSP
    monitoring, remote access, and ticketing integration.

.ITEMS
    7-zip-7zip-7zip: Installs 7-Zip (winget ID: 7zip.7zip). Used for archive management. Silent install with --accept-package-agreements --accept-source-agreements flags required for unattended deployment.
    adobe-acrobat-reader-64-bit-adobe-acroba: Installs Adobe Acrobat Reader DC 64-bit (Adobe.Acrobat.Reader.64-bit). Required as the default PDF viewer to prevent Edge from handling PDFs in browser mode, which limits functionality.
    openvpn-connect-openvpntechnologies-open: Installs OpenVPN Connect client. Used for client VPN access when the client network requires a VPN. The ovpn profile and credentials are configured separately per client.
    atera-agent-install: Atera RMM agent installed via msiexec /qn. Download: Invoke-WebRequest from https://x9.servicedesk.atera.com/api/utils/agent-install/windows/?cid=31&aeid=50b72e7113e54a63ac76b96c54c7e337. Agent enables MSP monitoring, remote access, and ticketing integration with the Atera dashboard.
    adobe-pdf-default-pdf-acrord32-po-instal: Sets .pdf -> AcroRd32 file association after Acrobat install via HKCR (system-wide, no UserChoice hash issue). UCPD driver is stopped immediately before the write and restarted after to ensure the association persists across Edge updates.
    ucpd-sys-kernel-driver-od-feb-2024-bloku: UCPD.sys (User Choice Protection Driver) is stopped before the PDF association write and restarted after. Pattern: Stop-Service ucpd -> set HKCR\.pdf -> Start-Service ucpd. Implemented in this script.
#>
param(
    [object]$Config,
    [string]$LogFile
)

$ErrorActionPreference = "Continue"

function Write-Log {
    param([string]$Message, [string]$Level = "INFO")
    $line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
    Add-Content -Path $LogFile -Value $line -Encoding UTF8
}

function Get-Feature {
    param([object]$Cfg, [string]$StepID, [string]$FeatureID, [bool]$Default = $true)
    try {
        if ($null -eq $Cfg) { return $Default }
        $stepFeatures = $Cfg.features.$StepID
        if ($null -eq $stepFeatures) { return $Default }
        $val = $stepFeatures.$FeatureID
        if ($null -eq $val) { return $Default }
        return [bool]$val
    } catch { return $Default }
}

# -----------------------------------------------------------------------
# Check winget availability
# -----------------------------------------------------------------------
Write-Log "Checking winget availability" -Level INFO

$winget = Get-Command winget -ErrorAction SilentlyContinue
if (-not $winget) {
    # Try to find winget in known locations
    $wingetPaths = @(
        "$env:LOCALAPPDATA\Microsoft\WindowsApps\winget.exe"
        "$env:ProgramFiles\WindowsApps\Microsoft.DesktopAppInstaller*\winget.exe"
    )
    foreach ($p in $wingetPaths) {
        $found = Get-Item $p -ErrorAction SilentlyContinue | Select-Object -First 1
        if ($found) { $winget = $found.FullName; break }
    }
}

if (-not $winget) {
    Write-Log "winget not found - software installation skipped" -Level ERROR
    exit 1
}

Write-Log "winget found: $($winget.Source -or $winget)" -Level OK

# Accept agreements upfront
& winget source update --accept-source-agreements 2>&1 | Out-Null

# -----------------------------------------------------------------------
# Install packages from config
# -----------------------------------------------------------------------
if (-not $Config -or -not $Config.software -or -not $Config.software.install) {
    Write-Log "No software list in config - skipping installs" -Level WARN
} else {
    foreach ($pkg in $Config.software.install) {
        Write-Log "Installing $($pkg.name) ($($pkg.wingetId))" -Level INFO
        $result = & winget install --id $pkg.wingetId `
            --silent `
            --accept-package-agreements `
            --accept-source-agreements `
            --disable-interactivity `
            2>&1

        $exitCode = $LASTEXITCODE
        if ($exitCode -eq 0) {
            Write-Log "  Installed OK: $($pkg.name)" -Level OK
        } elseif ($exitCode -eq -1978335189) {
            # 0x8A150011 = already installed
            Write-Log "  Already installed: $($pkg.name)" -Level OK
        } else {
            Write-Log "  Failed: $($pkg.name) (exit $exitCode)" -Level ERROR
            Write-Log "  Output: $($result -join ' ')" -Level ERROR
        }
    }
}

# -----------------------------------------------------------------------
# Set Adobe Reader as default PDF app
# -----------------------------------------------------------------------
$forcePdf = $true
if ($Config -and $Config.pdfDefault) {
    $forcePdf = [bool]$Config.pdfDefault.forceAdobeReader
}

if ($forcePdf) {
    Write-Log "Setting Adobe Reader as default PDF app" -Level INFO

    # Stop UCPD driver before writing file association.
    # UCPD.sys (User Choice Protection Driver) blocks UserChoice registry writes
    # on Win11 23H2+ and some Win10 22H2 builds. Stopping it temporarily allows
    # the HKCR association to be written reliably.
    $ucpdStopped = $false
    $ucpdSvc = Get-Service -Name "ucpd" -ErrorAction SilentlyContinue
    if ($ucpdSvc) {
        try {
            Stop-Service -Name "ucpd" -Force -ErrorAction Stop
            $ucpdStopped = $true
            Write-Log "  UCPD driver stopped" -Level OK
        }
        catch {
            Write-Log "  Could not stop UCPD: $_ (association may not persist on some builds)" -Level WARN
        }
    }

    # Find Adobe PDF viewer executable (Acrobat DC or Reader DC)
    $acroPaths = @(
        "$env:ProgramFiles\Adobe\Acrobat DC\Acrobat\Acrobat.exe"
        "${env:ProgramFiles(x86)}\Adobe\Acrobat DC\Acrobat\Acrobat.exe"
        "${env:ProgramFiles(x86)}\Adobe\Acrobat Reader DC\Reader\AcroRd32.exe"
        "$env:ProgramFiles\Adobe\Acrobat Reader DC\Reader\AcroRd32.exe"
        "${env:ProgramFiles(x86)}\Adobe\Reader\Reader\AcroRd32.exe"
    )
    $acroExe = $acroPaths | Where-Object { Test-Path $_ } | Select-Object -First 1

    if (-not $acroExe) {
        Write-Log "  Adobe PDF viewer not found - PDF default not set" -Level WARN
    } else {
        Write-Log "  Found: $acroExe" -Level INFO

        # Set file type association via HKCR (system-wide, requires admin)
        $progId = "AcroExch.Document.DC"
        $openCmd = "`"$acroExe`" `"%1`""

        # HKCR\.pdf -> progId
        if (-not (Test-Path "HKCR:\.pdf")) {
            New-Item -Path "HKCR:\.pdf" -Force | Out-Null
        }
        Set-ItemProperty -Path "HKCR:\.pdf" -Name "(Default)" -Value $progId

        # HKCR\AcroExch.Document.DC\shell\open\command
        $cmdPath = "HKCR:\$progId\shell\open\command"
        if (-not (Test-Path $cmdPath)) {
            New-Item -Path $cmdPath -Force | Out-Null
        }
        Set-ItemProperty -Path $cmdPath -Name "(Default)" -Value $openCmd

        Write-Log "  PDF default set to AcroRd32 (HKCR)" -Level OK
    }

    # Restart UCPD after writing association
    if ($ucpdStopped) {
        try {
            Start-Service -Name "ucpd" -ErrorAction SilentlyContinue
            Write-Log "  UCPD driver restarted" -Level OK
        }
        catch {
            Write-Log "  Could not restart UCPD: $_" -Level WARN
        }
    }
}

# -----------------------------------------------------------------------
# Install Atera RMM Agent
# -----------------------------------------------------------------------
Write-Log "Installing Atera RMM Agent" -Level INFO

$ateraUrl = "https://x9.servicedesk.atera.com/api/utils/agent-install/windows/?cid=31&aeid=50b72e7113e54a63ac76b96c54c7e337"
$ateraMsi = "$env:TEMP\AteraAgent.msi"

try {
    Write-Log "  Downloading Atera agent..." -Level INFO
    Invoke-WebRequest -Uri $ateraUrl -OutFile $ateraMsi -UseBasicParsing -ErrorAction Stop
    Write-Log "  Download complete" -Level OK

    $msiResult = & msiexec /i $ateraMsi /qn 2>&1
    Start-Sleep -Seconds 5

    $ateraExe = "$env:ProgramFiles\ATERA Networks\AteraAgent\AteraAgent.exe"
    if (Test-Path $ateraExe) {
        Write-Log "  Atera agent installed" -Level OK
    } else {
        Write-Log "  Atera agent install may have failed - binary not found at expected path" -Level WARN
        Write-Log "  msiexec output: $($msiResult -join ' ')" -Level WARN
    }
}
catch {
    Write-Log "  Atera agent download/install failed: $_" -Level ERROR
}
finally {
    Remove-Item $ateraMsi -Force -ErrorAction SilentlyContinue
}

Write-Log "Step 2 complete" -Level OK

-if (-not $Config -or -not $Config.software -or -not $Config.software.install) {

-    Write-Log "No software list in config - skipping installs" -Level WARN

-    foreach ($pkg in $Config.software.install) {

-        Write-Log "Installing $($pkg.name) ($($pkg.wingetId))" -Level INFO

-        $result = & winget install --id $pkg.wingetId `

-            --accept-package-agreements `

-            --accept-source-agreements `

+if (Get-Feature $Config "software" "wingetInstalls") {

+    if (-not $Config -or -not $Config.software -or -not $Config.software.install) {

+        Write-Log "No software list in config - skipping installs" -Level WARN

+        foreach ($pkg in $Config.software.install) {

+            Write-Log "Installing $($pkg.name) ($($pkg.wingetId))" -Level INFO

+            $result = & winget install --id $pkg.wingetId `

+                --accept-package-agreements `

+                --accept-source-agreements `

+                --disable-interactivity `

-            Write-Log "  Installed OK: $($pkg.name)" -Level OK

-            # 0x8A150011 = already installed

-            Write-Log "  Already installed: $($pkg.name)" -Level OK

-            Write-Log "  Failed: $($pkg.name) (exit $exitCode)" -Level ERROR

-            Write-Log "  Output: $($result -join ' ')" -Level ERROR

+                Write-Log "  Installed OK: $($pkg.name)" -Level OK

+                # 0x8A150011 = already installed

+                Write-Log "  Already installed: $($pkg.name)" -Level OK

+                Write-Log "  Failed: $($pkg.name) (exit $exitCode)" -Level ERROR

+                Write-Log "  Output: $($result -join ' ')" -Level ERROR

+    Write-Log "wingetInstalls feature disabled - skipping" -Level INFO


# -----------------------------------------------------------------------
# Set Adobe Reader as default PDF app
# -----------------------------------------------------------------------
$forcePdf = $true
if ($Config -and $Config.pdfDefault) {
    $forcePdf = [bool]$Config.pdfDefault.forceAdobeReader
}

if ($forcePdf) {
    Write-Log "Setting Adobe Reader as default PDF app" -Level INFO

    # Stop UCPD driver before writing file association.
    # UCPD.sys (User Choice Protection Driver) blocks UserChoice registry writes
    # on Win11 23H2+ and some Win10 22H2 builds. Stopping it temporarily allows
    # the HKCR association to be written reliably.
    $ucpdStopped = $false
    $ucpdSvc = Get-Service -Name "ucpd" -ErrorAction SilentlyContinue
    if ($ucpdSvc) {
        try {
            Stop-Service -Name "ucpd" -Force -ErrorAction Stop
            $ucpdStopped = $true
            Write-Log "  UCPD driver stopped" -Level OK
        }
        catch {
            Write-Log "  Could not stop UCPD: $_ (association may not persist on some builds)" -Level WARN
        }
    }

    # Find Adobe PDF viewer executable (Acrobat DC or Reader DC)
    $acroPaths = @(
        "$env:ProgramFiles\Adobe\Acrobat DC\Acrobat\Acrobat.exe"
        "${env:ProgramFiles(x86)}\Adobe\Acrobat DC\Acrobat\Acrobat.exe"
        "${env:ProgramFiles(x86)}\Adobe\Acrobat Reader DC\Reader\AcroRd32.exe"
        "$env:ProgramFiles\Adobe\Acrobat Reader DC\Reader\AcroRd32.exe"
        "${env:ProgramFiles(x86)}\Adobe\Reader\Reader\AcroRd32.exe"
    )
    $acroExe = $acroPaths | Where-Object { Test-Path $_ } | Select-Object -First 1

    if (-not $acroExe) {
        Write-Log "  Adobe PDF viewer not found - PDF default not set" -Level WARN
    } else {
        Write-Log "  Found: $acroExe" -Level INFO

        # Set file type association via HKCR (system-wide, requires admin)
        $progId = "AcroExch.Document.DC"
        $openCmd = "`"$acroExe`" `"%1`""

        # HKCR\.pdf -> progId
        if (-not (Test-Path "HKCR:\.pdf")) {
            New-Item -Path "HKCR:\.pdf" -Force | Out-Null
        }
        Set-ItemProperty -Path "HKCR:\.pdf" -Name "(Default)" -Value $progId

        # HKCR\AcroExch.Document.DC\shell\open\command
        $cmdPath = "HKCR:\$progId\shell\open\command"
        if (-not (Test-Path $cmdPath)) {
            New-Item -Path $cmdPath -Force | Out-Null
        }
        Set-ItemProperty -Path $cmdPath -Name "(Default)" -Value $openCmd

        Write-Log "  PDF default set to AcroRd32 (HKCR)" -Level OK
    }

    # Restart UCPD after writing association
    if ($ucpdStopped) {
        try {
            Start-Service -Name "ucpd" -ErrorAction SilentlyContinue
            Write-Log "  UCPD driver restarted" -Level OK
        }
        catch {
            Write-Log "  Could not restart UCPD: $_" -Level WARN
        }
    }
}

# -----------------------------------------------------------------------
# Set Adobe Reader as default PDF app
# -----------------------------------------------------------------------
if (Get-Feature $Config "software" "pdfDefault") {
    Write-Log "Setting Adobe Reader as default PDF app" -Level INFO

    # Stop UCPD driver before writing file association.
    # UCPD.sys (User Choice Protection Driver) blocks UserChoice registry writes
    # on Win11 23H2+ and some Win10 22H2 builds. Stopping it temporarily allows
    # the HKCR association to be written reliably.
    $ucpdStopped = $false
    $ucpdSvc = Get-Service -Name "ucpd" -ErrorAction SilentlyContinue
    if ($ucpdSvc) {
        try {
            Stop-Service -Name "ucpd" -Force -ErrorAction Stop
            $ucpdStopped = $true
            Write-Log "  UCPD driver stopped" -Level OK
        }
        catch {
            Write-Log "  Could not stop UCPD: $_ (association may not persist on some builds)" -Level WARN
        }
    }

    # Find Adobe PDF viewer executable (Acrobat DC or Reader DC)
    $acroPaths = @(
        "$env:ProgramFiles\Adobe\Acrobat DC\Acrobat\Acrobat.exe"
        "${env:ProgramFiles(x86)}\Adobe\Acrobat DC\Acrobat\Acrobat.exe"
        "${env:ProgramFiles(x86)}\Adobe\Acrobat Reader DC\Reader\AcroRd32.exe"
        "$env:ProgramFiles\Adobe\Acrobat Reader DC\Reader\AcroRd32.exe"
        "${env:ProgramFiles(x86)}\Adobe\Reader\Reader\AcroRd32.exe"
    )
    $acroExe = $acroPaths | Where-Object { Test-Path $_ } | Select-Object -First 1

    if (-not $acroExe) {
        Write-Log "  Adobe PDF viewer not found - PDF default not set" -Level WARN
    } else {
        Write-Log "  Found: $acroExe" -Level INFO

        # Set file type association via HKCR (system-wide, requires admin)
        $progId = "AcroExch.Document.DC"
        $openCmd = "`"$acroExe`" `"%1`""

        # HKCR\.pdf -> progId
        if (-not (Test-Path "HKCR:\.pdf")) {
            New-Item -Path "HKCR:\.pdf" -Force | Out-Null
        }
        Set-ItemProperty -Path "HKCR:\.pdf" -Name "(Default)" -Value $progId

        # HKCR\AcroExch.Document.DC\shell\open\command
        $cmdPath = "HKCR:\$progId\shell\open\command"
        if (-not (Test-Path $cmdPath)) {
            New-Item -Path $cmdPath -Force | Out-Null
        }
        Set-ItemProperty -Path $cmdPath -Name "(Default)" -Value $openCmd

        Write-Log "  PDF default set to AcroRd32 (HKCR)" -Level OK
    }

    # Restart UCPD after writing association
    if ($ucpdStopped) {
        try {
            Start-Service -Name "ucpd" -ErrorAction SilentlyContinue
            Write-Log "  UCPD driver restarted" -Level OK
        }
        catch {
            Write-Log "  Could not restart UCPD: $_" -Level WARN
        }
    }
} else {
    Write-Log "pdfDefault feature disabled - skipping" -Level INFO
}


# -----------------------------------------------------------------------
# Set Adobe Reader as default PDF app
# -----------------------------------------------------------------------
if (Get-Feature $Config "software" "pdfDefault") {
    Write-Log "Setting Adobe Reader as default PDF app" -Level INFO

    # Stop UCPD driver before writing file association.
    # UCPD.sys (User Choice Protection Driver) blocks UserChoice registry writes
    # on Win11 23H2+ and some Win10 22H2 builds. Stopping it temporarily allows
    # the HKCR association to be written reliably.
    $ucpdStopped = $false
    $ucpdSvc = Get-Service -Name "ucpd" -ErrorAction SilentlyContinue
    if ($ucpdSvc) {
        try {
            Stop-Service -Name "ucpd" -Force -ErrorAction Stop
            $ucpdStopped = $true
            Write-Log "  UCPD driver stopped" -Level OK
        }
        catch {
            Write-Log "  Could not stop UCPD: $_ (association may not persist on some builds)" -Level WARN
        }
    }

    # Find Adobe PDF viewer executable (Acrobat DC or Reader DC)
    $acroPaths = @(
        "$env:ProgramFiles\Adobe\Acrobat DC\Acrobat\Acrobat.exe"
        "${env:ProgramFiles(x86)}\Adobe\Acrobat DC\Acrobat\Acrobat.exe"
        "${env:ProgramFiles(x86)}\Adobe\Acrobat Reader DC\Reader\AcroRd32.exe"
        "$env:ProgramFiles\Adobe\Acrobat Reader DC\Reader\AcroRd32.exe"
        "${env:ProgramFiles(x86)}\Adobe\Reader\Reader\AcroRd32.exe"
    )
    $acroExe = $acroPaths | Where-Object { Test-Path $_ } | Select-Object -First 1

    if (-not $acroExe) {
        Write-Log "  Adobe PDF viewer not found - PDF default not set" -Level WARN
    } else {
        Write-Log "  Found: $acroExe" -Level INFO

        # Set file type association via HKCR (system-wide, requires admin)
        $progId = "AcroExch.Document.DC"
        $openCmd = "`"$acroExe`" `"%1`""

        # HKCR\.pdf -> progId
        if (-not (Test-Path "HKCR:\.pdf")) {
            New-Item -Path "HKCR:\.pdf" -Force | Out-Null
        }
        Set-ItemProperty -Path "HKCR:\.pdf" -Name "(Default)" -Value $progId

        # HKCR\AcroExch.Document.DC\shell\open\command
        $cmdPath = "HKCR:\$progId\shell\open\command"
        if (-not (Test-Path $cmdPath)) {
            New-Item -Path $cmdPath -Force | Out-Null
        }
        Set-ItemProperty -Path $cmdPath -Name "(Default)" -Value $openCmd

        Write-Log "  PDF default set to AcroRd32 (HKCR)" -Level OK
    }

    # Restart UCPD after writing association
    if ($ucpdStopped) {
        try {
            Start-Service -Name "ucpd" -ErrorAction SilentlyContinue
            Write-Log "  UCPD driver restarted" -Level OK
        }
        catch {
            Write-Log "  Could not restart UCPD: $_" -Level WARN
        }
    }
} else {
    Write-Log "pdfDefault feature disabled - skipping" -Level INFO
}

<#
.SYNOPSIS
    Installs standard business software via winget, sets Adobe PDF default, and installs Atera RMM agent.

.DESCRIPTION
    Uses winget to install the standard X9.cz MSP software bundle. Checks winget
    availability before running. Each install is logged. After Adobe Acrobat Reader,
    temporarily stops the UCPD driver (User Choice Protection Driver, present since
    Win11 23H2 / Win10 22H2 update) to allow the HKCR file association write, sets
    .pdf -> AcroRd32, then restarts UCPD. Atera RMM agent is installed for MSP
    monitoring, remote access, and ticketing integration.

.ITEMS
    7-zip-7zip-7zip: Installs 7-Zip (winget ID: 7zip.7zip). Used for archive management. Silent install with --accept-package-agreements --accept-source-agreements flags required for unattended deployment.
    adobe-acrobat-reader-64-bit-adobe-acroba: Installs Adobe Acrobat Reader DC 64-bit (Adobe.Acrobat.Reader.64-bit). Required as the default PDF viewer to prevent Edge from handling PDFs in browser mode, which limits functionality.
    openvpn-connect-openvpntechnologies-open: Installs OpenVPN Connect client. Used for client VPN access when the client network requires a VPN. The ovpn profile and credentials are configured separately per client.
    atera-agent-install: Atera RMM agent installed via msiexec /qn. Download: Invoke-WebRequest from https://x9.servicedesk.atera.com/api/utils/agent-install/windows/?cid=31&aeid=50b72e7113e54a63ac76b96c54c7e337. Agent enables MSP monitoring, remote access, and ticketing integration with the Atera dashboard.
    adobe-pdf-default-pdf-acrord32-po-instal: Sets .pdf -> AcroRd32 file association after Acrobat install via HKCR (system-wide, no UserChoice hash issue). UCPD driver is stopped immediately before the write and restarted after to ensure the association persists across Edge updates.
    ucpd-sys-kernel-driver-od-feb-2024-bloku: UCPD.sys (User Choice Protection Driver) is stopped before the PDF association write and restarted after. Pattern: Stop-Service ucpd -> set HKCR\.pdf -> Start-Service ucpd. Implemented in this script.
#>
param(
    [object]$Config,
    [string]$LogFile
)

$ErrorActionPreference = "Continue"

function Write-Log {
    param([string]$Message, [string]$Level = "INFO")
    $line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
    Add-Content -Path $LogFile -Value $line -Encoding UTF8
}

function Get-Feature {
    param([object]$Cfg, [string]$StepID, [string]$FeatureID, [bool]$Default = $true)
    try {
        if ($null -eq $Cfg) { return $Default }
        $stepFeatures = $Cfg.features.$StepID
        if ($null -eq $stepFeatures) { return $Default }
        $val = $stepFeatures.$FeatureID
        if ($null -eq $val) { return $Default }
        return [bool]$val
    } catch { return $Default }
}

# -----------------------------------------------------------------------
# Check winget availability
# -----------------------------------------------------------------------
Write-Log "Checking winget availability" -Level INFO

$winget = Get-Command winget -ErrorAction SilentlyContinue
if (-not $winget) {
    # Try to find winget in known locations
    $wingetPaths = @(
        "$env:LOCALAPPDATA\Microsoft\WindowsApps\winget.exe"
        "$env:ProgramFiles\WindowsApps\Microsoft.DesktopAppInstaller*\winget.exe"
    )
    foreach ($p in $wingetPaths) {
        $found = Get-Item $p -ErrorAction SilentlyContinue | Select-Object -First 1
        if ($found) { $winget = $found.FullName; break }
    }
}

if (-not $winget) {
    Write-Log "winget not found - software installation skipped" -Level ERROR
    exit 1
}

Write-Log "winget found: $($winget.Source -or $winget)" -Level OK

# Accept agreements upfront
& winget source update --accept-source-agreements 2>&1 | Out-Null

# -----------------------------------------------------------------------
# Install packages from config
# -----------------------------------------------------------------------
if (Get-Feature $Config "software" "wingetInstalls") {
    if (-not $Config -or -not $Config.software -or -not $Config.software.install) {
        Write-Log "No software list in config - skipping installs" -Level WARN
    } else {
        foreach ($pkg in $Config.software.install) {
            Write-Log "Installing $($pkg.name) ($($pkg.wingetId))" -Level INFO
            $result = & winget install --id $pkg.wingetId `
                --silent `
                --accept-package-agreements `
                --accept-source-agreements `
                --disable-interactivity `
                2>&1

            $exitCode = $LASTEXITCODE
            if ($exitCode -eq 0) {
                Write-Log "  Installed OK: $($pkg.name)" -Level OK
            } elseif ($exitCode -eq -1978335189) {
                # 0x8A150011 = already installed
                Write-Log "  Already installed: $($pkg.name)" -Level OK
            } else {
                Write-Log "  Failed: $($pkg.name) (exit $exitCode)" -Level ERROR
                Write-Log "  Output: $($result -join ' ')" -Level ERROR
            }
        }
    }
} else {
    Write-Log "wingetInstalls feature disabled - skipping" -Level INFO
}

# -----------------------------------------------------------------------
# Set Adobe Reader as default PDF app
# -----------------------------------------------------------------------
$forcePdf = $true
if ($Config -and $Config.pdfDefault) {
    $forcePdf = [bool]$Config.pdfDefault.forceAdobeReader
}

if ($forcePdf) {
    Write-Log "Setting Adobe Reader as default PDF app" -Level INFO

    # Stop UCPD driver before writing file association.
    # UCPD.sys (User Choice Protection Driver) blocks UserChoice registry writes
    # on Win11 23H2+ and some Win10 22H2 builds. Stopping it temporarily allows
    # the HKCR association to be written reliably.
    $ucpdStopped = $false
    $ucpdSvc = Get-Service -Name "ucpd" -ErrorAction SilentlyContinue
    if ($ucpdSvc) {
        try {
            Stop-Service -Name "ucpd" -Force -ErrorAction Stop
            $ucpdStopped = $true
            Write-Log "  UCPD driver stopped" -Level OK
        }
        catch {
            Write-Log "  Could not stop UCPD: $_ (association may not persist on some builds)" -Level WARN
        }
    }

    # Find Adobe PDF viewer executable (Acrobat DC or Reader DC)
    $acroPaths = @(
        "$env:ProgramFiles\Adobe\Acrobat DC\Acrobat\Acrobat.exe"
        "${env:ProgramFiles(x86)}\Adobe\Acrobat DC\Acrobat\Acrobat.exe"
        "${env:ProgramFiles(x86)}\Adobe\Acrobat Reader DC\Reader\AcroRd32.exe"
        "$env:ProgramFiles\Adobe\Acrobat Reader DC\Reader\AcroRd32.exe"
        "${env:ProgramFiles(x86)}\Adobe\Reader\Reader\AcroRd32.exe"
    )
    $acroExe = $acroPaths | Where-Object { Test-Path $_ } | Select-Object -First 1

    if (-not $acroExe) {
        Write-Log "  Adobe PDF viewer not found - PDF default not set" -Level WARN
    } else {
        Write-Log "  Found: $acroExe" -Level INFO

        # Set file type association via HKCR (system-wide, requires admin)
        $progId = "AcroExch.Document.DC"
        $openCmd = "`"$acroExe`" `"%1`""

        # HKCR\.pdf -> progId
        if (-not (Test-Path "HKCR:\.pdf")) {
            New-Item -Path "HKCR:\.pdf" -Force | Out-Null
        }
        Set-ItemProperty -Path "HKCR:\.pdf" -Name "(Default)" -Value $progId

        # HKCR\AcroExch.Document.DC\shell\open\command
        $cmdPath = "HKCR:\$progId\shell\open\command"
        if (-not (Test-Path $cmdPath)) {
            New-Item -Path $cmdPath -Force | Out-Null
        }
        Set-ItemProperty -Path $cmdPath -Name "(Default)" -Value $openCmd

        Write-Log "  PDF default set to AcroRd32 (HKCR)" -Level OK
    }

    # Restart UCPD after writing association
    if ($ucpdStopped) {
        try {
            Start-Service -Name "ucpd" -ErrorAction SilentlyContinue
            Write-Log "  UCPD driver restarted" -Level OK
        }
        catch {
            Write-Log "  Could not restart UCPD: $_" -Level WARN
        }
    }
}

# -----------------------------------------------------------------------
# Install Atera RMM Agent
# -----------------------------------------------------------------------
Write-Log "Installing Atera RMM Agent" -Level INFO

$ateraUrl = "https://x9.servicedesk.atera.com/api/utils/agent-install/windows/?cid=31&aeid=50b72e7113e54a63ac76b96c54c7e337"
$ateraMsi = "$env:TEMP\AteraAgent.msi"

try {
    Write-Log "  Downloading Atera agent..." -Level INFO
    Invoke-WebRequest -Uri $ateraUrl -OutFile $ateraMsi -UseBasicParsing -ErrorAction Stop
    Write-Log "  Download complete" -Level OK

    $msiResult = & msiexec /i $ateraMsi /qn 2>&1
    Start-Sleep -Seconds 5

    $ateraExe = "$env:ProgramFiles\ATERA Networks\AteraAgent\AteraAgent.exe"
    if (Test-Path $ateraExe) {
        Write-Log "  Atera agent installed" -Level OK
    } else {
        Write-Log "  Atera agent install may have failed - binary not found at expected path" -Level WARN
        Write-Log "  msiexec output: $($msiResult -join ' ')" -Level WARN
    }
}
catch {
    Write-Log "  Atera agent download/install failed: $_" -Level ERROR
}
finally {
    Remove-Item $ateraMsi -Force -ErrorAction SilentlyContinue
}

Write-Log "Step 2 complete" -Level OK

-    $forcePdf = [bool]$Config.pdfDefault.forceAdobeReader

+if (Get-Feature $Config "software" "pdfDefault") {

+    Write-Log "pdfDefault feature disabled - skipping" -Level INFO


# -----------------------------------------------------------------------
# Install Atera RMM Agent
# -----------------------------------------------------------------------
Write-Log "Installing Atera RMM Agent" -Level INFO

$ateraUrl = "https://x9.servicedesk.atera.com/api/utils/agent-install/windows/?cid=31&aeid=50b72e7113e54a63ac76b96c54c7e337"
$ateraMsi = "$env:TEMP\AteraAgent.msi"

try {
    Write-Log "  Downloading Atera agent..." -Level INFO
    Invoke-WebRequest -Uri $ateraUrl -OutFile $ateraMsi -UseBasicParsing -ErrorAction Stop
    Write-Log "  Download complete" -Level OK

    $msiResult = & msiexec /i $ateraMsi /qn 2>&1
    Start-Sleep -Seconds 5

    $ateraExe = "$env:ProgramFiles\ATERA Networks\AteraAgent\AteraAgent.exe"
    if (Test-Path $ateraExe) {
        Write-Log "  Atera agent installed" -Level OK
    } else {
        Write-Log "  Atera agent install may have failed - binary not found at expected path" -Level WARN
        Write-Log "  msiexec output: $($msiResult -join ' ')" -Level WARN
    }
}
catch {
    Write-Log "  Atera agent download/install failed: $_" -Level ERROR
}
finally {
    Remove-Item $ateraMsi -Force -ErrorAction SilentlyContinue
}

# -----------------------------------------------------------------------
# Install Atera RMM Agent
# -----------------------------------------------------------------------
if (Get-Feature $Config "software" "ateraAgent") {
    Write-Log "Installing Atera RMM Agent" -Level INFO

    $ateraUrl = "https://x9.servicedesk.atera.com/api/utils/agent-install/windows/?cid=31&aeid=50b72e7113e54a63ac76b96c54c7e337"
    $ateraMsi = "$env:TEMP\AteraAgent.msi"

    try {
        Write-Log "  Downloading Atera agent..." -Level INFO
        Invoke-WebRequest -Uri $ateraUrl -OutFile $ateraMsi -UseBasicParsing -ErrorAction Stop
        Write-Log "  Download complete" -Level OK

        $msiResult = & msiexec /i $ateraMsi /qn 2>&1
        Start-Sleep -Seconds 5

        $ateraExe = "$env:ProgramFiles\ATERA Networks\AteraAgent\AteraAgent.exe"
        if (Test-Path $ateraExe) {
            Write-Log "  Atera agent installed" -Level OK
        } else {
            Write-Log "  Atera agent install may have failed - binary not found at expected path" -Level WARN
            Write-Log "  msiexec output: $($msiResult -join ' ')" -Level WARN
        }
    }
    catch {
        Write-Log "  Atera agent download/install failed: $_" -Level ERROR
    }
    finally {
        Remove-Item $ateraMsi -Force -ErrorAction SilentlyContinue
    }
} else {
    Write-Log "ateraAgent feature disabled - skipping" -Level INFO
}


# -----------------------------------------------------------------------
# Install Atera RMM Agent
# -----------------------------------------------------------------------
if (Get-Feature $Config "software" "ateraAgent") {
    Write-Log "Installing Atera RMM Agent" -Level INFO

    $ateraUrl = "https://x9.servicedesk.atera.com/api/utils/agent-install/windows/?cid=31&aeid=50b72e7113e54a63ac76b96c54c7e337"
    $ateraMsi = "$env:TEMP\AteraAgent.msi"

    try {
        Write-Log "  Downloading Atera agent..." -Level INFO
        Invoke-WebRequest -Uri $ateraUrl -OutFile $ateraMsi -UseBasicParsing -ErrorAction Stop
        Write-Log "  Download complete" -Level OK

        $msiResult = & msiexec /i $ateraMsi /qn 2>&1
        Start-Sleep -Seconds 5

        $ateraExe = "$env:ProgramFiles\ATERA Networks\AteraAgent\AteraAgent.exe"
        if (Test-Path $ateraExe) {
            Write-Log "  Atera agent installed" -Level OK
        } else {
            Write-Log "  Atera agent install may have failed - binary not found at expected path" -Level WARN
            Write-Log "  msiexec output: $($msiResult -join ' ')" -Level WARN
        }
    }
    catch {
        Write-Log "  Atera agent download/install failed: $_" -Level ERROR
    }
    finally {
        Remove-Item $ateraMsi -Force -ErrorAction SilentlyContinue
    }
} else {
    Write-Log "ateraAgent feature disabled - skipping" -Level INFO
}

<#
.SYNOPSIS
    Installs standard business software via winget, sets Adobe PDF default, and installs Atera RMM agent.

.DESCRIPTION
    Uses winget to install the standard X9.cz MSP software bundle. Checks winget
    availability before running. Each install is logged. After Adobe Acrobat Reader,
    temporarily stops the UCPD driver (User Choice Protection Driver, present since
    Win11 23H2 / Win10 22H2 update) to allow the HKCR file association write, sets
    .pdf -> AcroRd32, then restarts UCPD. Atera RMM agent is installed for MSP
    monitoring, remote access, and ticketing integration.

.ITEMS
    7-zip-7zip-7zip: Installs 7-Zip (winget ID: 7zip.7zip). Used for archive management. Silent install with --accept-package-agreements --accept-source-agreements flags required for unattended deployment.
    adobe-acrobat-reader-64-bit-adobe-acroba: Installs Adobe Acrobat Reader DC 64-bit (Adobe.Acrobat.Reader.64-bit). Required as the default PDF viewer to prevent Edge from handling PDFs in browser mode, which limits functionality.
    openvpn-connect-openvpntechnologies-open: Installs OpenVPN Connect client. Used for client VPN access when the client network requires a VPN. The ovpn profile and credentials are configured separately per client.
    atera-agent-install: Atera RMM agent installed via msiexec /qn. Download: Invoke-WebRequest from https://x9.servicedesk.atera.com/api/utils/agent-install/windows/?cid=31&aeid=50b72e7113e54a63ac76b96c54c7e337. Agent enables MSP monitoring, remote access, and ticketing integration with the Atera dashboard.
    adobe-pdf-default-pdf-acrord32-po-instal: Sets .pdf -> AcroRd32 file association after Acrobat install via HKCR (system-wide, no UserChoice hash issue). UCPD driver is stopped immediately before the write and restarted after to ensure the association persists across Edge updates.
    ucpd-sys-kernel-driver-od-feb-2024-bloku: UCPD.sys (User Choice Protection Driver) is stopped before the PDF association write and restarted after. Pattern: Stop-Service ucpd -> set HKCR\.pdf -> Start-Service ucpd. Implemented in this script.
#>
param(
    [object]$Config,
    [string]$LogFile
)

$ErrorActionPreference = "Continue"

function Write-Log {
    param([string]$Message, [string]$Level = "INFO")
    $line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
    Add-Content -Path $LogFile -Value $line -Encoding UTF8
}

function Get-Feature {
    param([object]$Cfg, [string]$StepID, [string]$FeatureID, [bool]$Default = $true)
    try {
        if ($null -eq $Cfg) { return $Default }
        $stepFeatures = $Cfg.features.$StepID
        if ($null -eq $stepFeatures) { return $Default }
        $val = $stepFeatures.$FeatureID
        if ($null -eq $val) { return $Default }
        return [bool]$val
    } catch { return $Default }
}

# -----------------------------------------------------------------------
# Check winget availability
# -----------------------------------------------------------------------
Write-Log "Checking winget availability" -Level INFO

$winget = Get-Command winget -ErrorAction SilentlyContinue
if (-not $winget) {
    # Try to find winget in known locations
    $wingetPaths = @(
        "$env:LOCALAPPDATA\Microsoft\WindowsApps\winget.exe"
        "$env:ProgramFiles\WindowsApps\Microsoft.DesktopAppInstaller*\winget.exe"
    )
    foreach ($p in $wingetPaths) {
        $found = Get-Item $p -ErrorAction SilentlyContinue | Select-Object -First 1
        if ($found) { $winget = $found.FullName; break }
    }
}

if (-not $winget) {
    Write-Log "winget not found - software installation skipped" -Level ERROR
    exit 1
}

Write-Log "winget found: $($winget.Source -or $winget)" -Level OK

# Accept agreements upfront
& winget source update --accept-source-agreements 2>&1 | Out-Null

# -----------------------------------------------------------------------
# Install packages from config
# -----------------------------------------------------------------------
if (Get-Feature $Config "software" "wingetInstalls") {
    if (-not $Config -or -not $Config.software -or -not $Config.software.install) {
        Write-Log "No software list in config - skipping installs" -Level WARN
    } else {
        foreach ($pkg in $Config.software.install) {
            Write-Log "Installing $($pkg.name) ($($pkg.wingetId))" -Level INFO
            $result = & winget install --id $pkg.wingetId `
                --silent `
                --accept-package-agreements `
                --accept-source-agreements `
                --disable-interactivity `
                2>&1

            $exitCode = $LASTEXITCODE
            if ($exitCode -eq 0) {
                Write-Log "  Installed OK: $($pkg.name)" -Level OK
            } elseif ($exitCode -eq -1978335189) {
                # 0x8A150011 = already installed
                Write-Log "  Already installed: $($pkg.name)" -Level OK
            } else {
                Write-Log "  Failed: $($pkg.name) (exit $exitCode)" -Level ERROR
                Write-Log "  Output: $($result -join ' ')" -Level ERROR
            }
        }
    }
} else {
    Write-Log "wingetInstalls feature disabled - skipping" -Level INFO
}

# -----------------------------------------------------------------------
# Set Adobe Reader as default PDF app
# -----------------------------------------------------------------------
if (Get-Feature $Config "software" "pdfDefault") {
    Write-Log "Setting Adobe Reader as default PDF app" -Level INFO

    # Stop UCPD driver before writing file association.
    # UCPD.sys (User Choice Protection Driver) blocks UserChoice registry writes
    # on Win11 23H2+ and some Win10 22H2 builds. Stopping it temporarily allows
    # the HKCR association to be written reliably.
    $ucpdStopped = $false
    $ucpdSvc = Get-Service -Name "ucpd" -ErrorAction SilentlyContinue
    if ($ucpdSvc) {
        try {
            Stop-Service -Name "ucpd" -Force -ErrorAction Stop
            $ucpdStopped = $true
            Write-Log "  UCPD driver stopped" -Level OK
        }
        catch {
            Write-Log "  Could not stop UCPD: $_ (association may not persist on some builds)" -Level WARN
        }
    }

    # Find Adobe PDF viewer executable (Acrobat DC or Reader DC)
    $acroPaths = @(
        "$env:ProgramFiles\Adobe\Acrobat DC\Acrobat\Acrobat.exe"
        "${env:ProgramFiles(x86)}\Adobe\Acrobat DC\Acrobat\Acrobat.exe"
        "${env:ProgramFiles(x86)}\Adobe\Acrobat Reader DC\Reader\AcroRd32.exe"
        "$env:ProgramFiles\Adobe\Acrobat Reader DC\Reader\AcroRd32.exe"
        "${env:ProgramFiles(x86)}\Adobe\Reader\Reader\AcroRd32.exe"
    )
    $acroExe = $acroPaths | Where-Object { Test-Path $_ } | Select-Object -First 1

    if (-not $acroExe) {
        Write-Log "  Adobe PDF viewer not found - PDF default not set" -Level WARN
    } else {
        Write-Log "  Found: $acroExe" -Level INFO

        # Set file type association via HKCR (system-wide, requires admin)
        $progId = "AcroExch.Document.DC"
        $openCmd = "`"$acroExe`" `"%1`""

        # HKCR\.pdf -> progId
        if (-not (Test-Path "HKCR:\.pdf")) {
            New-Item -Path "HKCR:\.pdf" -Force | Out-Null
        }
        Set-ItemProperty -Path "HKCR:\.pdf" -Name "(Default)" -Value $progId

        # HKCR\AcroExch.Document.DC\shell\open\command
        $cmdPath = "HKCR:\$progId\shell\open\command"
        if (-not (Test-Path $cmdPath)) {
            New-Item -Path $cmdPath -Force | Out-Null
        }
        Set-ItemProperty -Path $cmdPath -Name "(Default)" -Value $openCmd

        Write-Log "  PDF default set to AcroRd32 (HKCR)" -Level OK
    }

    # Restart UCPD after writing association
    if ($ucpdStopped) {
        try {
            Start-Service -Name "ucpd" -ErrorAction SilentlyContinue
            Write-Log "  UCPD driver restarted" -Level OK
        }
        catch {
            Write-Log "  Could not restart UCPD: $_" -Level WARN
        }
    }
} else {
    Write-Log "pdfDefault feature disabled - skipping" -Level INFO
}

# -----------------------------------------------------------------------
# Install Atera RMM Agent
# -----------------------------------------------------------------------
Write-Log "Installing Atera RMM Agent" -Level INFO

$ateraUrl = "https://x9.servicedesk.atera.com/api/utils/agent-install/windows/?cid=31&aeid=50b72e7113e54a63ac76b96c54c7e337"
$ateraMsi = "$env:TEMP\AteraAgent.msi"

try {
    Write-Log "  Downloading Atera agent..." -Level INFO
    Invoke-WebRequest -Uri $ateraUrl -OutFile $ateraMsi -UseBasicParsing -ErrorAction Stop
    Write-Log "  Download complete" -Level OK

    $msiResult = & msiexec /i $ateraMsi /qn 2>&1
    Start-Sleep -Seconds 5

    $ateraExe = "$env:ProgramFiles\ATERA Networks\AteraAgent\AteraAgent.exe"
    if (Test-Path $ateraExe) {
        Write-Log "  Atera agent installed" -Level OK
    } else {
        Write-Log "  Atera agent install may have failed - binary not found at expected path" -Level WARN
        Write-Log "  msiexec output: $($msiResult -join ' ')" -Level WARN
    }
}
catch {
    Write-Log "  Atera agent download/install failed: $_" -Level ERROR
}
finally {
    Remove-Item $ateraMsi -Force -ErrorAction SilentlyContinue
}

Write-Log "Step 2 complete" -Level OK

-Write-Log "Installing Atera RMM Agent" -Level INFO

+if (Get-Feature $Config "software" "ateraAgent") {

+    Write-Log "Installing Atera RMM Agent" -Level INFO

+    $ateraUrl = "https://x9.servicedesk.atera.com/api/utils/agent-install/windows/?cid=31&aeid=50b72e7113e54a63ac76b96c54c7e337"

+    $ateraMsi = "$env:TEMP\AteraAgent.msi"

-    Write-Log "  Downloading Atera agent..." -Level INFO

-    Invoke-WebRequest -Uri $ateraUrl -OutFile $ateraMsi -UseBasicParsing -ErrorAction Stop

-    Write-Log "  Download complete" -Level OK

+        Write-Log "  Downloading Atera agent..." -Level INFO

+        Invoke-WebRequest -Uri $ateraUrl -OutFile $ateraMsi -UseBasicParsing -ErrorAction Stop

+        Write-Log "  Download complete" -Level OK

-    $msiResult = & msiexec /i $ateraMsi /qn 2>&1

+        $msiResult = & msiexec /i $ateraMsi /qn 2>&1

-    $ateraExe = "$env:ProgramFiles\ATERA Networks\AteraAgent\AteraAgent.exe"

-        Write-Log "  Atera agent installed" -Level OK

-        Write-Log "  Atera agent install may have failed - binary not found at expected path" -Level WARN

-        Write-Log "  msiexec output: $($msiResult -join ' ')" -Level WARN

+        $ateraExe = "$env:ProgramFiles\ATERA Networks\AteraAgent\AteraAgent.exe"

+            Write-Log "  Atera agent installed" -Level OK

+            Write-Log "  Atera agent install may have failed - binary not found at expected path" -Level WARN

+            Write-Log "  msiexec output: $($msiResult -join ' ')" -Level WARN

+        Write-Log "  Atera agent download/install failed: $_" -Level ERROR

+        Remove-Item $ateraMsi -Force -ErrorAction SilentlyContinue

+    Write-Log "ateraAgent feature disabled - skipping" -Level INFO

-    Write-Log "  Atera agent download/install failed: $_" -Level ERROR

-    Remove-Item $ateraMsi -Force -ErrorAction SilentlyContinue


`02-software.ps1` hotovo. Teď `03-system-registry.ps1`: