# ============================================================================
# Entra Connect Gate G1 - AD Hygiene / Pre-sync Cleanup
# ----------------------------------------------------------------------------
# Runs on CS-SERVER via GuruRMM. Idempotent. Each section reports what it
# would do (dry-run) OR what it did (execute). Pre-state backups always run.
#
# USAGE:
#   Default invocation is DRY-RUN (no writes). Run with $doExecute=$true to
#   apply changes.
#
# Sections:
#   0. Pre-state backup          (always runs, no changes)
#   1. OU=Excluded-From-Sync     (create; move 4 role accounts)
#   2. proxyAddresses populate   (34 users - live data from M365 Graph)
#   3. SG-* security groups      (create 16 groups in OU=Groups)
#   4. DisplayName cosmetics     (3 fixes: Crystal, howard, Cathy)
#   5. Summary                   (counts, rollback path)
#
# Maps to risk register: docs/migration/entra-connect-risk-register-2026-04-22.md
# ============================================================================

# Toggle at top of script so it's obvious in GuruRMM command preview
$doExecute = $false  # CHANGE TO $true TO APPLY

$ErrorActionPreference = 'Continue'
Import-Module ActiveDirectory -ErrorAction Stop

$ts = Get-Date -Format 'yyyy-MM-dd-HHmmss'
$backupDir = "D:\Backups\g1-hygiene-$ts"
$mode = if ($doExecute) { 'EXECUTE' } else { 'DRY-RUN (no changes)' }

function Section($n) {
    Write-Output ''
    Write-Output ('=' * 76)
    Write-Output "== $n"
    Write-Output ('=' * 76)
}
function Log($level, $msg) {
    $prefix = switch ($level) {
        'WOULD' { '[WOULD]' }
        'DID'   { '[DID]  ' }
        'OK'    { '[OK]   ' }
        'SKIP'  { '[SKIP] ' }
        'WARN'  { '[WARN] ' }
        'FAIL'  { '[FAIL] ' }
        default { '       ' }
    }
    Write-Output "$prefix $msg"
}

Write-Output "G1 AD Hygiene - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss zzz')"
Write-Output "Host: $env:COMPUTERNAME"
Write-Output "Mode: $mode"
Write-Output "Backup dir: $backupDir"

# Counters
$script:stats = @{ created=0; moved=0; updated=0; skipped=0; errors=0 }

