# install-gateway.ps1 —— 医院前置机数据网关(hd-gateway)一键装机(Windows 服务)。
# 目标系统:Windows 10 / 11 / Server 2019+,x64。对齐 Linux 版 install-gateway.sh。
#
# 用法(管理员 PowerShell,与 hd-gateway.exe 同目录):
#   右键「以管理员身份运行 PowerShell」→  Set-ExecutionPolicy -Scope Process Bypass -Force
#                                     →  .\install-gateway.ps1
#                                     →  .\install-gateway.ps1 -InstallDir D:\hd-gateway   # 换安装磁盘
#   - 交互式逐项问(带默认值,回车即取默认);任一项可用环境变量预填跳过提问。
#   - 幂等:重装保留已有 gateway.json(不覆盖运维改过的配置),只换 exe + 重装服务。
#   - 装哪个盘:-InstallDir(或环境变量 GW_DIR);已装过则默认沿用原位置,不会搬回 C 盘。
#
# 环境变量(预填则不问该项,mirror .sh):
#   GW_ID / VENDOR / ADVERTISE / ADMIN_PASS / ADAPTER / REQUIRE_ACT / VENDOR_TOKEN / AUTH_CODE / LISTEN / GW_DIR
#   ADMIN_PASS 默认 Qwe2026!(与 Setup.exe / 网关内置缺省同口径;交付后请在控制台改密)
# 云中台配置(地址/签名凭据/机构编码)装机不填:运维平台「接入医院」按医院配置,网关激活授权码后
# 心跳自动领取(v0.4.47 托管下发);配置到位前数据只入队不转发,到位即自动补报,零丢失。
param([string]$InstallDir)
$ErrorActionPreference = 'Stop'

# ---- 常量(对齐 .sh 的 /opt/hd-gateway,Windows 默认落在 ProgramData)----
$SvcName       = 'hd-gateway'
$DefaultVendor = 'http://110.40.205.181:8090'
$Src           = Split-Path -Parent $MyInvocation.MyCommand.Path

function Die($msg) { Write-Host "✗ $msg" -ForegroundColor Red; exit 1 }

