<#
.SYNOPSIS
    Checks password expiration for both AD and local Windows accounts.
.DESCRIPTION
    Auto-detects whether the current user is a domain or local account,
    queries the appropriate source for password expiration, and shows
    a notification if the password expires within the warning threshold.
    Can be scheduled via Task Scheduler to run at logon or daily.
.PARAMETER WarningDays
    Number of days before expiration to start warning. Default: 14
.PARAMETER User
    Check a specific user instead of the current user.
.PARAMETER Quiet
    Suppress output unless password is within the warning window.
.PARAMETER Test
    Force notification even if expiry is not near.
.PARAMETER Help
    Show usage information.
.NOTES
    Author:  Phil Connor <contact@mylinux.work>
    License: MIT (https://opensource.org/licenses/MIT)
    Version: 1.02
#>

param(
    [int]$WarningDays = 14,
    [string]$User = "",
    [switch]$Quiet,
    [switch]$Test,
    [switch]$InstallScheduledTask,
    [int]$TaskIntervalMinutes = 60,
    [Alias("h")]
    [switch]$Help
)

if ($Help) {
    Write-Host @"
Usage: .\password-expiry-check.ps1 [OPTIONS]

Check password expiration for domain (AD) and local Windows accounts.
Auto-detects account type. Shows desktop popup when expiry is near.

Options:
  -WarningDays N             Warning threshold in days (default: 14)
  -User USERNAME             Check a specific user (default: current user)
  -Quiet                     Suppress output unless password is within warning window
  -Test                      Force notification even if expiry is not near
  -InstallScheduledTask      Create a scheduled task for recurring checks
  -TaskIntervalMinutes N     Interval for scheduled task (default: 60)
  -Help                      Show this help

Examples:
  .\password-expiry-check.ps1                         # check current user
  .\password-expiry-check.ps1 -WarningDays 30         # warn within 30 days
  .\password-expiry-check.ps1 -User jdoe              # check specific user
  .\password-expiry-check.ps1 -Test                   # force notification
  .\password-expiry-check.ps1 -Quiet                  # silent unless near expiry
  .\password-expiry-check.ps1 -InstallScheduledTask   # create scheduled task
"@
    exit 0
}

# Determine which username to check
$CheckUsername = if ($User -ne "") { $User } else { $env:USERNAME }

if ($InstallScheduledTask) {
    $taskName = "PasswordExpiryCheck"
    $existingTask = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue

    if (-not $existingTask) {
        $taskAction = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$($MyInvocation.MyCommand.Path)`" -Quiet"

        if (-not $TaskIntervalMinutes -or $TaskIntervalMinutes -le 0) {
            throw "TaskIntervalMinutes must be a positive integer"
        }

        $taskTrigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) -RepetitionInterval (New-TimeSpan -Minutes $TaskIntervalMinutes) -RepetitionDuration (New-TimeSpan -Days 365)
        $taskPrincipal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest

        try {
            Write-Host "Creating scheduled task: $taskName"
            Register-ScheduledTask -TaskName $taskName -Action $taskAction -Trigger $taskTrigger -Principal $taskPrincipal -Description "Checks password expiry every $TaskIntervalMinutes minutes"

            $createdTask = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
            if (-not $createdTask) {
                throw "Failed to verify scheduled task creation"
            }
            Write-Host "Successfully created scheduled task: $taskName" -ForegroundColor Green
        } catch {
            Write-Error "Failed to create auto-start task: $($_.Exception.Message)"
            throw
        }
    } else {
        Write-Host "Scheduled task '$taskName' already exists, skipping creation"
    }
}

function Get-AccountType {
    $computerSystem = Get-WmiObject Win32_ComputerSystem
    $isDomainJoined = $computerSystem.PartOfDomain

    if ($isDomainJoined -and ($env:USERDOMAIN -ne $env:COMPUTERNAME)) {
        return "Domain"
    }
    return "Local"
}

function Get-ADPasswordExpiry {
    param([string]$Username)

    try {
        # Method 1: net user /domain (works without RSAT)
        $netUser = net user $Username /domain 2>&1
        if ($LASTEXITCODE -eq 0) {
            $expiryLine = $netUser | Select-String "Password expires"
            if ($expiryLine) {
                $dateStr = ($expiryLine -split "\s{2,}")[1].Trim()
                if ($dateStr -ne "Never") {
                    return [datetime]::Parse($dateStr)
                }
                return "Never"
            }
        }
    }
    catch {
        Write-Warning "net user /domain method failed: $_"
    }

    try {
        # Method 2: ADSI (no RSAT required)
        $searcher = New-Object DirectoryServices.DirectorySearcher
        $searcher.Filter = "(&(objectCategory=person)(objectClass=user)(sAMAccountName=$Username))"
        $searcher.PropertiesToLoad.AddRange(@("pwdLastSet", "msDS-UserPasswordExpiryTimeComputed"))
        $result = $searcher.FindOne()

        if ($result) {
            $expiryTicks = $result.Properties["msDS-UserPasswordExpiryTimeComputed"]
            if ($expiryTicks -and $expiryTicks[0] -gt 0 -and $expiryTicks[0] -lt [long]::MaxValue) {
                return [datetime]::FromFileTime($expiryTicks[0])
            }
            return "Never"
        }
    }
    catch {
        Write-Warning "ADSI method failed: $_"
    }

    return $null
}

function Get-LocalPasswordExpiry {
    param([string]$Username)

    # Method 1: Get-LocalUser (available on Windows 10/Server 2016+)
    if (Get-Command Get-LocalUser -ErrorAction SilentlyContinue) {
        try {
            $localUser = Get-LocalUser -Name $Username -ErrorAction Stop
            if ($localUser.PasswordExpires) {
                return $localUser.PasswordExpires
            }
            return "Never"
        }
        catch {
            Write-Warning "Get-LocalUser method failed: $_"
        }
    }

    # Method 2: net user (fallback)
    try {
        $netUser = net user $Username 2>&1
        if ($LASTEXITCODE -eq 0) {
            $expiryLine = $netUser | Select-String "Password expires"
            if ($expiryLine) {
                $dateStr = ($expiryLine -split "\s{2,}")[1].Trim()
                if ($dateStr -ne "Never") {
                    return [datetime]::Parse($dateStr)
                }
                return "Never"
            }
        }
    }
    catch {
        Write-Warning "net user method failed: $_"
    }

    return $null
}

function Show-TerminalBanner {
    param(
        [string]$Title,
        [string]$Message
    )

    # Word-wrap message to fit within the box (max 42 chars per line)
    $words = $Message -split '\s+'
    $lines = @()
    $currentLine = ""
    foreach ($word in $words) {
        if ($currentLine.Length -eq 0) {
            $currentLine = $word
        } elseif (($currentLine.Length + 1 + $word.Length) -le 42) {
            $currentLine += " $word"
        } else {
            $lines += $currentLine
            $currentLine = $word
        }
    }
    if ($currentLine.Length -gt 0) { $lines += $currentLine }

    Write-Host ""
    Write-Host ([char]0x2554 + ([string][char]0x2550 * 46) + [char]0x2557)
    Write-Host ([char]0x2551 + (" " * 46) + [char]0x2551)
    Write-Host ([char]0x2551 + "  " + $Title.PadRight(42) + "  " + [char]0x2551)
    Write-Host ([char]0x2551 + ([string][char]0x2500 * 46) + [char]0x2551)
    foreach ($line in $lines) {
        Write-Host ([char]0x2551 + "  " + $line.PadRight(42) + "  " + [char]0x2551)
    }
    Write-Host ([char]0x2551 + (" " * 46) + [char]0x2551)
    Write-Host ([char]0x255A + ([string][char]0x2550 * 46) + [char]0x255D)
    Write-Host ""
}

function Show-MessageBoxNotification {
    param(
        [string]$Title,
        [string]$Message
    )

    # Terminal banner (always shown)
    Show-TerminalBanner -Title $Title -Message $Message

    # Desktop dialog - requires user to click OK to dismiss
    try {
        Add-Type -AssemblyName System.Windows.Forms -ErrorAction Stop
        [System.Windows.Forms.MessageBox]::Show(
            $Message,
            $Title,
            [System.Windows.Forms.MessageBoxButtons]::OK,
            [System.Windows.Forms.MessageBoxIcon]::Warning
        ) | Out-Null
    } catch {
        # Non-interactive session (Server Core, SSH, etc.) - banner is sufficient
    }
}

# --- Main ---

$accountType = Get-AccountType

if ($accountType -eq "Domain") {
    $expiryDate = Get-ADPasswordExpiry -Username $CheckUsername
}
else {
    $expiryDate = Get-LocalPasswordExpiry -Username $CheckUsername
}

$displayUser = if ($User -ne "") { $User } else { "$env:USERDOMAIN\$env:USERNAME" }

if ($expiryDate -eq "Never") {
    if (-not $Quiet) {
        Write-Host "Account Type:     $accountType"
        Write-Host "User:             $displayUser"
        Write-Host "Password is set to never expire."
    }
    if ($Test) {
        Show-MessageBoxNotification -Title "Password Expiry Test" -Message "Account: $displayUser ($accountType) - password never expires. (-Test mode)"
    }
    exit 0
}

if (-not $expiryDate) {
    if (-not $Quiet) {
        Write-Host "Account Type:     $accountType"
        Write-Host "User:             $displayUser"
        Write-Host "Could not determine password expiry date."
    }
    exit 1
}

$daysRemaining = ($expiryDate - (Get-Date)).Days

# Quiet mode - exit silently if not near expiry
if ($Quiet -and $daysRemaining -gt $WarningDays -and -not $Test) {
    exit 0
}

Write-Host "Account Type:     $accountType"
Write-Host "User:             $displayUser"
Write-Host "Password Expires: $($expiryDate.ToString('yyyy-MM-dd HH:mm'))"
Write-Host "Days Remaining:   $daysRemaining"

if ($daysRemaining -le 0) {
    $title = "PASSWORD EXPIRED"
    $message = "Your password has EXPIRED as of $($expiryDate.ToString('yyyy-MM-dd')).`nPlease change it immediately (Ctrl+Alt+Del > Change Password)."
    Show-MessageBoxNotification -Title $title -Message $message
}
elseif ($daysRemaining -le $WarningDays) {
    $title = "Password Expiring Soon"
    $message = "Your password will expire in $daysRemaining day(s) on $($expiryDate.ToString('yyyy-MM-dd')).`nPlease change it soon (Ctrl+Alt+Del > Change Password)."
    Show-MessageBoxNotification -Title $title -Message $message
}
elseif ($Test) {
    $title = "Password Expiry Test"
    $message = "Your password will expire in $daysRemaining day(s) on $($expiryDate.ToString('yyyy-MM-dd')). (-Test mode)"
    Show-MessageBoxNotification -Title $title -Message $message
}
else {
    Write-Host "Password expiry is more than $WarningDays days away. No notification needed."
}