# ----------------------------------------------------------------------------
Section '0. Pre-state backup (always runs)'
# ----------------------------------------------------------------------------
try {
    if (-not (Test-Path $backupDir)) {
        New-Item -Path $backupDir -ItemType Directory -Force | Out-Null
    }

    # Full user dump with relevant attributes for diff-later
    Get-ADUser -Filter * -Properties SamAccountName,UserPrincipalName,DisplayName,GivenName,Surname,`
        Mail,proxyAddresses,Enabled,PasswordLastSet,PasswordNeverExpires,DistinguishedName,whenCreated,whenChanged |
        Select-Object SamAccountName,UserPrincipalName,DisplayName,GivenName,Surname,Mail,
            @{Name='ProxyAddresses';Expression={($_.proxyAddresses -join ';')}},
            Enabled,PasswordLastSet,PasswordNeverExpires,DistinguishedName |
        Export-Csv "$backupDir\users-pre.csv" -NoTypeInformation -Encoding UTF8
    Log 'OK' "Exported users-pre.csv"

    Get-ADGroup -Filter * -Properties Description,GroupScope,GroupCategory,Members |
        Select-Object SamAccountName,Name,GroupScope,GroupCategory,Description,
            @{Name='MemberCount';Expression={$_.Members.Count}},
            DistinguishedName |
        Export-Csv "$backupDir\groups-pre.csv" -NoTypeInformation -Encoding UTF8
    Log 'OK' "Exported groups-pre.csv"

    Get-ADOrganizationalUnit -Filter * |
        Select-Object Name,DistinguishedName |
        Export-Csv "$backupDir\ous-pre.csv" -NoTypeInformation -Encoding UTF8
    Log 'OK' "Exported ous-pre.csv"

    Write-Output ""
    Log 'OK' "Pre-state saved at $backupDir"
    Write-Output "Rollback commands (if needed after execute):"
    Write-Output "   - proxyAddresses: Set-ADUser from users-pre.csv column ProxyAddresses"
    Write-Output "   - OU moves: Move-ADObject back to old DistinguishedName"
    Write-Output "   - Groups created today: Remove-ADGroup (safe since memberless)"
} catch {
    Log 'FAIL' "Pre-state backup failed: $_"
    $script:stats.errors++
}

# ----------------------------------------------------------------------------
Section '1. OU=Excluded-From-Sync  +  move 4 role accounts'
# ----------------------------------------------------------------------------
$domainDN = (Get-ADDomain).DistinguishedName
$excludedOU = "OU=Excluded-From-Sync,$domainDN"

# Create OU
$existing = $null
try { $existing = Get-ADOrganizationalUnit -Identity $excludedOU -ErrorAction Stop } catch {}

if ($existing) {
    Log 'SKIP' "OU=Excluded-From-Sync already exists"
} else {
    if ($doExecute) {
        try {
            New-ADOrganizationalUnit -Name 'Excluded-From-Sync' -Path $domainDN `
                -Description 'Accounts scoped OUT of Entra Connect sync. Do not move staff users here.' `
                -ProtectedFromAccidentalDeletion $true
            Log 'DID' "Created OU=Excluded-From-Sync"
            $script:stats.created++
        } catch {
            Log 'FAIL' "New-ADOrganizationalUnit failed: $_"
            $script:stats.errors++
        }
    } else {
        Log 'WOULD' "Create OU=Excluded-From-Sync (ProtectedFromAccidentalDeletion=true)"
        $script:stats.created++
    }
}

# Move role accounts
$roleAccounts = @('Culinary','Receptionist','saleshare','directoryshare')
foreach ($sam in $roleAccounts) {
    try {
        $u = Get-ADUser -Identity $sam -Properties DistinguishedName -ErrorAction Stop
        $currentOU = ($u.DistinguishedName -split ',', 2)[1]
        if ($currentOU -eq $excludedOU) {
            Log 'SKIP' "$sam already in Excluded-From-Sync"
            continue
        }
        if ($doExecute) {
            Move-ADObject -Identity $u.DistinguishedName -TargetPath $excludedOU
            Log 'DID' "Moved ${sam}: $currentOU -> $excludedOU"
            $script:stats.moved++
        } else {
            Log 'WOULD' "Move $sam from $currentOU to $excludedOU"
            $script:stats.moved++
        }
    } catch {
        Log 'WARN' "Cannot resolve $sam - $_"
    }
}

# ----------------------------------------------------------------------------
Section '2. Populate proxyAddresses (34 users - live data from M365 Graph 2026-04-22)'
# ----------------------------------------------------------------------------
# Mapping derived from live Graph API pull. Primary SMTP uses uppercase 'SMTP:'
# prefix (Exchange convention for primary). Aliases use lowercase 'smtp:'.
$proxyMap = @{
    # 24 users with existing M365 mailboxes
    'Allison.Reibschied'  = @('Allison.Reibschied@cascadestucson.com')
    'Alyssa.Brooks'       = @('alyssa.brooks@cascadestucson.com')
    'Ashley.Jensen'       = @('ashley.jensen@cascadestucson.com','ashley.jenson@cascadestucson.com')
    'britney.thompson'    = @('Britney.Thompson@cascadestucson.com')
    'Christina.DuPras'    = @('christina.dupras@cascadestucson.com')
    'Christine.Nyanzunda' = @('christine.nyanzunda@cascadestucson.com')
    'Crystal.Rodriguez'   = @('crystal.rodriguez@cascadestucson.com','crystal.suszek@cascadestucson.com')
    'howard'              = @('dax.howard@cascadestucson.com','cara.lespron@cascadestucson.com')
    'JD.Martin'           = @('jd.martin@cascadestucson.com')
    'John.Trozzi'         = @('john.trozzi@cascadestucson.com')
    'karen.rossini'       = @('karen.rossini@cascadestucson.com')
    'lauren.hasselman'    = @('lauren.hasselman@cascadestucson.com')
    'Lois.Lane'           = @('lois.lane@cascadestucson.com')
    'Lupe.Sanchez'        = @('lupe.sanchez@cascadestucson.com')
    'Matt.Brooks'         = @('matthew.brooks@cascadestucson.com')   # soft-match bridge (M365 UPN is matthew.brooks)
    'Megan.Hiatt'         = @('megan.hiatt@cascadestucson.com')
    'Meredith.Kuhn'       = @('meredith.kuhn@cascadestucson.com')
    'Ramon.Castaneda'     = @('ramon.castaneda@cascadestucson.com','ramon.castanada@cascadestucson.com','ramon.casteneda@cascadestucson.com')
    'Sharon.Edwards'      = @('sharon.edwards@cascadestucson.com')
    'Shelby.Trozzi'       = @('Shelby.Trozzi@cascadestucson.com')
    'Susan.Hicks'         = @('susan.hicks@cascadestucson.com')
    'sysadmin'            = @('sysadmin@cascadestucson.com')
    'Tamra.Matthews'      = @('tamra.matthews@cascadestucson.com','tamra.johnson@cascadestucson.com')
    'Veronica.Feller'     = @('veronica.feller@cascadestucson.com')
    # 10 AD-only users (no M365 mailbox yet; reserve canonical SMTP for when they get licensed)
    'Cathy.Kingston'      = @('cathy.kingston@cascadestucson.com')
    'Christopher.Holick'  = @('christopher.holick@cascadestucson.com')
    'Julian.Crim'         = @('julian.crim@cascadestucson.com')
    'Kyla.QuickTiffany'   = @('kyla.quicktiffany@cascadestucson.com')
    'Michelle.Shestko'    = @('michelle.shestko@cascadestucson.com')
    'Ray.Rai'             = @('ray.rai@cascadestucson.com')
    'Richard.Adams'       = @('richard.adams@cascadestucson.com')
    'Sebastian.Leon'      = @('sebastian.leon@cascadestucson.com')
    'Sheldon.Gardfrey'    = @('sheldon.gardfrey@cascadestucson.com')
    'Shontiel.Nunn'       = @('shontiel.nunn@cascadestucson.com')
}

foreach ($sam in ($proxyMap.Keys | Sort-Object)) {
    try {
        $u = Get-ADUser -Identity $sam -Properties proxyAddresses,mail -ErrorAction Stop
    } catch {
        Log 'WARN' "AD user '$sam' not found - $($_.Exception.Message.Split([char]10)[0])"
        continue
    }

    # Build the target proxyAddresses value: SMTP:<primary> + smtp:<aliases>
    $primary = $proxyMap[$sam][0]
    $aliases = @($proxyMap[$sam] | Select-Object -Skip 1)
    $targetProxies = @("SMTP:$primary") + ($aliases | ForEach-Object { "smtp:$_" })
    $targetMail = $primary

    $currentProxies = @($u.proxyAddresses)
    $currentSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
    foreach ($p in $currentProxies) { [void]$currentSet.Add($p) }
    $targetSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
    foreach ($p in $targetProxies) { [void]$targetSet.Add($p) }

    if ($currentSet.SetEquals($targetSet) -and $u.mail -eq $targetMail) {
        Log 'SKIP' "$sam proxyAddresses already current"
        continue
    }

    $currentStr = if ($currentProxies.Count -eq 0) { '<empty>' } else { $currentProxies -join '; ' }
    $targetStr  = $targetProxies -join '; '

    if ($doExecute) {
        try {
            Set-ADUser -Identity $sam -Replace @{
                proxyAddresses = $targetProxies
                mail           = $targetMail
            } -ErrorAction Stop
            Log 'DID' "$sam"
            Write-Output "       before: $currentStr"
            Write-Output "       after:  $targetStr"
            Write-Output "       mail=$targetMail"
            $script:stats.updated++
        } catch {
            Log 'FAIL' "$sam Set-ADUser failed: $_"
            $script:stats.errors++
        }
    } else {
        Log 'WOULD' "$sam"
        Write-Output "       before: $currentStr"
        Write-Output "       after:  $targetStr"
        Write-Output "       mail=<empty> -> $targetMail"
        $script:stats.updated++
    }
}

# ----------------------------------------------------------------------------
Section '3. Create 16 SG-* security groups (CA / file-share / break-glass)'
# ----------------------------------------------------------------------------
$groupsOU = "OU=Groups,$domainDN"
# Verify Groups OU exists
try { Get-ADOrganizationalUnit -Identity $groupsOU -ErrorAction Stop | Out-Null }
catch {
    Log 'FAIL' "OU=Groups does not exist at $groupsOU - cannot create groups"
    $script:stats.errors++
}

$groupsToCreate = @(
    @{Name='SG-External-Signin-Allowed'; Desc='Members may sign in from outside Cascades building (CA policy target).'},
    @{Name='SG-Caregivers';               Desc='All shift-work caregivers. CA policy target for shared-phone mobile policy.'},
    @{Name='SG-FrontDesk';                Desc='Front desk receptionists sharing reception PCs.'},
    @{Name='SG-CourtesyPatrol';           Desc='Courtesy patrol staff.'},
    @{Name='SG-Drivers';                  Desc='Transportation drivers (AD accounts being disabled 2026-04-22 - group retained for history).'},
    @{Name='SG-Management-RW';            Desc='Read/write on \\CS-SERVER\Management file share (Phase 4).'},
    @{Name='SG-Sales-RW';                 Desc='Read/write on \\CS-SERVER\SalesDept file share (Phase 4).'},
    @{Name='SG-Culinary-RW';              Desc='Read/write on \\CS-SERVER\Culinary file share (Phase 4).'},
    @{Name='SG-IT-RW';                    Desc='Read/write on \\CS-SERVER\IT file share (Phase 4).'},
    @{Name='SG-Receptionist-RW';          Desc='Read/write on \\CS-SERVER\Receptionist file share (Phase 4).'},
    @{Name='SG-Directory-RW';             Desc='Read/write on \\CS-SERVER\directoryshare file share (Phase 4).'},
    @{Name='SG-Server-RW';                Desc='Read/write on \\CS-SERVER\Server share (IT admin, Phase 4).'},
    @{Name='SG-Chat-RW';                  Desc='Read/write on \\CS-SERVER\chat file share (Phase 4).'},
    @{Name='SG-Office-PHI-External';      Desc='Office PHI staff with external sign-in permission (CA policy).'},
    @{Name='SG-Office-PHI-Internal';      Desc='Office PHI staff limited to in-building sign-in (CA policy).'},
    @{Name='SG-CA-BreakGlass';            Desc='Break-glass accounts excluded from all Conditional Access policies.'}
)

foreach ($g in $groupsToCreate) {
    try {
        $existing = Get-ADGroup -Identity $g.Name -ErrorAction Stop
        Log 'SKIP' "$($g.Name) already exists"
        continue
    } catch {
        # group missing - proceed to create
    }
    if ($doExecute) {
        try {
            New-ADGroup -Name $g.Name -SamAccountName $g.Name -GroupCategory Security -GroupScope Global `
                -Path $groupsOU -Description $g.Desc
            Log 'DID' "Created $($g.Name)"
            $script:stats.created++
        } catch {
            Log 'FAIL' "Create $($g.Name) failed: $_"
            $script:stats.errors++
        }
    } else {
        Log 'WOULD' "Create $($g.Name) (Global Security) in $groupsOU"
        Write-Output "       desc: $($g.Desc)"
        $script:stats.created++
    }
}

