#!/bin/bash
# Windows 门店（后端跑在 WSL 里）：经 WSL interop 调 Windows 的 PowerShell，查看 / 升级桌面收银 Aaden Desktop。
# 云端设备命令 WINDOWS_PROBE（probe）和 DESKTOP_UPDATE（update）用，输出整段回传云端。
#
#   bash windowsDesktop.sh probe    只读：装在哪、什么版本、谁在用、多久没人操作、有没有已下载待装的更新
#   bash windowsDesktop.sh update   下载最新安装包 → 收银电脑 10 分钟没人操作才动手 → 关掉收银程序、静默安装、重新打开
#
# 起因（POS-129 / POS-148）：1.2.160 以前的桌面端只在正常退出时才装更新，收银机全屏、没人正常退出，
# 75 台 Windows 停在旧版本。装上 1.2.160 以后它会自己更新，这里只需要推一次。
#
# WSL 是门店 Windows 登录用户的启动项（run_hidden.vbs → start-aaden-server.bat）拉起来的，
# 所以经 interop 起的 Windows 进程以这个登录用户、在他的桌面会话里运行，桌面端也装在他的 %LOCALAPPDATA%。
#
# 退出码：0 成功（探测成功 / 装好了 / 已是最新）；3 不是 WSL；4 调不起 Windows 程序；
#        22 收银电脑有人在用，没动；其余是升级失败（见输出）
MODE=${1:-probe}
MIN_IDLE_SECONDS=600
PS=/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe

if [ "$MODE" != probe ] && [ "$MODE" != update ]; then
  echo "用法: windowsDesktop.sh probe|update"
  exit 2
fi

if ! grep -qi microsoft /proc/version 2>/dev/null; then
  echo "不是 WSL 设备（$(uname -r)），这条命令只对 Windows 门店"
  exit 3
fi

echo "WSL: $(uname -r)  用户: $(id -un)"
echo "binfmt WSLInterop: $(head -1 /proc/sys/fs/binfmt_misc/WSLInterop 2>/dev/null || echo 没有)"
echo "WSL_INTEROP: ${WSL_INTEROP:-（未设置，cron 下正常）}"
echo "/run/WSL: $(ls /run/WSL 2>/dev/null | wc -l) 个，最新 5 个："
ls -lt /run/WSL/ 2>/dev/null | sed -n '2,6p' | sed 's/^/  /'

if [ ! -e "$PS" ]; then
  echo "找不到 $PS（C 盘没挂到 /mnt/c？）"
  ls -ld /mnt/c 2>&1
  exit 4
fi

