<#
    orchestrate-mount.ps1  (Phase-3.b1)

    End-to-end mount driver for a user-managed encrypted container. Run on
    the Windows endpoint (DESKTOP-I1KFRDN for first tests; called by the
    .NET agent or manually from a desktop shortcut).

    Pipeline:
      0. GET /appmanagement/workspaces/containers/<id>/mount-context/  →
         workspace policy, tpm_pcr_profile, geofence verdict, idle-lock
         resolution, brute-force status, watermark/snapshot config.
      1. If the server already reported an active brute-force lock-out,
         show bruteforce-lockout dialog and abort.
      2. Geofence pre-check (when enforcement != 'off' and verdict known):
           - 'enforce' + outside  → refuse mount, post mount_refused audit.
           - 'audit' + outside    → server already wrote an audit row in
                                    mount-context; we only log and proceed.
      3. password-prompt → capture user password.
      4. (optional) windows-hello-prompt if workspace.require_windows_hello.
      5. SecureContainer.exe mount  (with --enforce-tpm-marker when bound).
      6. Snapshot dispatch (when snapshot_interval_hours > 0 and last
         snapshot is stale): SC.exe snapshot + retention prune.
      7. Spawn watermark-overlay UI.User dialog (background, no-wait) if
         workspace.watermark_enabled.
      8. Spawn idle-watcher (SC.exe idle-watcher) with the resolved
         idle-lock minutes.
      9. POST mount audit event.

    Usage (manual):
      .\orchestrate-mount.ps1 `
          -ContainerId 29 `
          -BackendUrl 'https://win.mdmlab.ru' `
          -AuthToken '<TRMM token>'

    Defaults: ContainerPath/Label/UserName resolved from mount-context.
    Override on the command line if you need to mount under a different
    drive letter or against a relocated VHDX.
#>
[CmdletBinding()]
param(
    [Parameter(Mandatory)] [int]    $ContainerId,
    [Parameter(Mandatory)] [string] $BackendUrl,
    [Parameter(Mandatory)] [string] $AuthToken,
    [string] $ContainerPath = '',
    [string] $DriveLetter = '',
    [string] $Label = '',
    [string] $UserName = $env:USERNAME,
    [double] $LatOverride = [double]::NaN,
    [double] $LngOverride = [double]::NaN,
    [string] $UiUserExe = 'C:\Program Files (x86)\LaboratoMDM Agent\UI.User\LaboratoMDM.UI.User.exe',
    [string] $SecureContainerExe = 'C:\Program Files\LaboratoMDM\SecureContainer.exe'
)

$ErrorActionPreference = 'Stop'
$ts = (Get-Date).ToString('yyyyMMdd-HHmmss')
$logDir = Join-Path $env:ProgramData 'LaboratoMDM\logs'
New-Item -ItemType Directory -Force -Path $logDir | Out-Null
$log = Join-Path $logDir "mount-$ContainerId-$ts.log"
Start-Transcript -Path $log -Append | Out-Null

function Write-Step($msg) { Write-Host "[orchestrate-mount] $msg" }

function Get-EndpointLatLng {
    # Best-effort local geolocation. If the SecureContainer agent has a
    # cached lat/lng in C:\ProgramData\LaboratoMDM\geo.json (written by
    # the .NET geo reporter) prefer that. Otherwise use Win32_BIOS-less
    # fallback: return $null, the orchestrator falls back to whatever
    # mount-context returned from the DeviceLocation table.
    $geo = Join-Path $env:ProgramData 'LaboratoMDM\geo.json'
    if (Test-Path $geo) {
        try {
            $j = Get-Content -Raw -Path $geo | ConvertFrom-Json
            if ($null -ne $j.lat -and $null -ne $j.lng) {
                return @{ lat = [double]$j.lat; lng = [double]$j.lng; source = 'agent-geo-cache' }
            }
        } catch {}
    }
    return $null
}

function Post-AuditEvent {
    param(
        [int]    $ContainerId,
        [string] $EventType,
        [hashtable] $Payload
    )
    try {
        $body = @{
            events = @(
                @{
                    event_type = $EventType
                    payload    = $Payload
                }
            )
        } | ConvertTo-Json -Depth 8 -Compress
        $url = "$($BackendUrl.TrimEnd('/'))/appmanagement/workspaces/containers/$ContainerId/audit-events/"
        $headers = @{
            'Authorization' = "Token $AuthToken"
            'Content-Type'  = 'application/json'
        }
        Invoke-RestMethod -Uri $url -Method POST -Headers $headers -Body $body | Out-Null
        Write-Step "audit posted: $EventType"
    } catch {
        Write-Warning "audit POST failed ($EventType): $_"
    }
}

$userPassword = $null

try {
    if (-not (Test-Path $UiUserExe))         { throw "UI.User.exe not found: $UiUserExe" }
    if (-not (Test-Path $SecureContainerExe)) { throw "SC.exe not found: $SecureContainerExe" }

    # ---- 0. mount-context ---- #
    Write-Step "Step 0: GET mount-context"
    $localGeo = Get-EndpointLatLng
    $lat = $null; $lng = $null
    if (-not [double]::IsNaN($LatOverride)) { $lat = $LatOverride; $lng = $LngOverride }
    elseif ($null -ne $localGeo)            { $lat = $localGeo.lat; $lng = $localGeo.lng }

    $ctxUrl = "$($BackendUrl.TrimEnd('/'))/appmanagement/workspaces/containers/$ContainerId/mount-context/"
    if ($null -ne $lat) {
        $ctxUrl += "?lat=$lat&lng=$lng"
    }
    $ctx = Invoke-RestMethod -Uri $ctxUrl -Method GET -Headers @{
        'Authorization' = "Token $AuthToken"
    }
    Write-Step "mount-context: state=$($ctx.container.state) tpm_bound=$($ctx.container.tpm_bound) idle=$($ctx.idle_lock.minutes)m geofence_decision=$($ctx.geofence_verdict.decision)"

    if ([string]::IsNullOrWhiteSpace($ContainerPath)) { $ContainerPath = $ctx.container.vhdx_path }
    if ([string]::IsNullOrWhiteSpace($DriveLetter))   { $DriveLetter = $ctx.container.drive_letter }
    if ([string]::IsNullOrWhiteSpace($Label))         { $Label = $ctx.container.label }

    if ($ctx.container.password_owner -ne 'user') {
        throw "Container is admin-managed (password_owner=$($ctx.container.password_owner)); use the legacy mount path."
    }
    if ($ctx.container.state -eq 'mounted') {
        Write-Step "Container already mounted; nothing to do."
        return
    }

    # ---- 1. brute-force lockout pre-check ---- #
    if ($ctx.bruteforce_status.lock_active) {
        Write-Step "Step 1: server reports brute-force lockout active until $($ctx.bruteforce_status.locked_until); refusing mount."
        $lockArgs = @(
            '--dialog', 'bruteforce-lockout',
            '--container-label', $Label,
            '--locked-until', $ctx.bruteforce_status.locked_until
        )
        & $UiUserExe @lockArgs | Out-Null
        Post-AuditEvent -ContainerId $ContainerId -EventType 'mount_refused' `
            -Payload @{ reason = 'bruteforce-lockout'; locked_until = $ctx.bruteforce_status.locked_until }
        throw "Refused: brute-force lockout active."
    }

    # ---- 2. geofence pre-check ---- #
    $gv = $ctx.geofence_verdict
    if ($gv.mode -eq 'enforce' -and $gv.decision -eq 'outside') {
        Write-Step "Step 2: geofence ENFORCE + outside → refuse mount."
        Post-AuditEvent -ContainerId $ContainerId -EventType 'mount_refused' `
            -Payload @{ reason = 'outside-geofence (enforce)'; lat = $gv.lat; lng = $gv.lng; source = $gv.source }
        throw "Refused: outside the workspace geofence."
    }
    if ($gv.mode -eq 'enforce' -and $gv.decision -eq 'unknown_location') {
        Write-Step "Step 2: geofence ENFORCE but no location → refuse mount (fail-closed)."
        Post-AuditEvent -ContainerId $ContainerId -EventType 'mount_refused' `
            -Payload @{ reason = 'unknown-location (enforce)' }
        throw "Refused: workspace requires geofence but no location available."
    }
    if ($gv.mode -eq 'audit' -and $gv.decision -eq 'outside') {
        Write-Step "Step 2: geofence AUDIT + outside (server already wrote an audit row); proceeding."
    }

    # ---- 3. password prompt ---- #
    Write-Step "Step 3: password-prompt"
    $promptArgs = @(
        '--dialog', 'password-prompt',
        '--container-id', $ContainerId,
        '--label', $Label
    )
    $promptOut = & $UiUserExe @promptArgs 2>&1 | Out-String
    $promptJson = $promptOut | ConvertFrom-Json
    if (-not $promptJson.ok) { throw "User cancelled password prompt: $($promptJson.reason)" }
    $userPassword = $promptJson.data.password
    if ([string]::IsNullOrWhiteSpace($userPassword)) { throw "Password prompt returned empty password." }

    # ---- 4. Windows Hello (when required) ---- #
    if ($ctx.workspace.require_windows_hello) {
        Write-Step "Step 4: windows-hello-prompt"
        $helloArgs = @(
            '--dialog', 'windows-hello-prompt',
            '--container-label', $Label
        )
        $helloOut = & $UiUserExe @helloArgs 2>&1 | Out-String
        $helloJson = $helloOut | ConvertFrom-Json
        if (-not $helloJson.ok) {
            Post-AuditEvent -ContainerId $ContainerId -EventType 'mount_refused' `
                -Payload @{ reason = 'hello-cancelled' }
            throw "Refused: Windows Hello cancelled."
        }
        Write-Step "Hello result: verified_by_hello=$($helloJson.data.verified_by_hello), status=$($helloJson.data.verifier_status)"
        # Note: Hello result currently advisory; the password is still the
        # gating factor at the SC.exe layer. A future revision may bind
        # the Hello assertion into the DEK derivation.
    }

    # ---- 5. SC.exe mount ---- #
    # SC.exe MountCommand requires --letter. If the DB row didn't carry one
    # (fresh container), pick the first free letter G..Z that isn't already
    # used by an OS drive or by another mounted SC container.
    if (-not $DriveLetter) {
        $taken = (Get-PSDrive -PSProvider FileSystem | ForEach-Object { $_.Name.ToUpperInvariant() })
        foreach ($l in @('G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z')) {
            if ($taken -notcontains $l) { $DriveLetter = "$l`:"; break }
        }
    }
    if (-not $DriveLetter) { throw "No free drive letter available" }
    Write-Step "Step 5: SC.exe mount $ContainerPath -> $DriveLetter"
    $mountArgs = @('mount', '--json', '--path', $ContainerPath, '--password', $userPassword,
                   '--letter', $DriveLetter, '--background')
    if ($ctx.container.tpm_bound) {
        $marker = $ctx.container.tpm_pcr_profile.marker_cipher_b64
        $fp     = $ctx.container.tpm_pcr_profile.marker_fingerprint_hex
        $keyName = $ctx.container.tpm_pcr_profile.key_name
        if ($marker -and $fp -and $keyName) {
            $mountArgs += @('--enforce-tpm-marker',
                            '--tpm-key-name', $keyName,
                            '--marker-cipher-b64', $marker,
                            '--marker-fingerprint-hex', $fp)
        } else {
            Write-Warning "tpm_bound=True but PCR profile is missing fields; mounting without --enforce-tpm-marker."
        }
    }
    $scOut = & $SecureContainerExe @mountArgs 2>&1 | Out-String
    $scReply = $scOut | ConvertFrom-Json
    if (-not $scReply.ok) {
        Post-AuditEvent -ContainerId $ContainerId -EventType 'mount_failed' `
            -Payload @{ reason = $scReply.error; tpm_enforced = [bool]$ctx.container.tpm_bound }
        throw "SC.exe mount failed: $($scReply.error)"
    }
    $mountedDrive = $scReply.data.drive_letter
    Write-Step "Mount OK on drive $mountedDrive."

    Post-AuditEvent -ContainerId $ContainerId -EventType 'mount_success' `
        -Payload @{
            drive_letter = $mountedDrive
            tpm_enforced = [bool]$ctx.container.tpm_bound
            idle_minutes = $ctx.idle_lock.minutes
            idle_source  = $ctx.idle_lock.source
            geofence_decision = $gv.decision
            hello_required = [bool]$ctx.workspace.require_windows_hello
        }

    # ---- 6. Snapshot dispatch (interval-hours > 0) ---- #
    $snapEnabled = [int]$ctx.workspace.snapshot.interval_hours
    if ($snapEnabled -gt 0) {
        Write-Step "Step 6: snapshot policy is interval=${snapEnabled}h, retention=$($ctx.workspace.snapshot.retention_count)"
        $snapDir = Join-Path $env:ProgramData "LaboratoMDM\snapshots\$ContainerId"
        New-Item -ItemType Directory -Force -Path $snapDir | Out-Null
        # Simple staleness rule: if no snapshot or newest mtime is older
        # than interval hours, take one now.
        $newest = Get-ChildItem -Path $snapDir -Filter '*.lsc' -ErrorAction SilentlyContinue |
                  Sort-Object LastWriteTime -Descending | Select-Object -First 1
        $needSnap = $true
        if ($null -ne $newest) {
            $ageH = ((Get-Date) - $newest.LastWriteTime).TotalHours
            if ($ageH -lt $snapEnabled) { $needSnap = $false; Write-Step "Newest snapshot is $([math]::Round($ageH,1))h old, < ${snapEnabled}h interval; skipping." }
        }
        if ($needSnap) {
            $snapName = "snap-{0}.lsc" -f (Get-Date).ToString('yyyyMMdd-HHmmss')
            $snapPath = Join-Path $snapDir $snapName
            Write-Step "Taking snapshot → $snapPath"
            $snapArgs = @('snapshot', '--json',
                          '--path', $ContainerPath,
                          '--output', $snapPath,
                          '--password', $userPassword)
            $snapOut = & $SecureContainerExe @snapArgs 2>&1 | Out-String
            try {
                $snapReply = $snapOut | ConvertFrom-Json
                if ($snapReply.ok) {
                    Post-AuditEvent -ContainerId $ContainerId -EventType 'snapshot_completed' `
                        -Payload @{ path = $snapPath; size_bytes = (Get-Item $snapPath).Length }
                } else {
                    Write-Warning "snapshot failed: $($snapReply.error)"
                    Post-AuditEvent -ContainerId $ContainerId -EventType 'snapshot_failed' `
                        -Payload @{ reason = $snapReply.error }
                }
            } catch {
                Write-Warning "snapshot stdout was not JSON: $snapOut"
            }
            # Prune to retention_count.
            $keep = [int]$ctx.workspace.snapshot.retention_count
            if ($keep -lt 1) { $keep = 1 }
            $all = Get-ChildItem -Path $snapDir -Filter '*.lsc' | Sort-Object LastWriteTime -Descending
            if ($all.Count -gt $keep) {
                $toDelete = $all | Select-Object -Skip $keep
                foreach ($f in $toDelete) {
                    Remove-Item -Force -Path $f.FullName
                    Write-Step "pruned old snapshot: $($f.Name)"
                }
            }
        }
    }

    # ---- 7. Watermark overlay (when enabled) ---- #
    if ($ctx.workspace.watermark.enabled) {
        Write-Step "Step 7: spawn watermark overlay"
        $wmArgs = @(
            '--dialog', 'watermark-overlay',
            '--user', $UserName,
            '--label', $Label,
            '--workspace', $ctx.workspace.name
        )
        # Spawn detached. The .NET service will track its PID via the
        # well-known marker file written by UI.User on startup.
        $markerDir = Join-Path $env:ProgramData "LaboratoMDM\watermarks"
        New-Item -ItemType Directory -Force -Path $markerDir | Out-Null
        $markerFile = Join-Path $markerDir "container-$ContainerId.pid"
        $wmProc = Start-Process -FilePath $UiUserExe -ArgumentList $wmArgs -PassThru -WindowStyle Hidden
        Set-Content -Path $markerFile -Value $wmProc.Id
        Write-Step "watermark PID=$($wmProc.Id) marker=$markerFile"
    }

    # ---- 8. Idle-watcher ---- #
    $idleMin = [int]$ctx.idle_lock.minutes
    if ($idleMin -gt 0) {
        Write-Step "Step 8: spawn idle-watcher (timeout=${idleMin}m, source=$($ctx.idle_lock.source))"
        $iwArgs = @('idle-watcher', '--container-id', $ContainerId,
                    '--path', $ContainerPath,
                    '--idle-minutes', $idleMin)
        Start-Process -FilePath $SecureContainerExe -ArgumentList $iwArgs -WindowStyle Hidden | Out-Null
    }

    Write-Step "DONE."
}
catch {
    Write-Error $_
    Write-Step "FAILED: $($_.Exception.Message)"
    exit 1
}
finally {
    if ($userPassword) { Clear-Variable -Name userPassword -ErrorAction SilentlyContinue }
    Stop-Transcript | Out-Null
}