# ----------------------------------------------------------------------------
Section '4. DisplayName cosmetic fixes (3 users)'
# ----------------------------------------------------------------------------
$displayFixes = @(
    @{SAM='Crystal.Rodriguez'; Current='Crystal  Rodriguez'; Target='Crystal Rodriguez'}
    @{SAM='howard';             Current='howard';             Target='Howard Dax'}
    @{SAM='Cathy.Kingston';     Current='Cathy.Kingston';     Target='Cathy Kingston'}
)
foreach ($d in $displayFixes) {
    try {
        $u = Get-ADUser -Identity $d.SAM -Properties DisplayName -ErrorAction Stop
        if ($u.DisplayName -eq $d.Target) {
            Log 'SKIP' "$($d.SAM) DisplayName already '$($d.Target)'"
            continue
        }
        if ($doExecute) {
            Set-ADUser -Identity $d.SAM -DisplayName $d.Target
            Log 'DID' "$($d.SAM) DisplayName: '$($u.DisplayName)' -> '$($d.Target)'"
            $script:stats.updated++
        } else {
            Log 'WOULD' "$($d.SAM) DisplayName: '$($u.DisplayName)' -> '$($d.Target)'"
            $script:stats.updated++
        }
    } catch {
        Log 'WARN' "$($d.SAM) lookup failed: $_"
    }
}

