# Setup-CameraShare.ps1 v2 (2026-08-19) — one-shot standalone camera-to-PC shared folder
# setup + end-to-end verification (NW400 / NW500 pattern).
# Double-click 2-Setup-CameraShare-Preview.cmd first (changes nothing, shows the plan), then
# 3-Setup-CameraShare-Apply.cmd. Safe to re-run: every step keeps what already exists.
#
# v2 changes: NW400 support, adopts EITHER addressing orientation (PC .1/camera .2 like
# Baton Rouge, or PC .2/camera .1 like the NW400 guide and Saint Lawrence), folds in the
# old Align-CameraLink stray-IP cleanup, default camera user is now 'unified' (team
# convention), and the apply run finishes with a real end-to-end test: it connects to the
# share AS the camera user and writes a file through the network path — the same trip the
# camera makes — so a green run means the camera-side "verify" should pass too.
#
# What it does, in order:
#   1. Asks which camera (NW500/NW400) — that names the share and the inbox subfolder.
#   2. Finds the wired port the camera is plugged into (the one with no gateway), keeps
#      the fixed IP it already has (or sets 10.1.1.2, the go-forward standard: PC = .2,
#      camera = .1), removes stray extra addresses, and marks the link Private
#      (Windows blocks file sharing on Public networks).
#   3. Creates a local sign-in for the camera (cameras cannot use guest access on modern
#      Windows). An existing user is kept untouched so an already-configured camera keeps
#      working — answer the user prompt with whatever name that camera already uses.
#   4. Creates/reuses the shared folder under C:\unified_imaging\inbox\<camera> and
#      grants the camera user write permission.
#   5. Opens the File and Printer Sharing firewall rules and makes sure the Server
#      (file sharing) service is running and set to start automatically.
#   6. Windows 11 24H2 requires SMB signing by default; many camera firmwares cannot
#      sign, which shows up as "cannot connect" with everything else correct. The apply
#      run turns the requirement off (standalone kiosk trade-off, deliberate).
#   7. Runs the end-to-end share test, pings the camera, and prints the exact settings
#      to type into the camera plus a PASS/FAIL summary.

param(
    [switch]$DryRun,
    [switch]$Elevated,
    [string]$CameraModel = '',      # 'nw500' or 'nw400'; blank = ask
    [string]$InterfaceAlias = '',   # blank = auto-detect the gatewayless wired port
    [string]$ServerIp = '',         # blank = keep the port's existing fixed IP, else 10.1.1.2
    [string]$CameraIp = '',         # blank = the counterpart of the PC address (.1 <-> .2)
    [string]$ShareName = '',        # blank = camera model
    [string]$SharePath = '',        # blank = C:\unified_imaging\inbox\<camera model>
    [string]$CameraUser = '',       # blank = ask (Enter = 'unified')
    [string]$CameraPassword = ''    # blank = ask when the user must be created
)

$ErrorActionPreference = 'Stop'
$script:failures = 0

function Step([string]$label, [scriptblock]$work) {
    try { & $work; Write-Host ("  OK   " + $label) }
    catch { $script:failures++; Write-Host ("  FAIL " + $label + " -> " + $_.Exception.Message) }
}
function Ask([string]$prompt, [string]$default) {
    try {
        $v = Read-Host ($prompt + ' [' + $default + ']')
        if ($null -ne $v -and $v.Trim() -ne '') { return $v.Trim() }
    } catch { }
    return $default
}
function Pause-IfInteractive([string]$msg) {
    try { Read-Host $msg | Out-Null } catch { }
}

$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
if (-not $DryRun -and -not $isAdmin) {
    # Relaunch elevated; the new window pauses at the end on its own.
    $args = '-NoProfile -ExecutionPolicy Bypass -File "' + $MyInvocation.MyCommand.Path + '" -Elevated'
    foreach ($p in 'CameraModel', 'InterfaceAlias', 'ServerIp', 'CameraIp', 'ShareName', 'SharePath', 'CameraUser', 'CameraPassword') {
        $v = Get-Variable -Name $p -ValueOnly
        if ($v -ne '') { $args += ' -' + $p + ' "' + $v + '"' }
    }
    Start-Process powershell -Verb RunAs -ArgumentList $args
    exit
}

Write-Host ''
if ($DryRun) { Write-Host '=== PREVIEW ONLY - nothing will be changed ===' }
else { Write-Host '=== Camera share setup (changes will be applied) ===' }
Write-Host ''