# 已装过就返回它实际装在哪 —— 已注册服务的 ImagePath 是三条装机线(Setup.exe / 本脚本 /
# 手工 install)共用的唯一事实源。漏了这步,装在 D 盘的机器再跑一次本脚本会被当首装,
# 又在 C 盘铺一份,老那份还占着 9090 端口。
function Get-InstalledPrefix {
  try {
    $img = (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Services\$SvcName" -ErrorAction Stop).ImagePath
    if ($img) {
      $exe = $null
      if ($img -match '^\s*"([^"]+)"') { $exe = $Matches[1] } elseif ($img -match '^\s*(\S+)') { $exe = $Matches[1] }
      if ($exe) { return (Split-Path -Parent $exe) }
    }
  } catch {}
  return $null
}

# ---- 安装位置:-InstallDir > 环境变量 GW_DIR > 已装服务的实际位置 > C:\ProgramData\hd-gateway ----
# 前置机 C 盘常年吃紧,而转发队列在断网/云端拒收期间会一路涨,必须能挪到数据盘。
$Prefix = $InstallDir
if (-not $Prefix) { $Prefix = $env:GW_DIR }
if (-not $Prefix) { $Prefix = Get-InstalledPrefix }
if (-not $Prefix) { $Prefix = Join-Path $env:ProgramData 'hd-gateway' }
$Prefix = $Prefix.Trim().TrimEnd('\')
# 服务开机自启时网络盘/UNC 还没连上,装那儿必起不来;顺带挡住相对路径。
if ($Prefix -notmatch '^[A-Za-z]:\\') { Die "安装目录必须是带盘符的本机绝对路径(不支持网络盘/UNC),当前: $Prefix" }
$Data          = Join-Path $Prefix 'data'
$Conf          = Join-Path $Prefix 'gateway.json'
$Bin           = Join-Path $Prefix 'hd-gateway.exe'

# ---- 前置检查:管理员 + 架构 ----
$admin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
         ).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
if (-not $admin) { Die '请以管理员身份运行 PowerShell 再执行本脚本' }
if ([Environment]::Is64BitOperatingSystem -eq $false) { Die '本包是 x64 二进制,当前系统非 64 位' }

# ---- 定位源 exe(同目录 hd-gateway-windows-amd64.exe 或 hd-gateway.exe)----
$SrcBin = $null
foreach ($c in @('hd-gateway-windows-amd64.exe', 'bin\hd-gateway-windows-amd64.exe', 'hd-gateway.exe')) {
  $p = Join-Path $Src $c
  if (Test-Path $p) { $SrcBin = $p; break }
}
if (-not $SrcBin) { Die '找不到网关 exe(hd-gateway-windows-amd64.exe),请与本脚本放同目录' }

# ---- 取值助手:环境变量已设(即便空)则直接用不问;否则交互带默认(回车取默认)----
function Ask($prompt, $default, $envName) {
  if (Test-Path "env:$envName") { return (Get-Item "env:$envName").Value }
  $ans = Read-Host "  $prompt [$default]"
  if ([string]::IsNullOrEmpty($ans)) { return $default }
  return $ans
}
# 地址格式校验:填错(典型是粘贴两遍 http://ip:9090http//ip:9090)当场重问,
# 别把畸形值写进 gateway.json —— 它会在装完后以读不懂的 DNS 错的形式现身。
function Ask-Url($prompt, $default, $envName) {
  while ($true) {
    $v = (Ask $prompt $default $envName).Trim()
    if ([string]::IsNullOrEmpty($v)) { return $v }
    $bad = $null
    if ($v -match '\s')                          { $bad = '里面有空格' }
    elseif ($v -notmatch '^https?://')           { $bad = '必须以 http:// 或 https:// 开头' }
    else {
      $hostport = ($v -replace '^https?://','') -split '/' | Select-Object -First 1
      if ($hostport -match 'http')               { $bad = '像是把地址粘贴了两遍,请只保留一段' }
      elseif ([string]::IsNullOrEmpty($hostport)) { $bad = '缺少主机名' }
    }
    if (-not $bad) { return $v }
    Write-Host "  ✗ 地址有误($bad): $v" -ForegroundColor Red
    if (Test-Path "env:$envName") { Die "环境变量 $envName 的地址格式有误: $v" }
  }
}
function Detect-IP {
  try {
    $cfg = Get-NetIPConfiguration -ErrorAction Stop |
      Where-Object { $_.IPv4DefaultGateway -and $_.NetAdapter.Status -eq 'Up' } | Select-Object -First 1
    if ($cfg -and $cfg.IPv4Address) { return $cfg.IPv4Address.IPAddress }
  } catch {}
  try {
    $ip = (Get-NetIPAddress -AddressFamily IPv4 -ErrorAction Stop |
      Where-Object { $_.IPAddress -notmatch '^(127\.|169\.254\.)' } | Select-Object -First 1).IPAddress
    if ($ip) { return $ip }
  } catch {}
  return '127.0.0.1'
}
# 控制台初始密码:固化成与 Setup.exe(hd-gateway.iss)、网关内置缺省(config.go defAdminPass)、
# Linux 装机脚本同一个值。以前这里随机生成,三条装机线三个口径,现场按「Qwe2026!」登不进去。
$DefAdminPass = 'Qwe2026!'

Write-Host '== 医院前置机网关一键装机(Windows) =='
Write-Host "  安装位置: $Prefix  (程序/配置/日志/转发队列都在这;换盘: -InstallDir D:\hd-gateway)"
$keepConf = $false
if (Test-Path $Conf) {
  Write-Host "  检测到已有配置 $Conf —— 保留不覆盖(只换 exe + 重装服务)"
  $keepConf = $true
} else {
  Write-Host '  首次安装,请填写配置(回车取默认;也可用环境变量预填):'
  # 规范默认:gw- 前缀 + 计算机名清洗(小写、去非字母数字、截断);清洗后为空则 gw-<6位随机>。
  $gwClean = ($env:COMPUTERNAME -replace '[^A-Za-z0-9]','').ToLower()
  if ($gwClean.Length -gt 24) { $gwClean = $gwClean.Substring(0,24) }
  $gwDefault = if ($gwClean) { "gw-$gwClean" } else { "gw-$(-join ((48..57)+(97..122) | Get-Random -Count 6 | ForEach-Object {[char]$_}))" }
  Write-Host '  ⚠ 网关 id 须与授权码签发端约定的一致,否则激活报「不是本网关的授权码」;建议用 gw-<医院拼音>NN。'
  $gwId      = Ask '网关 id(gateway_id)'                $gwDefault                     'GW_ID'
  $vendor    = Ask-Url '运维服务器(vendor_server,对时源;厂商平台别填本机)' $DefaultVendor               'VENDOR'
  $advertise = Ask-Url '本机对盒子可达地址(advertise_addr,已自动探测)'    ("http://$(Detect-IP):9090")  'ADVERTISE'
  $adminPass = Ask "控制台密码(admin_pass,回车取默认 $DefAdminPass)" $DefAdminPass       'ADMIN_PASS'
  $authCode  = Ask '授权码(auth_code,可选;填了装完即认证云端可见,留空则装完在控制台配额页贴)' '' 'AUTH_CODE'
  $adapter   = if (Test-Path env:ADAPTER)      { $env:ADAPTER }      else { 'hospital_v2' }
  $reqAct    = if (Test-Path env:REQUIRE_ACT)  { $env:REQUIRE_ACT }  else { 'true' }
  $vToken    = if (Test-Path env:VENDOR_TOKEN) { $env:VENDOR_TOKEN } else { '' }
  $listen    = if (Test-Path env:LISTEN)       { $env:LISTEN }       else { ':9090' }
}

# ---- 1) 目录 ----
New-Item -ItemType Directory -Force -Path $Data | Out-Null

# ---- 2) 停并移除旧服务(才能覆盖正在占用的 exe)----
$existing = Get-Service -Name $SvcName -ErrorAction SilentlyContinue
if ($existing) {
  Write-Host '  停止并移除旧服务…'
  try { Stop-Service $SvcName -Force -ErrorAction SilentlyContinue } catch {}
  & sc.exe delete $SvcName | Out-Null
  Start-Sleep -Seconds 2
}

# ---- 3) exe(覆盖)----
Copy-Item -Path $SrcBin -Destination $Bin -Force
$ver = (& $Bin -V 2>&1) -join ' '
Write-Host "  ✓ 二进制 -> $Bin ($(if ($ver) { $ver } else { '版本未知' }))"

# ---- 4) 配置(仅首装生成;admin_pass 明文靠 NTFS 权限保护)----
if (-not $keepConf) {
  $json = @"
{
  "listen": "$listen",
  "queue_dir": "$($Data.Replace('\','\\'))\\queue",
  "mapping_file": "$($Data.Replace('\','\\'))\\mapping.json",
  "hospital": { "adapter": "$adapter" },
  "vendor_server": "$vendor",
  "vendor_token": "$vToken",
  "advertise_addr": "$advertise",
  "gateway_id": "$gwId",
  "admin_pass": "$adminPass",
  "auth_code": "$authCode",
  "require_activation": $reqAct
}
"@
  # 不能用 Set-Content -Encoding UTF8:Windows PowerShell 5.1 的 UTF8 恒带 BOM,Go 解析配置会炸。
  [IO.File]::WriteAllText($Conf, $json, (New-Object System.Text.UTF8Encoding $false))
  Write-Host "  ✓ 配置 -> $Conf"
}
# 收紧配置文件权限:去继承,仅 SYSTEM + Administrators 可读(admin_pass 明文保护)。
try {
  & icacls $Conf /inheritance:r /grant:r 'SYSTEM:F' 'Administrators:F' | Out-Null
} catch { Write-Host "  ! 配置文件 ACL 设置失败(不阻断): $_" }

# ---- 5) 安装服务(exe 自注册,自动重启策略在 exe 内设好)----
& $Bin install -c $Conf
if ($LASTEXITCODE -ne 0) { Die '服务安装失败(见上)' }

# ---- 6) 系统时钟兜底:开 Windows Time(vendor_server 够不到时靠它)----
try {
  Set-Service -Name w32time -StartupType Automatic -ErrorAction SilentlyContinue
  Start-Service w32time -ErrorAction SilentlyContinue
  & w32tm /resync /force 2>$null | Out-Null
  Write-Host '  ✓ 已启用系统时间同步(Windows Time)作时钟兜底'
} catch { Write-Host '  ! 系统时间同步未能自动开启,请手动确认 w32tm' }

# ---- 6b) 防火墙:放行 TCP 9090(盒子连网关)+ UDP 9099(局域网发现应答)----
# 漏 UDP 9099 盒子永远发现不了前置机(2026-07-24 首装现场实锤);先删同名规则避免重复。
& netsh advfirewall firewall delete rule name='hd-gateway 9090' 2>$null | Out-Null
& netsh advfirewall firewall add rule name='hd-gateway 9090' dir=in action=allow protocol=TCP localport=9090 | Out-Null
& netsh advfirewall firewall delete rule name='hd-gateway discover 9099' 2>$null | Out-Null
& netsh advfirewall firewall add rule name='hd-gateway discover 9099' dir=in action=allow protocol=UDP localport=9099 | Out-Null
Write-Host '  防火墙已放行: TCP 9090 / UDP 9099'

# ---- 7) 自检 ----
Start-Sleep -Seconds 2
Write-Host '== 自检 =='
$svc = Get-Service -Name $SvcName -ErrorAction SilentlyContinue
Write-Host "  服务状态: $(if ($svc) { $svc.Status } else { '未找到' })"
$lst = 9090
if ($listen -match '(\d+)\s*$') { $lst = $Matches[1] }
elseif ($keepConf -and (Test-Path $Conf)) {
  $c = Get-Content $Conf -Raw | ConvertFrom-Json
  if ($c.listen -match '(\d+)\s*$') { $lst = $Matches[1] }
}
$status = $null
try {
  $status = (Invoke-WebRequest -UseBasicParsing -TimeoutSec 5 "http://127.0.0.1:$lst/status").Content
} catch {}
if ($status -and $status -match '"received"') {
  Write-Host '  ✓ /status 可访问(网关已就绪)'
  if ($status -match '"synced":true') { Write-Host '  ✓ 已对上运维服务器时钟(对时生效)' }
  else { Write-Host '  · 时钟尚未对上(等下一轮心跳 30s;vendor_server 不可达则靠系统时间兜底)' }
} else {
  Write-Host '  ! 暂未取到 /status(还在启动或防火墙拦截);稍后开控制台确认'
}

$ip = Detect-IP
Write-Host ''
Write-Host '== 完成 =='
Write-Host "  控制台:  http://$ip`:$lst/     密码见下(admin_pass)"
if (-not $keepConf) {
  Write-Host "  admin 密码: $adminPass   (也在 $Conf,已收紧 NTFS 权限)"
  if ($adminPass -eq $DefAdminPass) {
    Write-Host '  ⚠ 这是固化初始密码,交付后请在控制台「改密」;忘了密码可在本机重置:' -ForegroundColor Yellow
    Write-Host "      & '$Bin' -c '$Conf' -reset-pass '新密码' ; Restart-Service $SvcName"
  }
}
Write-Host "  日志:    事件查看器 / 或前台调试: `"$Bin`" -c `"$Conf`""
Write-Host "  改映射:  控制台页面操作;整表重载重启服务: Restart-Service $SvcName"
Write-Host "  停/卸:   Stop-Service $SvcName  /  .\uninstall-gateway.ps1"
Write-Host '  下一步:开控制台看「本机钟 vs 运维服务器」绿灯 + 盒子上电后待激活页逐台激活。'
