Eigenverft.Manifested.Drydock.ScheduledTask.ps1

function New-CompatScheduledTask {
<#
.SYNOPSIS
Create or update a Windows Scheduled Task through late-bound COM with Win7/10/11 compatibility, clear run-context semantics, configurable instance-overlap behavior for the same scheduled task, optional repetition, and optional immediate start.
 
.DESCRIPTION
Creates or updates a scheduled task by using the Task Scheduler COM API instead of the newer ScheduledTasks module,
so it remains usable on Windows 7 / Windows PowerShell 5.x while still working well on newer Windows versions.
 
This function is intended to be user-friendly and predictable for the most common task-scheduling scenarios:
- local PC automation for the current user
- background automation for the current user
- automation for a specific named account
- machine-level automation through LocalSystem
- daily, logon, startup, and repeating schedules
- optional immediate start after registration
 
Run-context choices:
- CurrentUser -> run as the current user
- SpecificUser -> run as a named user account
- System -> run as LocalSystem
 
Execution-mode choices:
- interactive / visible session
- background / "run whether user is logged on or not"
 
Trigger choices:
- logon of this user
- logon of any user
- startup / boot
- daily at a fixed time
 
Repetition choices:
- repeat every N minutes for a configurable duration
- implemented through COM trigger repetition for broad compatibility
- user-friendly daily behavior:
  - when -DailyAtTime is combined with -RepeatEveryMinutes and today's daily anchor has already passed,
    the function automatically adds a one-time bridge trigger at the next aligned interval for today
  - this avoids the confusing "next run is tomorrow" behavior when a repeating daily task is created later in the day
  - example: created at 04:39 with -DailyAtTime '00:00' -RepeatEveryMinutes 15
    -> next scheduled run becomes about 04:45 for today, not tomorrow at 00:00
- -RunNow still starts the task once immediately after registration
 
Multiple-instance behavior:
- -MultipleInstancesPolicy applies to multiple instances of the SAME scheduled task.
- It does not compare tasks globally by ActionPath alone, and it does not group tasks by ActionPath + ActionArguments.
- It is evaluated when this task is triggered again while an earlier instance of this same task is still running.
- In practice, if task '\User-Every15m' starts powershell.exe with certain arguments, and '\User-Every15m' is triggered again before the first run finishes,
  Task Scheduler uses the selected policy to decide what happens to the NEW instance of '\User-Every15m'.
- -MultipleInstancesPolicy IgnoreNew -> safest default for recurring scripts; skip a new run if one is still active
- -MultipleInstancesPolicy Parallel -> allow overlapping runs of this same task
- -MultipleInstancesPolicy Queue -> queue later runs of this same task until the current one finishes
- -MultipleInstancesPolicy StopExisting -> stop the running instance of this same task and start the new one
 
Behavior and safety defaults:
- StartWhenAvailable is enabled.
- MultipleInstances defaults to IgnoreNew.
- WakeToRun is opt-in.
- Daily time parsing uses invariant "HH:mm".
- Action path validation is performed unless -ForceRegister is used.
- Useful hints are emitted for common configuration mistakes.
 
Run-context notes:
- -RunAsAccount CurrentUser is the default and is the easiest choice for user-desktop automation.
- -RunAsAccount SpecificUser is useful for shared machines and server jobs tied to a known account.
- -RunAsAccount System is useful for machine-level automation, startup tasks, and unattended server jobs.
 
Background notes:
- -Background means "run whether user is logged on or not".
- -DoNotStorePassword uses S4U:
  - does not store the password
  - commonly requires elevation
  - typically cannot access network resources at runtime
- -Credential uses PASSWORD mode:
  - stores credentials in Task Scheduler
  - supports network resources
  - avoids interactive prompts when supplied up front
 
Repetition notes:
- -RepeatEveryMinutes can be used with -DailyAtTime, -Startup, -LogonThisUser, or -LogonAnyUser.
- When repetition is used with -DailyAtTime and -RepeatFor is omitted, the function defaults to 23:59.
- When repetition is used with startup or logon triggers and -RepeatFor is omitted, the function defaults to 1 day.
- For daily repeating tasks, if today's anchor has already passed, a one-time bridge trigger is added automatically so the schedule starts on today's cadence instead of waiting until tomorrow.
 
When elevation is usually NOT required to add/register the task:
- Current user + interactive task
- Current user + daily task
- Current user + logon task for the same current user
- Current user + many simple desktop-automation scenarios
 
When elevation IS typically required to add/register the task:
- -RunAsAccount System
- -Startup (boot trigger)
- -RunAsAccount SpecificUser for another account
- -Background with -DoNotStorePassword (S4U), depending on policy and environment
- creating or updating tasks in protected task folders / locations where the current user lacks rights
 
Important distinction:
- Elevation to ADD the task is about whether the current PowerShell session is allowed to register that task definition.
- -Highest affects how the task RUNS later.
- -Highest does not by itself grant permission to register an otherwise protected task.
 
Practical guidance for non-admin users:
- Use -RunAsAccount CurrentUser for the smoothest experience.
- Prefer -LogonThisUser or -DailyAtTime for user-space automation.
- Avoid -Startup, -RunAsAccount System, and other-user scenarios unless you are elevated.
- If the task must use network resources in background mode, prefer -Credential over S4U.
 
Fast-win improvements:
- If -DoNotStorePassword is set without -Background, background mode is enabled automatically.
- If -LogonAnyUser is combined with an interactive non-System principal, the function warns about the real behavior.
- If S4U is used and arguments appear to reference UNC / SMB paths, the function warns about network access limitations.
- Daily repeating schedules started later in the day automatically receive a bridge trigger for today's cadence.
- Multiple-instance behavior is configurable through -MultipleInstancesPolicy.
- HRESULT failures are surfaced with targeted hints where possible.
- A structured summary object is returned; -Json also emits JSON.
 
.PARAMETER TaskName
Leaf name of the task.
 
.PARAMETER TaskFolder
Task folder (for example '\MyCompany\MyApp').
The folder is created if it does not already exist.
Default: '\'.
 
.PARAMETER ActionPath
Executable to run, such as 'powershell.exe' or a full path to a program or script host.
 
.PARAMETER ActionArguments
Arguments passed to the action.
 
.PARAMETER WorkingDirectory
Working directory for the action.
Useful to avoid relative-path issues.
 
.PARAMETER RunAsAccount
Run context:
- 'CurrentUser' (default)
- 'SpecificUser'
- 'System'
 
Alias: -RunAs
 
.PARAMETER SpecificUser
User for SpecificUser context.
Accepts 'DOMAIN\User' or 'User@Domain'.
 
.PARAMETER Background
Run even when the user is not logged on.
 
Alias: -RunWhetherUserLoggedOn
 
.PARAMETER DoNotStorePassword
Use S4U ("Do not store password").
Implies -Background.
Commonly requires elevation.
Best for local-only background work that does not require network resources.
 
Alias: -NoStorePassword
 
.PARAMETER Credential
PSCredential for PASSWORD mode.
Avoids prompting and supports network access for background execution.
 
.PARAMETER NoPrompt
If a password is needed and -Credential is not supplied, throw instead of prompting.
 
Alias: -NonInteractive
 
.PARAMETER Highest
Request "Run with highest privileges" for the run-context user.
 
.PARAMETER LogonThisUser
Trigger at logon for the run-as user (CurrentUser or SpecificUser).
 
Alias: -AtLogon
 
.PARAMETER LogonAnyUser
Trigger at logon of any user.
 
Alias: -AtLogonAnyUser
 
.PARAMETER Startup
Trigger at system startup / boot.
 
Alias: -AtStartup
 
.PARAMETER DailyAtTime
Daily time in invariant 24-hour format ('HH:mm') or as a [DateTime].
Can be combined with repetition.
 
Alias: -DailyAt
 
.PARAMETER RepeatEveryMinutes
Repeat the fired trigger every N minutes.
Typical example: 15.
 
Can be used with:
- -DailyAtTime
- -Startup
- -LogonThisUser
- -LogonAnyUser
 
For daily repeating tasks, the function automatically adds a one-time bridge trigger for today if the daily anchor has already passed.
 
.PARAMETER RepeatFor
How long the repetition window stays active.
 
Accepts:
- [TimeSpan]
- 'hh:mm[:ss]'
- 'd.hh:mm:ss'
- integer minutes
 
Smart defaults:
- with -DailyAtTime: 23:59
- with -Startup / -Logon*: 1 day
 
.PARAMETER MultipleInstancesPolicy
How Task Scheduler behaves when a new run is triggered while another instance of the same scheduled task is already running.
 
Valid values:
- IgnoreNew -> skip the new run
- Parallel -> allow overlap
- Queue -> run the next instance after the current one finishes
- StopExisting -> stop the current run and start the new one
 
Default: IgnoreNew
 
.PARAMETER StopAtDurationEnd
When repetition is configured, stop a still-running instance when the repetition window ends.
 
.PARAMETER WakeComputer
Attempt to wake the computer to run.
Depends on firmware, OS policy, and hardware support.
 
Alias: -WakeToRun
 
.PARAMETER ForceRegister
Register even if ActionPath or referenced script does not currently exist.
Disables the action-path guard.
 
.PARAMETER RunNow
After successful registration, start the task once immediately.
 
.PARAMETER Quiet
Suppress Write-Host informational and hint output.
Errors still throw.
 
.PARAMETER Json
Emit the returned summary object as JSON and also return the object.
 
.PARAMETER Description
Optional task description.
 
.EXAMPLE
Works without admin: start a visible desktop app when the current user signs in
 
New-CompatScheduledTask `
  -TaskName 'RegEdit-AtLogon' `
  -ActionPath 'C:\Windows\regedit.exe' `
  -LogonThisUser
 
.EXAMPLE
Works without admin: run a PowerShell script every day at 09:30 as the current user
 
New-CompatScheduledTask `
  -TaskName 'User-Daily-0930' `
  -DailyAtTime '09:30' `
  -ActionPath "$env:WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe" `
  -ActionArguments '-NoProfile -ExecutionPolicy Bypass -File "C:\Scripts\DailyUserJob.ps1"' `
  -WorkingDirectory 'C:\Scripts'
 
.EXAMPLE
Works without admin: recurring job every 15 minutes with the default no-overlap behavior
If a run is still active, the next repeated run is skipped.
 
New-CompatScheduledTask `
  -TaskName 'User-Every15m' `
  -DailyAtTime '00:00' `
  -RepeatEveryMinutes 15 `
  -RunNow `
  -ActionPath "$env:WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe" `
  -ActionArguments '-NoProfile -ExecutionPolicy Bypass -File "C:\Scripts\LoopJob.ps1"' `
  -WorkingDirectory 'C:\Scripts'
 
.EXAMPLE
Allow overlap: start a new instance every 15 minutes even if the previous run is still active
 
New-CompatScheduledTask `
  -TaskName 'User-Every15m-Parallel' `
  -DailyAtTime '00:00' `
  -RepeatEveryMinutes 15 `
  -MultipleInstancesPolicy Parallel `
  -ActionPath "$env:WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe" `
  -ActionArguments '-NoProfile -ExecutionPolicy Bypass -File "C:\Scripts\LoopJob.ps1"' `
  -WorkingDirectory 'C:\Scripts'
 
.EXAMPLE
Queue overlap: if a run is still active, queue the next run and start it when the current one finishes
 
New-CompatScheduledTask `
  -TaskName 'User-Every15m-Queue' `
  -DailyAtTime '00:00' `
  -RepeatEveryMinutes 15 `
  -MultipleInstancesPolicy Queue `
  -ActionPath "$env:WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe" `
  -ActionArguments '-NoProfile -ExecutionPolicy Bypass -File "C:\Scripts\LoopJob.ps1"' `
  -WorkingDirectory 'C:\Scripts'
 
.EXAMPLE
Restart freshest: stop the running instance and start a fresh one at each repeated trigger
 
New-CompatScheduledTask `
  -TaskName 'User-Every15m-StopExisting' `
  -DailyAtTime '00:00' `
  -RepeatEveryMinutes 15 `
  -MultipleInstancesPolicy StopExisting `
  -ActionPath "$env:WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe" `
  -ActionArguments '-NoProfile -ExecutionPolicy Bypass -File "C:\Scripts\LoopJob.ps1"' `
  -WorkingDirectory 'C:\Scripts'
 
.EXAMPLE
May work without admin, but depends on policy and credentials: run in the background as the current user with stored credentials
 
$cred = Get-Credential
New-CompatScheduledTask `
  -TaskName 'User-Background-Network' `
  -RunAsAccount CurrentUser `
  -Background `
  -Credential $cred `
  -DailyAtTime '18:00' `
  -ActionPath "$env:WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe" `
  -ActionArguments '-NoProfile -ExecutionPolicy Bypass -File "\\server\share\job.ps1"'
 
.EXAMPLE
Requires admin in most environments: run for a specific named account at that account's logon
 
New-CompatScheduledTask `
  -TaskName 'OpsUser-AtLogon' `
  -RunAsAccount SpecificUser `
  -SpecificUser 'CONTOSO\OpsUser' `
  -LogonThisUser `
  -ActionPath "$env:WINDIR\System32\notepad.exe"
 
.EXAMPLE
Requires admin: run a nightly maintenance job under a specific service-style account in the background
 
$cred = Get-Credential 'CONTOSO\svc_batch'
New-CompatScheduledTask `
  -TaskName 'SvcBatch-Nightly' `
  -TaskFolder '\Company\ServerJobs' `
  -RunAsAccount SpecificUser `
  -SpecificUser 'CONTOSO\svc_batch' `
  -Background `
  -Credential $cred `
  -Highest `
  -DailyAtTime '02:00' `
  -ActionPath "$env:WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe" `
  -ActionArguments '-NoProfile -ExecutionPolicy Bypass -File "D:\Jobs\nightly.ps1"' `
  -WorkingDirectory 'D:\Jobs'
 
.EXAMPLE
Requires admin: run every 15 minutes all day as LocalSystem without overlap
 
New-CompatScheduledTask `
  -TaskName 'System-Every15m' `
  -TaskFolder '\Company\ServerJobs' `
  -RunAsAccount System `
  -Highest `
  -DailyAtTime '00:00' `
  -RepeatEveryMinutes 15 `
  -MultipleInstancesPolicy IgnoreNew `
  -ActionPath "$env:WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe" `
  -ActionArguments '-NoProfile -ExecutionPolicy Bypass -File "D:\Jobs\heartbeat.ps1"' `
  -WorkingDirectory 'D:\Jobs'
 
.EXAMPLE
Requires admin: start at boot as LocalSystem and keep repeating
 
New-CompatScheduledTask `
  -TaskName 'System-Startup-Repeating' `
  -TaskFolder '\Company\ServerJobs' `
  -RunAsAccount System `
  -Highest `
  -Startup `
  -RepeatEveryMinutes 30 `
  -RepeatFor '1.00:00:00' `
  -MultipleInstancesPolicy Queue `
  -ActionPath "$env:WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe" `
  -ActionArguments '-NoProfile -ExecutionPolicy Bypass -File "D:\Jobs\startup-loop.ps1"' `
  -WorkingDirectory 'D:\Jobs'
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string]$TaskName,
        [string]$TaskFolder = '\',

        [Parameter(Mandatory)] [string]$ActionPath,
        [string]$ActionArguments = '',
        [string]$WorkingDirectory,

        [Alias('RunAs')]
        [ValidateSet('CurrentUser','SpecificUser','System')]
        [string]$RunAsAccount = 'CurrentUser',

        [string]$SpecificUser,

        [Alias('RunWhetherUserLoggedOn')]
        [switch]$Background,

        [Alias('NoStorePassword')]
        [switch]$DoNotStorePassword,

        [System.Management.Automation.PSCredential]$Credential,

        [Alias('NonInteractive')]
        [switch]$NoPrompt,

        [switch]$Highest,

        [Alias('AtLogon')]
        [switch]$LogonThisUser,

        [Alias('AtLogonAnyUser')]
        [switch]$LogonAnyUser,

        [Alias('AtStartup')]
        [switch]$Startup,

        [Alias('DailyAt')]
        [object]$DailyAtTime,

        [ValidateRange(1,1439)]
        [int]$RepeatEveryMinutes,

        [object]$RepeatFor,

        [ValidateSet('IgnoreNew','Parallel','Queue','StopExisting')]
        [string]$MultipleInstancesPolicy = 'IgnoreNew',

        [switch]$StopAtDurationEnd,

        [Alias('WakeToRun')]
        [switch]$WakeComputer,

        [switch]$ForceRegister,

        [switch]$RunNow,

        [switch]$Quiet,

        [switch]$Json,

        [string]$Description
    )

    function _writeInfo($m){ if(-not $Quiet){ Write-Host "[INFO] $m" } }
    function _writeHint($m){ if(-not $Quiet){ Write-Host "[HINT] $m" -ForegroundColor Yellow } }
    function _writeErrT($m){ Write-Host "[ERROR] $m" -ForegroundColor Red }
    function _releaseCom($o){ if($o){ try{ [void][Runtime.InteropServices.Marshal]::ReleaseComObject($o) }catch{} } }

    function Test-UserMatches($expect, $actual){
        if(-not $actual){ return $false }
        if($expect -eq $actual){ return $true }
        try{
            $a1 = (New-Object System.Security.Principal.NTAccount($actual)).Translate([System.Security.Principal.SecurityIdentifier]).Translate([System.Security.Principal.NTAccount]).Value
            if($a1 -eq $expect){ return $true }
        } catch {}
        return $false
    }

    function Resolve-ActionPath([string]$PathText){
        if([string]::IsNullOrWhiteSpace($PathText)){ return $null }

        if(Test-Path -LiteralPath $PathText){
            try { return (Resolve-Path -LiteralPath $PathText -ErrorAction Stop).Path } catch { return $PathText }
        }

        try{
            $cmd = Get-Command -Name $PathText -ErrorAction Stop | Select-Object -First 1
            if($cmd.CommandType -in @('Application','ExternalScript') -and $cmd.Path){
                return $cmd.Path
            }
        } catch {}

        return $null
    }

    function Normalize-TaskFolderPath([string]$path){
        $path = ($path -replace '/','\')
        if([string]::IsNullOrWhiteSpace($path)){ return '\' }
        if($path -eq '\'){ return '\' }
        return '\' + $path.Trim('\')
    }

    function ConvertTo-TaskIsoDuration([TimeSpan]$ts){
        if($ts -lt [TimeSpan]::FromMinutes(1)){
            throw "Repetition duration must be at least 1 minute."
        }

        $out = 'P'
        if($ts.Days -gt 0){ $out += "$($ts.Days)D" }

        $timePart = ''
        if($ts.Hours   -gt 0){ $timePart += "$($ts.Hours)H" }
        if($ts.Minutes -gt 0){ $timePart += "$($ts.Minutes)M" }
        if($ts.Seconds -gt 0){ $timePart += "$($ts.Seconds)S" }

        if([string]::IsNullOrEmpty($timePart)){
            $timePart = '0S'
        }

        return $out + 'T' + $timePart
    }

    function Resolve-RepeatTimeSpan([object]$value){
        if($null -eq $value){ return $null }

        if($value -is [TimeSpan]){ return [TimeSpan]$value }

        if($value -is [int] -or $value -is [long]){
            return [TimeSpan]::FromMinutes([double]$value)
        }

        $s = [string]$value
        $ts = [TimeSpan]::Zero
        if([TimeSpan]::TryParse($s, [ref]$ts)){
            return $ts
        }

        throw "Invalid -RepeatFor. Use [TimeSpan], 'hh:mm[:ss]', 'd.hh:mm:ss', or integer minutes."
    }

    function Set-TriggerRepetition($trigger, [string]$intervalIso, [string]$durationIso, [bool]$stopAtDurationEnd){
        if(-not $trigger -or [string]::IsNullOrWhiteSpace($intervalIso)){ return }
        $rp = $trigger.Repetition
        $rp.Interval = $intervalIso
        $rp.Duration = $durationIso
        $rp.StopAtDurationEnd = $stopAtDurationEnd
    }

    function Get-NextAlignedBoundary([datetime]$Anchor, [int]$IntervalMinutes, [datetime]$Now){
        if($Now -lt $Anchor){ return $Anchor }

        $elapsed = $Now - $Anchor
        $steps = [math]::Floor($elapsed.TotalMinutes / $IntervalMinutes) + 1
        $candidate = $Anchor.AddMinutes($steps * $IntervalMinutes)

        if($candidate -le $Now.AddSeconds(5)){
            $candidate = $candidate.AddMinutes($IntervalMinutes)
        }

        return $candidate
    }

    $TaskInstancePolicyMap = @{
        Parallel     = 0
        Queue        = 1
        IgnoreNew    = 2
        StopExisting = 3
    }

    $id  = [Security.Principal.WindowsIdentity]::GetCurrent()
    $pri = [Security.Principal.WindowsPrincipal]$id
    $IsElevated = $pri.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
    $CurrentUser = $id.Name
    $TaskFolder = Normalize-TaskFolderPath $TaskFolder

    if([string]::IsNullOrWhiteSpace($TaskName)){
        _writeErrT "TaskName cannot be empty."
        throw "Invalid TaskName."
    }
    if($TaskName -match '[\/:\*\?"<>|]'){
        _writeErrT ("TaskName contains illegal characters: {0}" -f $TaskName)
        _writeHint "Disallowed: / : * ? "" < > |"
        throw "Invalid TaskName."
    }

    if($RunAsAccount -eq 'SpecificUser' -and [string]::IsNullOrWhiteSpace($SpecificUser)){
        _writeErrT "When -RunAsAccount SpecificUser is used, you must provide -SpecificUser."
        _writeHint "Accepted formats: DOMAIN\User or user@domain."
        throw "SpecificUser is required."
    }

    if($RunAsAccount -eq 'SpecificUser' -and -not [string]::IsNullOrWhiteSpace($SpecificUser)){
        if($SpecificUser -notmatch '^(?:[^\\\/:\*\?"<>|]+\@[^\s@]+|[^\\\/:\*\?"<>|]+\\[^\\\/:\*\?"<>|]+)$'){
            _writeHint ("SpecificUser format looks unusual: {0}. Expected DOMAIN\User or user@domain." -f $SpecificUser)
        }
    }

    if($DoNotStorePassword -and -not $Background){
        _writeInfo "-DoNotStorePassword implies background mode; enabling -Background."
        $Background = $true
    }

    if (-not ($LogonThisUser -or $LogonAnyUser -or $Startup -or $DailyAtTime)) {
        _writeErrT "No trigger specified."
        _writeHint "Add -LogonThisUser, -LogonAnyUser, -Startup, or -DailyAtTime 'HH:mm'."
        throw "At least one trigger is required."
    }

    if($RunAsAccount -eq 'System' -and $LogonThisUser){
        _writeErrT "LogonThisUser cannot be used with -RunAsAccount System. Use -LogonAnyUser or -Startup."
        throw "Invalid trigger combination."
    }

    $resolvedActionPath = Resolve-ActionPath $ActionPath
    if($resolvedActionPath){
        $ActionPath = $resolvedActionPath
    } elseif(-not $ForceRegister) {
        _writeErrT ("ActionPath not found: {0}" -f $ActionPath)
        _writeHint  "Provide a full path, something resolvable by Get-Command, or pass -ForceRegister."
        throw "ActionPath not found."
    } else {
        _writeHint ("Continuing with non-resolved ActionPath '{0}' (command lookup at runtime)." -f $ActionPath)
    }

    if(-not $IsElevated -and $RunAsAccount -eq 'System'){
        _writeErrT "System principal requires an elevated PowerShell."
        _writeHint "Relaunch as Administrator or use -Background with -Credential for user context."
        throw "Elevation required."
    }
    if(-not $IsElevated -and $Startup){
        _writeErrT "Startup trigger requires an elevated PowerShell."
        _writeHint "Only Administrators can create a task with a boot trigger."
        throw "Elevation required."
    }
    if(-not $IsElevated -and $RunAsAccount -eq 'SpecificUser' -and $SpecificUser -and -not (Test-UserMatches -expect $CurrentUser -actual $SpecificUser)){
        _writeErrT ("Cannot create a task for another user '{0}' from a non-elevated session." -f $SpecificUser)
        _writeHint "Run elevated, or use -RunAsAccount CurrentUser."
        throw "Elevation required."
    }
    if($Background -and $DoNotStorePassword -and -not $IsElevated){
        _writeErrT "S4U (Do not store password) commonly requires elevation."
        _writeHint "Run elevated or switch to PASSWORD mode with -Credential."
        throw "Elevation recommended for S4U."
    }

    if($LogonAnyUser -and $RunAsAccount -ne 'System' -and -not $Background){
        _writeHint "LogonAnyUser + interactive principal runs only when the run-as user logs in. For true 'any user' execution use -Background (PASSWORD/S4U) or -RunAsAccount System."
    }

    $dailyStart = $null
    if($DailyAtTime){
        if($DailyAtTime -is [datetime]){
            $dailyStart = [datetime]$DailyAtTime
        } else {
            $fmt = 'HH:mm'
            $ci  = [System.Globalization.CultureInfo]::InvariantCulture
            $styles = [System.Globalization.DateTimeStyles]::None
            $parsed = [datetime]::MinValue
            $ok = [datetime]::TryParseExact([string]$DailyAtTime,$fmt,$ci,$styles,[ref]$parsed)
            if(-not $ok){
                _writeErrT ("Could not parse -DailyAtTime '{0}'." -f $DailyAtTime)
                _writeHint  "Use 24h format 'HH:mm' (e.g., '09:30') or pass a [DateTime]."
                throw "Invalid DailyAtTime."
            }
            $dailyStart = $parsed
        }
    }

    if($PSBoundParameters.ContainsKey('RepeatFor') -and -not $PSBoundParameters.ContainsKey('RepeatEveryMinutes')){
        _writeErrT "-RepeatFor requires -RepeatEveryMinutes."
        throw "Invalid repetition configuration."
    }

    $repeatIntervalIso = $null
    $repeatDurationIso = $null
    $repeatForTs = $null

    if($PSBoundParameters.ContainsKey('RepeatEveryMinutes')){
        $repeatIntervalIso = ('PT{0}M' -f $RepeatEveryMinutes)

        if($PSBoundParameters.ContainsKey('RepeatFor')){
            $repeatForTs = Resolve-RepeatTimeSpan $RepeatFor
        }
        elseif($DailyAtTime){
            $repeatForTs = [TimeSpan]::FromHours(23) + [TimeSpan]::FromMinutes(59)
            _writeInfo "-RepeatEveryMinutes with -DailyAtTime and no -RepeatFor -> defaulting to 23:59."
        }
        elseif($Startup -or $LogonThisUser -or $LogonAnyUser){
            $repeatForTs = [TimeSpan]::FromDays(1)
            _writeHint "-RepeatEveryMinutes with Startup/Logon and no -RepeatFor defaults to 1 day. Set -RepeatFor explicitly to change that."
        } else {
            _writeErrT "-RepeatEveryMinutes needs a base trigger such as -DailyAtTime, -Startup, -LogonThisUser, or -LogonAnyUser."
            throw "Invalid repetition configuration."
        }

        if($repeatForTs -lt [TimeSpan]::FromMinutes($RepeatEveryMinutes)){
            _writeHint "-RepeatFor is shorter than -RepeatEveryMinutes, so the task may only run once per trigger."
        }

        if($DailyAtTime -and $repeatForTs -gt ([TimeSpan]::FromHours(23) + [TimeSpan]::FromMinutes(59))){
            _writeHint "Daily repetition duration exceeds 23:59. Daily repetition windows can overlap across days."
        }

        switch($MultipleInstancesPolicy){
            'Parallel'     { _writeHint "MultipleInstancesPolicy=Parallel allows overlapping runs. Use this only when concurrent instances are safe." }
            'Queue'        { _writeHint "MultipleInstancesPolicy=Queue can build backlog if each run takes longer than the trigger interval." }
            'StopExisting' { _writeHint "MultipleInstancesPolicy=StopExisting will terminate the current run when the next trigger fires." }
        }

        $repeatDurationIso = ConvertTo-TaskIsoDuration $repeatForTs
    }

    if($Background -and $DoNotStorePassword){
        $arguments = [string]$ActionArguments
        if($ActionPath -like '\\*' -or $arguments -match '(?i)(^|[^A-Za-z0-9_])(\\\\[^\\]+\\|smb:)'){
            _writeHint "S4U selected: background token has no network access. If you need UNC/mapped shares, use -Credential instead."
        }
    }

    $bridgeTriggerNeeded = $false
    $bridgeTriggerAdded = $false
    $bridgeStart = $null
    $bridgeStartText = $null
    $bridgeDurationIso = $null

    if($dailyStart -and $repeatIntervalIso){
        $now = Get-Date
        $todayAnchor = [datetime]::Today.AddHours($dailyStart.Hour).AddMinutes($dailyStart.Minute)
        $todayWindowEnd = $todayAnchor.Add($repeatForTs)

        if($todayAnchor -lt $now -and $now -lt $todayWindowEnd){
            $candidateBridgeStart = Get-NextAlignedBoundary -Anchor $todayAnchor -IntervalMinutes $RepeatEveryMinutes -Now $now

            if($candidateBridgeStart -lt $todayWindowEnd){
                $remaining = $todayWindowEnd - $candidateBridgeStart
                if($remaining -ge [TimeSpan]::FromMinutes(1)){
                    $bridgeTriggerNeeded = $true
                    $bridgeStart = $candidateBridgeStart
                    $bridgeStartText = $bridgeStart.ToString('HH:mm')
                    $bridgeDurationIso = ConvertTo-TaskIsoDuration $remaining
                }
            }
        }
    }

    $svc = $null
    $folder = $null
    $def = $null
    $trigs = $null
    $act = $null
    $registeredTask = $null
    $runningTask = $null

    try{
        $svc = New-Object -ComObject 'Schedule.Service'
        $svc.Connect()

        function Resolve-TaskFolder([__comobject]$service,[string]$path){
            $path = ($path -replace '/','\')
            if([string]::IsNullOrWhiteSpace($path)){ $path='\' }
            if($path -eq '\'){ return $service.GetFolder('\') }

            $parts = $path.Trim('\').Split('\')
            $cur = $service.GetFolder('\')
            foreach($p in $parts){
                try { $cur = $cur.GetFolder("\$p") }
                catch { $cur = $cur.CreateFolder($p) }
            }
            return $cur
        }

        $folder = Resolve-TaskFolder -service $svc -path $TaskFolder
        $def = $svc.NewTask(0)

        $def.RegistrationInfo.Description = $Description
        $def.Settings.Enabled = $true
        $def.Settings.AllowDemandStart = $true
        $def.Settings.MultipleInstances = $TaskInstancePolicyMap[$MultipleInstancesPolicy]
        $def.Settings.StopIfGoingOnBatteries = $false
        $def.Settings.DisallowStartIfOnBatteries = $false
        $def.Settings.RunOnlyIfNetworkAvailable = $false
        $def.Settings.StartWhenAvailable = $true
        $def.Settings.ExecutionTimeLimit = 'PT24H'
        if($WakeComputer){ $def.Settings.WakeToRun = $true }

        $TaskLogon = @{ Password=1; S4U=2; Interactive=3; Service=5 }
        $p = $def.Principal
        if($Highest){ $p.RunLevel = 1 }

        $RegUser = $null
        $RegPwd = $null
        $RegLogon = $null

        switch($RunAsAccount){
            'System'{
                $p.UserId = 'SYSTEM'
                $p.LogonType = $TaskLogon.Service
                $RegUser = 'SYSTEM'
                $RegLogon = $TaskLogon.Service
            }
            'CurrentUser'{
                $p.UserId = $CurrentUser
                if($Background){
                    if($DoNotStorePassword){
                        $p.LogonType = $TaskLogon.S4U
                        $RegUser = $CurrentUser
                        $RegLogon = $TaskLogon.S4U
                    } else {
                        if(-not $Credential){
                            if($NoPrompt){ throw "Credentials required; supply -Credential or use -DoNotStorePassword (elevated)." }
                            $Credential = Get-Credential -Message "Enter password for $CurrentUser to run when not logged on"
                        } elseif(-not (Test-UserMatches -expect $CurrentUser -actual $Credential.UserName)){
                            _writeHint ("Credential user '{0}' does not match current user '{1}'. This can cause 0x8007052E." -f $Credential.UserName, $CurrentUser)
                        }

                        $p.LogonType = $TaskLogon.Password
                        $RegUser = $Credential.UserName
                        $RegPwd = $Credential.GetNetworkCredential().Password
                        $RegLogon = $TaskLogon.Password
                    }
                } else {
                    $p.LogonType = $TaskLogon.Interactive
                    $RegLogon = $TaskLogon.Interactive
                }
            }
            'SpecificUser'{
                $p.UserId = $SpecificUser
                if($Background){
                    if($DoNotStorePassword){
                        $p.LogonType = $TaskLogon.S4U
                        $RegUser = $SpecificUser
                        $RegLogon = $TaskLogon.S4U
                    } else {
                        if(-not $Credential -or -not (Test-UserMatches -expect $SpecificUser -actual $Credential.UserName)){
                            if($NoPrompt){ throw "Credentials for $SpecificUser required; username must match the run-as account." }
                            $Credential = Get-Credential -UserName $SpecificUser -Message "Enter password for $SpecificUser to run when not logged on"
                        }

                        $p.LogonType = $TaskLogon.Password
                        $RegUser = $Credential.UserName
                        $RegPwd = $Credential.GetNetworkCredential().Password
                        $RegLogon = $TaskLogon.Password
                    }
                } else {
                    $p.LogonType = $TaskLogon.Interactive
                    $RegLogon = $TaskLogon.Interactive
                }
            }
        }

        $act = $def.Actions.Create(0)
        $act.Path = $ActionPath
        if($ActionArguments){ $act.Arguments = $ActionArguments }
        if($WorkingDirectory){ $act.WorkingDirectory = $WorkingDirectory }

        $trigs = $def.Triggers

        if($Startup){
            $bt = $trigs.Create(8)
            Set-TriggerRepetition $bt $repeatIntervalIso $repeatDurationIso ([bool]$StopAtDurationEnd)
            _writeInfo "Added Startup trigger."
        }

        if($LogonThisUser){
            $lt = $trigs.Create(9)
            if($RunAsAccount -eq 'CurrentUser'){ $lt.UserId = $CurrentUser }
            elseif($RunAsAccount -eq 'SpecificUser'){ $lt.UserId = $SpecificUser }
            Set-TriggerRepetition $lt $repeatIntervalIso $repeatDurationIso ([bool]$StopAtDurationEnd)
            _writeInfo "Added Logon trigger for specific user."
        }

        if($LogonAnyUser){
            $la = $trigs.Create(9)
            $la.UserId = $null
            Set-TriggerRepetition $la $repeatIntervalIso $repeatDurationIso ([bool]$StopAtDurationEnd)
            _writeInfo "Added Logon trigger for ANY user."
        }

        if($dailyStart){
            $anchorForDisplay = [datetime]::Today.AddHours($dailyStart.Hour).AddMinutes($dailyStart.Minute)
            $start = $anchorForDisplay
            if ($start -lt (Get-Date)) { $start = $start.AddDays(1) }

            $dt = $trigs.Create(2)  # DAILY
            $dt.StartBoundary = $start.ToString('s')
            $dt.DaysInterval  = 1
            Set-TriggerRepetition $dt $repeatIntervalIso $repeatDurationIso ([bool]$StopAtDurationEnd)

            if($repeatIntervalIso){
                _writeInfo ("Added Daily trigger at {0} with repetition every {1} minute(s)." -f $anchorForDisplay.ToString('HH:mm'), $RepeatEveryMinutes)
            } else {
                _writeInfo ("Added Daily trigger at {0}." -f $anchorForDisplay.ToString('HH:mm'))
            }
        }

        if($bridgeTriggerNeeded){
            $tt = $trigs.Create(1)   # TIME / one-time
            $tt.StartBoundary = $bridgeStart.ToString('s')
            Set-TriggerRepetition $tt $repeatIntervalIso $bridgeDurationIso ([bool]$StopAtDurationEnd)
            $bridgeTriggerAdded = $true
            _writeInfo ("Added one-time bridge trigger at {0} so repetition starts on today's schedule." -f $bridgeStartText)
        }

        $TASK_CREATE_OR_UPDATE = 6
        $taskPath = if($TaskFolder -eq '\'){ "\$TaskName" } else { "$TaskFolder\$TaskName" }

        try{
            $registeredTask = $folder.RegisterTaskDefinition($TaskName, $def, $TASK_CREATE_OR_UPDATE, $RegUser, $RegPwd, $RegLogon, $null)
            if(-not $Quiet){
                Write-Host ("[OK] Task '{0}' created/updated." -f $taskPath)
                if($WakeComputer){ _writeHint "Wake timers depend on firmware/policy; may be ignored on some devices." }
            }
        } catch {
            $hr = ('0x{0:X8}' -f $_.Exception.HResult)
            _writeErrT ("Task registration failed (HRESULT={0}). {1}" -f $hr, $_.Exception.Message)
            switch($hr){
                '0x80070005' { _writeHint "Access denied. Elevate for System/other-user, boot trigger, or use a delegated -TaskFolder." }
                '0x8007052E' { _writeHint "Logon failure (bad credentials). Verify -Credential username matches the run-as account." }
                '0x80070002' { _writeHint "File not found. Check ActionPath and any script paths in -ActionArguments." }
                '0x80041316' { _writeHint "One or more properties are invalid (e.g., logon type vs. principal). Review S4U/PASSWORD choices." }
                '0x80041314' { _writeHint "Account information not set. PASSWORD mode requires valid -Credential." }
                '0x80041309' { _writeHint "Invalid task name. Avoid special characters." }
                default      { _writeHint "Verify elevation (if needed), credentials, action paths, and folder ACLs." }
            }
            throw
        }

        $startedNow = $false
        if($RunNow){
            try{
                $runningTask = $registeredTask.Run($null)
                $startedNow = $true
                _writeInfo "Started task immediately (-RunNow)."
            } catch {
                $hr = ('0x{0:X8}' -f $_.Exception.HResult)
                _writeErrT ("Task was registered, but immediate start failed (HRESULT={0}). {1}" -f $hr, $_.Exception.Message)
                switch($hr){
                    '0x80041326' { _writeHint "The task is disabled. Enable it first." }
                    '0x80070534' { _writeHint "No mapping between account names and security IDs. Re-check the run-as account." }
                    default      { _writeHint "The task exists, but RunNow failed. Try starting it manually once from Task Scheduler to inspect the runtime context." }
                }
                throw
            }
        }

        $logonTypeName = switch($RegLogon){
            1 {'Password'}
            2 {'S4U'}
            3 {'Interactive'}
            5 {'Service'}
            default {"$RegLogon"}
        }

        $bgMode = if($RunAsAccount -eq 'System'){'Service'}
                  elseif($Background -and $DoNotStorePassword){'S4U'}
                  elseif($Background){'Password'}
                  else{'Interactive'}

        $trigList = @()
        if($Startup){ $trigList += 'Startup' }
        if($LogonThisUser){ $trigList += 'Logon-ThisUser' }
        if($LogonAnyUser){ $trigList += 'Logon-AnyUser' }
        if($dailyStart){
            $trigList += ('Daily@' + ([datetime]::Today.AddHours($dailyStart.Hour).AddMinutes($dailyStart.Minute)).ToString('HH:mm'))
        }
        if($bridgeTriggerAdded){ $trigList += ('Bridge@' + $bridgeStartText) }

        $result = [pscustomobject]@{
            TaskPath               = $taskPath
            TaskFolder             = $TaskFolder
            TaskName               = $TaskName
            Principal              = $RunAsAccount
            PrincipalUser          = if($RunAsAccount -eq 'CurrentUser'){$CurrentUser} elseif($RunAsAccount -eq 'SpecificUser'){$SpecificUser} else {'SYSTEM'}
            LogonType              = $logonTypeName
            Background             = $bgMode
            Triggers               = $trigList
            RepeatEveryMinutes     = if($repeatIntervalIso){ $RepeatEveryMinutes } else { $null }
            RepeatFor              = if($repeatDurationIso){ $repeatDurationIso } else { $null }
            MultipleInstancesPolicy= $MultipleInstancesPolicy
            StopAtDurationEnd      = [bool]$StopAtDurationEnd
            BridgeTriggerAdded     = [bool]$bridgeTriggerAdded
            BridgeTriggerStart     = $bridgeStartText
            ActionPath             = $ActionPath
            ActionArgs             = $ActionArguments
            WorkingDir             = $WorkingDirectory
            Elevated               = $IsElevated
            WakeComputer           = [bool]$WakeComputer
            StartedNow             = $startedNow
        }

        if($Json){
            Write-Host ($result | ConvertTo-Json -Depth 4)
        }

        return $result
    }
    finally{
        _releaseCom $runningTask
        _releaseCom $registeredTask
        _releaseCom $act
        _releaseCom $trigs
        _releaseCom $def
        _releaseCom $folder
        _releaseCom $svc
    }
}