# --- 1. Which camera?
if ($CameraModel -eq '') {
    $pick = Ask 'Which camera? 1 = NW500, 2 = NW400' '1'
    if ($pick -eq '2' -or $pick -like 'nw400') { $CameraModel = 'nw400' } else { $CameraModel = 'nw500' }
}
$CameraModel = $CameraModel.ToLower()
if ($ShareName -eq '') { $ShareName = $CameraModel }
if ($SharePath -eq '') { $SharePath = 'C:\unified_imaging\inbox\' + $CameraModel }
Write-Host ("Camera model: " + $CameraModel.ToUpper() + "  ->  share '" + $ShareName + "' at " + $SharePath)

# --- 2. Find the camera port: a physical wired adapter that is Up with no default gateway.
#        (The internet connection has a gateway; the direct camera cable never does.)
$adapter = $null
if ($InterfaceAlias -ne '') {
    $adapter = Get-NetAdapter -Name $InterfaceAlias -ErrorAction SilentlyContinue
    if ($null -eq $adapter) {
        Write-Host ("No adapter named '" + $InterfaceAlias + "' on this machine - stopping, nothing changed.")
        Get-NetAdapter | ForEach-Object { Write-Host ("  - " + $_.Name + "  (" + $_.Status + ")") }
        Pause-IfInteractive 'Press Enter to close'; exit 1
    }
} else {
    $wired = @(Get-NetAdapter -Physical | Where-Object { $_.Status -eq 'Up' -and $_.PhysicalMediaType -like '*802.3*' })
    $candidates = @()
    foreach ($a in $wired) {
        $conf = Get-NetIPConfiguration -InterfaceIndex $a.ifIndex -ErrorAction SilentlyContinue
        if ($null -eq $conf -or $null -eq $conf.IPv4DefaultGateway) { $candidates += $a }
    }
    if ($candidates.Count -eq 1) {
        $adapter = $candidates[0]
        Write-Host ("Camera port found: '" + $adapter.Name + "' (wired, up, no gateway = not the internet port)")
    } elseif ($candidates.Count -gt 1) {
        Write-Host 'More than one wired port has no gateway. Which one is the camera cable?'
        for ($i = 0; $i -lt $candidates.Count; $i++) {
            Write-Host ("  " + ($i + 1) + " = " + $candidates[$i].Name + "  (" + $candidates[$i].InterfaceDescription + ")")
        }
        $pick = Ask 'Type the number' '1'
        $idx = 0
        if (-not [int]::TryParse($pick, [ref]$idx) -or $idx -lt 1 -or $idx -gt $candidates.Count) { $idx = 1 }
        $adapter = $candidates[$idx - 1]
        Write-Host ("Using '" + $adapter.Name + "'")
    } else {
        Write-Host 'No wired camera port found (is the camera on and the cable plugged in?).'
        Write-Host 'Ports seen up right now:'
        Get-NetAdapter | Where-Object Status -eq 'Up' | ForEach-Object { Write-Host ("  - " + $_.Name + "  (" + $_.InterfaceDescription + ")") }
        Pause-IfInteractive 'Press Enter to close'; exit 1
    }
}

# --- 3. Addressing: honor what the port already has; only add ours when there is none.
#        Go-forward standard when the port is blank: PC = 10.1.1.2, camera = 10.1.1.1
#        (matches the NW400 guide). Existing installs the other way around keep working.
$manual = @(Get-NetIPAddress -InterfaceIndex $adapter.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue |
    Where-Object { $_.PrefixOrigin -eq 'Manual' })
$needIp = $false
if ($ServerIp -eq '') {
    if ($manual.Count -eq 1) {
        $ServerIp = $manual[0].IPAddress
        Write-Host ("Port already has a fixed address (" + $ServerIp + ") - keeping it.")
    } elseif ($manual.Count -gt 1) {
        Write-Host 'This port has more than one fixed address (leftover experiments). Which is the real one?'
        for ($i = 0; $i -lt $manual.Count; $i++) { Write-Host ("  " + ($i + 1) + " = " + $manual[$i].IPAddress) }
        $pick = Ask 'Type the number' '1'
        $idx = 0
        if (-not [int]::TryParse($pick, [ref]$idx) -or $idx -lt 1 -or $idx -gt $manual.Count) { $idx = 1 }
        $ServerIp = $manual[$idx - 1].IPAddress
    } else {
        $ServerIp = '10.1.1.2'
        $needIp = $true
    }
} elseif (-not ($manual | Where-Object { $_.IPAddress -eq $ServerIp })) {
    $needIp = $true
}

if ($needIp) {
    if ($DryRun) { Write-Host ("Would set fixed address " + $ServerIp + "/24 on '" + $adapter.Name + "'") }
    else {
        Step ("fixed address " + $ServerIp + " on '" + $adapter.Name + "'") {
            New-NetIPAddress -InterfaceIndex $adapter.ifIndex -IPAddress $ServerIp -PrefixLength 24 | Out-Null
        }
    }
}

# Remove stray extra addresses on the camera port (the old Align-CameraLink job).
foreach ($ip in $manual) {
    if ($ip.IPAddress -eq $ServerIp) { continue }
    if ($DryRun) { Write-Host ("Would remove stray address " + $ip.IPAddress + " from '" + $adapter.Name + "'") }
    else {
        Step ("removed stray address " + $ip.IPAddress) {
            Remove-NetIPAddress -InterfaceIndex $adapter.ifIndex -IPAddress $ip.IPAddress -Confirm:$false
        }
    }
}

# The camera is the counterpart address on the same subnet: .1 <-> .2
if ($CameraIp -eq '') {
    if ($ServerIp -match '\.1$') { $CameraIp = ($ServerIp -replace '\.\d+$', '.2') }
    else { $CameraIp = ($ServerIp -replace '\.\d+$', '.1') }
}
Write-Host ("Addressing: PC = " + $ServerIp + "  <->  camera = " + $CameraIp)

if ($DryRun) { Write-Host ("Would mark '" + $adapter.Name + "' as a Private network (sharing is blocked on Public)") }
else {
    Step ("network marked Private on '" + $adapter.Name + "'") {
        Set-NetConnectionProfile -InterfaceIndex $adapter.ifIndex -NetworkCategory Private
    }
}

# --- 4. Camera sign-in (kept untouched if it already exists — the camera may already know it).
if ($CameraUser -eq '') { $CameraUser = Ask 'Sign-in user the camera will use' 'unified' }
$existingUser = Get-LocalUser -Name $CameraUser -ErrorAction SilentlyContinue
$passwordKnown = $false
if ($existingUser) {
    Write-Host ("User '" + $CameraUser + "' already exists - keeping it (password unchanged).")
    if ($CameraPassword -ne '') { $passwordKnown = $true }
    else {
        $CameraPassword = Ask 'Its password (needed only for the connection test; Enter = skip test)' ''
        if ($CameraPassword -ne '') { $passwordKnown = $true }
    }
} else {
    if ($CameraPassword -eq '') { $CameraPassword = Ask ('Password for new user ' + $CameraUser) 'unified' }
    $passwordKnown = $true
    if ($DryRun) { Write-Host ("Would create local user '" + $CameraUser + "' (password never expires)") }
    else {
        Step ("local user '" + $CameraUser + "'") {
            $sec = ConvertTo-SecureString $CameraPassword -AsPlainText -Force
            New-LocalUser -Name $CameraUser -Password $sec -PasswordNeverExpires -AccountNeverExpires -Description 'Camera shared-folder account' | Out-Null
        }
    }
}

# --- 5. Shared folder (reuse an existing share of the same name, wherever it points).
$existingShare = Get-SmbShare -Name $ShareName -ErrorAction SilentlyContinue
if ($existingShare) {
    $SharePath = $existingShare.Path
    Write-Host ("Share '" + $ShareName + "' already exists at " + $SharePath + " - reusing it.")
} elseif ($DryRun) {
    Write-Host ("Would create folder " + $SharePath + " and share it as '" + $ShareName + "'")
} else {
    Step ("folder + share '" + $ShareName + "' at " + $SharePath) {
        New-Item -ItemType Directory -Force -Path $SharePath | Out-Null
        New-SmbShare -Name $ShareName -Path $SharePath -FullAccess 'Everyone' | Out-Null
    }
}
if (-not $DryRun) {
    Step ("write permission for '" + $CameraUser + "' on " + $SharePath) {
        icacls $SharePath /grant ($CameraUser + ':(OI)(CI)M') | Out-Null
        if ($LASTEXITCODE -ne 0) { throw 'icacls failed' }
    }
} else { Write-Host ("Would grant '" + $CameraUser + "' write permission on " + $SharePath) }

# --- 6. Firewall + the file sharing service itself.
if ($DryRun) { Write-Host 'Would enable the File and Printer Sharing firewall rules' }
else {
    Step 'File and Printer Sharing allowed through the firewall' {
        netsh advfirewall firewall set rule group="File and Printer Sharing" new enable=Yes | Out-Null
        if ($LASTEXITCODE -ne 0) { throw 'netsh failed' }
    }
}
if ($DryRun) { Write-Host 'Would make sure the Server (file sharing) service is running and automatic' }
else {
    Step 'Server (file sharing) service running and set to automatic' {
        Set-Service -Name LanmanServer -StartupType Automatic
        Start-Service -Name LanmanServer
    }
}

# --- 7. SMB signing requirement (Windows 11 24H2 default breaks many camera firmwares).
$smb = $null
try { $smb = Get-SmbServerConfiguration } catch { }
if ($null -eq $smb) {
    Write-Host 'Could not read SMB settings without admin rights - the apply run will handle it.'
} elseif (-not $smb.RequireSecuritySignature) {
    Write-Host 'SMB signing already not required - nothing to change.'
} elseif ($DryRun) {
    Write-Host 'Would turn OFF the SMB signing requirement (24H2 default; cameras cannot sign)'
} else {
    Step 'SMB signing requirement turned off (camera compatibility)' {
        Set-SmbServerConfiguration -RequireSecuritySignature $false -Force
    }
}

# --- 8. End-to-end test: the same trip the camera makes — connect to \\PC\share AS the
#        camera user, write a file through the network path, see it land in the folder.
$unc = '\\' + $ServerIp + '\' + $ShareName
if ($DryRun) {
    Write-Host ("Would test-write a file to " + $unc + " as '" + $CameraUser + "'")
} elseif (-not $passwordKnown) {
    Write-Host ("SKIP end-to-end share test (password for '" + $CameraUser + "' not entered). The camera-side verify is the real test anyway.")
} else {
    Step ("end-to-end share test: write a file to " + $unc + " as '" + $CameraUser + "'") {
        # net use runs via cmd so its chatter on stderr cannot trip the script's error handling.
        cmd /c ('net use "' + $unc + '" /delete >nul 2>&1') | Out-Null
        $out = cmd /c ('net use "' + $unc + '" "' + $CameraPassword + '" /user:' + $env:COMPUTERNAME + '\' + $CameraUser + ' 2>&1')
        if ($LASTEXITCODE -ne 0) { throw ('could not connect to the share: ' + ($out | Out-String).Trim()) }
        try {
            $probe = 'share-test-' + (Get-Date -Format 'yyyyMMdd-HHmmss') + '.txt'
            Set-Content -Path (Join-Path $unc $probe) -Value 'camera share test - safe to delete'
            if (-not (Test-Path (Join-Path $SharePath $probe))) { throw 'file written over the network did not appear in the local folder' }
            Remove-Item (Join-Path $unc $probe) -Force
        } finally {
            cmd /c ('net use "' + $unc + '" /delete >nul 2>&1') | Out-Null
        }
    }
}

# --- 9. Can we see the camera?
Write-Host ''
Write-Host ('Looking for the camera at ' + $CameraIp + ' ...')
$seen = Test-Connection -ComputerName $CameraIp -Count 2 -Quiet -ErrorAction SilentlyContinue
if ($seen) {
    Write-Host ('  OK   camera answers at ' + $CameraIp + ' - the cable and addressing are GOOD.')
} else {
    $arpSaysYes = $false
    try { $arpSaysYes = [bool]((arp -a) | Select-String -SimpleMatch $CameraIp) } catch { }
    if ($arpSaysYes) {
        Write-Host ('  OK   camera does not answer pings but IS on the wire at ' + $CameraIp + ' (arp table) - good enough.')
    } else {
        Write-Host ('  --   no answer from ' + $CameraIp + ' yet. Not fatal: some cameras only wake their network when')
        Write-Host ('       sending or testing. If the camera-side verify also fails, re-check the camera IP with site staff.')
    }
}

# --- 10. The card to type into the camera.
$passDisplay = $CameraPassword
if (-not $passwordKnown -or $passDisplay -eq '') { $passDisplay = '(unchanged - whatever was set before)' }
Write-Host ''
Write-Host '======================================================'
Write-Host (' Type these into the ' + $CameraModel.ToUpper() + ' (standalone / send-to menu).')
Write-Host ' NOTE: the folder fields stay LOCKED until "Save'
Write-Host ' Shared Folder" is switched ON in that menu.'
Write-Host ''
Write-Host ("   Camera IP address : " + $CameraIp + "   (subnet 255.255.255.0, gateway blank; leave as-is if already set)")
Write-Host ("   Server / PC       : " + $ServerIp + "   (use the IP, not the computer name)")
Write-Host ("   Shared folder     : " + $ShareName)
Write-Host ("   Username          : " + $CameraUser)
Write-Host ("   Password          : " + $passDisplay)
Write-Host ("   Full path         : \\" + $ServerIp + "\" + $ShareName)
Write-Host '======================================================'
Write-Host ' Then run the connection test ON THE CAMERA, and make'
Write-Host ' sure ImageAnywhere''s inbox points at'
Write-Host ' C:\unified_imaging\inbox (1-Setup-Inbox.cmd shows how).'
Write-Host ''
if (-not $DryRun) {
    if ($script:failures -eq 0) { Write-Host 'RESULT: all steps green.' }
    else { Write-Host ('RESULT: ' + $script:failures + ' step(s) FAILED - scroll up for the FAIL lines before trusting this setup.') }
    Write-Host ''
    Pause-IfInteractive 'Press Enter to close'
}