# ----------------------------------------------------------------------------
Section '5. Summary'
# ----------------------------------------------------------------------------
Write-Output "Mode:         $mode"
Write-Output "Created:      $($script:stats.created)"
Write-Output "Moved:        $($script:stats.moved)"
Write-Output "Updated:      $($script:stats.updated)"
Write-Output "Skipped:      $($script:stats.skipped)"
Write-Output "Errors:       $($script:stats.errors)"
Write-Output ""
Write-Output "Backup dir:   $backupDir"
Write-Output ""

if (-not $doExecute) {
    Write-Output 'DRY-RUN complete. To execute:'
    Write-Output '  1. Review the [WOULD] lines above'
    Write-Output '  2. Re-run this script with $doExecute = $true'
    Write-Output '  3. Compare post-state vs pre-state CSVs in the backup dir'
} else {
    Write-Output 'EXECUTE complete. Recommended next steps:'
    Write-Output '  1. Re-run in DRY-RUN to confirm 0 [WOULD] entries (idempotency check)'
    Write-Output "  2. Export users-post.csv for the audit trail (in $backupDir)"
    Write-Output '  3. Proceed to Gate G2 (M365 role-account shared-mailbox conversion)'
}

Write-Output ""
Write-Output "Completed at $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss zzz')"