PS_PREFIX=()
try_ps() { timeout 20 "${PS_PREFIX[@]}" "$PS" -NoProfile -NonInteractive -Command 'exit 0' </dev/null >/dev/null 2>&1; }
# WSL 很久没重启的机器 /run/WSL 下会堆几百个旧 socket（1632 逐个试了 45 分钟），只试最新的 5 个
recent_sockets() { ls -t /run/WSL/*_interop 2>/dev/null | head -5; }

# cron 和命令执行进程里没有 WSL_INTEROP，直接调 .exe 可能起不来：依次试 /run/WSL 下的 interop socket，再试 sudo
setup_interop() {
  try_ps && { echo "interop: 直接可用"; return 0; }
  local s
  for s in $(recent_sockets); do
    PS_PREFIX=(env WSL_INTEROP="$s")
    try_ps && { echo "interop: 用 $s"; return 0; }
  done
  if [ -n "$AADEN_PW" ] && echo "$AADEN_PW" | sudo -S -p '' -v 2>/dev/null; then
    for s in $(recent_sockets); do
      PS_PREFIX=(sudo -n env WSL_INTEROP="$s")
      try_ps && { echo "interop: sudo + $s"; return 0; }
    done
  fi
  PS_PREFIX=()
  return 1
}

if ! setup_interop; then
  echo "调不起 Windows 的 PowerShell（interop 没开，或者 socket 不可用），没法从这里管桌面端"
  exit 4
fi

# 跑一段 PowerShell：用 -EncodedCommand 传，免得引号转义；Windows 输出的 \r 去掉
run_ps() {
  local enc
  enc=$(printf '%s' "$1" | iconv -f UTF-8 -t UTF-16LE | base64 -w0)
  timeout "${2:-120}" "${PS_PREFIX[@]}" "$PS" -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand "$enc" </dev/null 2>&1 | tr -d '\r'
  return "${PIPESTATUS[0]}"
}

# 两种模式共用：路径、版本、桌面会话、空闲时间
read -r -d '' PS_COMMON <<'EOF'
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
$exe = Join-Path $env:LOCALAPPDATA 'Programs\aaden\Aaden Desktop.exe'
$mySession = [Diagnostics.Process]::GetCurrentProcess().SessionId
function Get-InstalledVersion {
  if (-not (Test-Path $exe)) { return $null }
  $v = (Get-Item $exe).VersionInfo.ProductVersion
  if (-not $v) { return $null }
  return (($v -split '[^0-9.]')[0].Split('.') | Select-Object -First 3) -join '.'
}
function Get-DesktopProcesses { @(Get-CimInstance Win32_Process -Filter "Name='Aaden Desktop.exe'") }
function Get-IdleSeconds {
  if (-not ('AadenIdle' -as [type])) {
    Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
public static class AadenIdle {
  [StructLayout(LayoutKind.Sequential)] struct LastInput { public uint cbSize; public uint dwTime; }
  [DllImport("user32.dll")] static extern bool GetLastInputInfo(ref LastInput info);
  public static long Seconds() {
    var info = new LastInput(); info.cbSize = (uint)Marshal.SizeOf(info);
    if (!GetLastInputInfo(ref info)) return -1;
    return (long)(((uint)Environment.TickCount - info.dwTime) / 1000);
  }
}
'@
  }
  [AadenIdle]::Seconds()
}
# 最新版本：先读 S3（POS-36，发版 workflow 上传，和 GitHub Release 同一份 latest.yml）；
# S3 读不到才退回 GitHub Releases（仓库改私有前的退路）。S3 的 latest.yml 带 sha512，下载后校验
function Get-LatestDesktop {
  [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
  $base = 'https://aaden-backend.s3.eu-central-1.amazonaws.com/desktop/win/'
  try {
    $res = Invoke-WebRequest ($base + 'latest.yml') -UseBasicParsing -TimeoutSec 30
    $yml = [Text.Encoding]::UTF8.GetString($res.RawContentStream.ToArray())
    $version = [regex]::Match($yml, '(?m)^version:\s*(\S+)').Groups[1].Value
    $name = [regex]::Match($yml, '(?m)^path:\s*(\S+)').Groups[1].Value
    $sha512 = [regex]::Match($yml, '(?m)^sha512:\s*(\S+)').Groups[1].Value
    $size = [regex]::Match($yml, '(?m)^\s+size:\s*(\d+)').Groups[1].Value
    if ($version -and $name -and $sha512) {
      return [pscustomobject]@{ Source = 's3'; Version = $version; Name = $name; Url = $base + $name; Sha512 = $sha512; Size = [long]$size }
    }
  } catch { }
  $r = Invoke-RestMethod 'https://api.github.com/repos/innerken-gmbh/aaden-vue-app/releases/latest' -UseBasicParsing -TimeoutSec 30
  $asset = $r.assets | Where-Object { $_.name -like '*.exe' } | Select-Object -First 1
  if (-not $asset) { throw "release $($r.tag_name) has no .exe" }
  [pscustomobject]@{ Source = 'github'; Version = $r.tag_name.TrimStart('v'); Name = $asset.name; Url = $asset.browser_download_url; Sha512 = $null; Size = [long]$asset.size }
}
# Smart App Control：0 关、1 开（只放行签过名 / 有信誉的程序，我们的安装包没签名会被拦）、2 评估中
function Get-SmartAppControlState {
  try { (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\CI\Policy' -Name VerifiedAndReputablePolicyState -ErrorAction Stop).VerifiedAndReputablePolicyState } catch { $null }
}
function Get-Sha512Base64($path) {
  $hex = (Get-FileHash $path -Algorithm SHA512).Hash
  $bytes = [byte[]](0..($hex.Length / 2 - 1) | ForEach-Object { [Convert]::ToByte($hex.Substring($_ * 2, 2), 16) })
  [Convert]::ToBase64String($bytes)
}
EOF

read -r -d '' PS_PROBE <<'EOF'
$ErrorActionPreference = 'Continue'
"windows_user=$env:USERDOMAIN\$env:USERNAME session=$mySession"
try { $os = Get-CimInstance Win32_OperatingSystem; "os=$($os.Caption) $($os.Version) boot=$($os.LastBootUpTime)" } catch { "os=? $_" }
"exe=$exe exists=$(Test-Path $exe) version=$(Get-InstalledVersion)"
$machineWide = Join-Path $env:ProgramFiles 'Aaden Desktop\Aaden Desktop.exe'
"program_files_install=$(Test-Path $machineWide)"
$procs = Get-DesktopProcesses
"desktop_processes=$($procs.Count)"
$procs | Group-Object SessionId, ExecutablePath | ForEach-Object { "  session/path=$($_.Name) count=$($_.Count)" }
$main = $procs | Where-Object { $procs.ProcessId -notcontains $_.ParentProcessId } | Select-Object -First 1
if ($main) {
  try { $o = Invoke-CimMethod -InputObject $main -MethodName GetOwner; "  main pid=$($main.ProcessId) owner=$($o.Domain)\$($o.User) started=$($main.CreationDate)" } catch { "  owner=? $_" }
}
try { "idle_seconds=$(Get-IdleSeconds)" } catch { "idle_seconds=? $_" }
$pending = Join-Path $env:LOCALAPPDATA 'aaden-updater\pending'
if (Test-Path $pending) { "updater_pending=$((Get-ChildItem $pending | ForEach-Object { $_.Name }) -join ',')" } else { 'updater_pending=none' }
$startup = [Environment]::GetFolderPath('Startup')
"startup_run_hidden=$(Test-Path (Join-Path $startup 'run_hidden.vbs'))"
"smart_app_control=$(Get-SmartAppControlState)"
try { $l = Get-LatestDesktop; "latest=$($l.Version) source=$($l.Source)" } catch { "latest=? $_" }
EOF

read -r -d '' PS_UPDATE <<'EOF'
$current = Get-InstalledVersion
"installed=$current"
if (-not $current) { "ABORT: Aaden Desktop is not installed at $exe"; exit 20 }
$other = @(Get-DesktopProcesses | Where-Object { $_.SessionId -ne $mySession })
if ($other.Count) { "ABORT: Aaden Desktop is running in another session ($(($other | ForEach-Object { $_.SessionId } | Sort-Object -Unique) -join ',')), ours is $mySession"; exit 21 }
$idle = Get-IdleSeconds
"idle_seconds=$idle"
if ($idle -lt __MIN_IDLE__) { "BUSY: last keyboard/mouse input $idle s ago (< __MIN_IDLE__ s), not touching it"; exit 22 }
$l = Get-LatestDesktop
$latest = $l.Version
"latest=$latest source=$($l.Source)"
if ($current -eq $latest) { 'UP_TO_DATE'; exit 0 }
if ((Get-SmartAppControlState) -eq 1) { 'BLOCKED: Smart App Control is on, unsigned installer would be blocked; not touching it'; exit 26 }
$file = Join-Path $env:TEMP $l.Name
Invoke-WebRequest $l.Url -OutFile $file -UseBasicParsing -TimeoutSec 900
$size = (Get-Item $file).Length
"downloaded $($l.Name) $size bytes"
if ($l.Size -and $size -ne $l.Size) { "ABORT: size mismatch, expected $($l.Size)"; exit 23 }
if ($l.Sha512) {
  if ((Get-Sha512Base64 $file) -ne $l.Sha512) { 'ABORT: sha512 mismatch, not installing'; exit 23 }
  'sha512 ok'
}
# 下载可能要几分钟，动手前再确认一次没人在用
$idle = Get-IdleSeconds
if ($idle -lt __MIN_IDLE__) { "BUSY after download: last input $idle s ago, not touching it"; exit 22 }
Stop-Process -Name 'Aaden Desktop' -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
# 关掉收银程序以后，不管安装出什么错（1955：安装包被应用控制策略拦截），都要把收银程序开回来；经 explorer 打开，不挂在这个进程下面
try {
  $installer = Start-Process $file -ArgumentList '/S' -PassThru
  $null = $installer.Handle
  $finished = $installer.WaitForExit(300000)
} catch {
  "BLOCKED: installer could not start: $($_.Exception.Message)"
  Start-Process explorer.exe -ArgumentList ('"' + $exe + '"')
  exit 26
}
$after = Get-InstalledVersion
Start-Process explorer.exe -ArgumentList ('"' + $exe + '"')
if (-not $finished) { "FAILED: installer still running after 5 minutes"; exit 24 }
"installer_exit=$($installer.ExitCode) installed_after=$after"
if ($after -ne $latest) { "FAILED: version is $after, expected $latest"; exit 25 }
"INSTALLED $current -> $after"
exit 0
EOF

# 升级后过一会儿再看：收银程序没起来就再开一次
read -r -d '' PS_CHECK_RUNNING <<'EOF'
$n = (Get-DesktopProcesses).Count
"desktop_processes=$n installed=$(Get-InstalledVersion)"
if (-not $n) { Start-Process explorer.exe -ArgumentList ('"' + $exe + '"'); 'relaunched'; exit 1 }
EOF

case "$MODE" in
  probe)
    echo "--- Windows 侧"
    run_ps "$PS_COMMON"$'\n'"$PS_PROBE" 180
    exit $?
    ;;
  update)
    echo "--- 升级（收银电脑 ${MIN_IDLE_SECONDS} 秒没人操作才动手）"
    run_ps "$PS_COMMON"$'\n'"${PS_UPDATE//__MIN_IDLE__/$MIN_IDLE_SECONDS}" 1500
    code=$?
    # 不管升级成没成功（包括 PowerShell 中途出错），最后都确认收银程序在运行，没运行就打开
    sleep 20
    echo "--- 20 秒后确认收银程序在运行"
    run_ps "$PS_COMMON"$'\n'"$PS_CHECK_RUNNING" 120
    check=$?
    [ "$code" -ne 0 ] && exit "$code"
    exit "$check"
    ;;
esac
