# 本地预览服务器 (零依赖 TcpListener + Runspace 池并发): 服务在线站点静态目录
# 用法: powershell -NoProfile -ExecutionPolicy Bypass -File tools\serve-web.ps1 [-Root release\web_2026.08] [-Port 8080]
# 并发: 每个请求一个 Runspace (池上限 16), 大文件 (wasm/图片) 不再阻塞小文件 (step 下载/页面)
param(
  [string]$Root = (Join-Path $PSScriptRoot '..\release\web_2026.08'),
  [int]$Port = 8080,
  [string]$ViewerRoot = (Join-Path $PSScriptRoot '..\viewer')   # /viewer/ 前缀 → 新迪查看器本地副本 (可选; 原项目 Z:\...\上海新迪3D\dev 只读)
)
if (-not (Test-Path $Root)) { Write-Output "目录不存在: $Root"; exit 1 }
$viewerRootFull = ''
if ($ViewerRoot -and (Test-Path $ViewerRoot)) {
  $viewerRootFull = [System.IO.Path]::GetFullPath($ViewerRoot)
  Write-Output ("查看器目录: " + $viewerRootFull)
}

$mime = @{
  '.html' = 'text/html; charset=utf-8'; '.js' = 'application/javascript; charset=utf-8'
  '.css' = 'text/css; charset=utf-8';   '.json' = 'application/json; charset=utf-8'
  '.png' = 'image/png'; '.jpg' = 'image/jpeg'; '.pdf' = 'application/pdf'
  '.step' = 'application/octet-stream'; '.ico' = 'image/x-icon'; '.txt' = 'text/plain; charset=utf-8'
  '.wasm' = 'application/wasm'; '.stp' = 'application/octet-stream'
}
$rootFull = [System.IO.Path]::GetFullPath($Root)

# 单请求处理逻辑 (自包含, 在 Runspace 中执行)
$handlerText = @'
param($client, $rootFull, $viewerRootFull, $mime)
try {
  $stream = $client.GetStream()
  $stream.ReadTimeout = 2000        # 预连接/空连接快速失败, 不阻塞后续请求
  $stream.WriteTimeout = 30000
  $reader = New-Object System.IO.StreamReader($stream, [System.Text.Encoding]::ASCII, $false, 4096, $true)
  $reqLine = $reader.ReadLine()
  if ($null -eq $reqLine) { $client.Close(); return }   # 客户端连上不发数据, 直接断开
  # 读掉请求头 (EOF 即 null 时必须退出, 否则死循环)
  $line = $reader.ReadLine()
  $guard = 0
  while ($line -ne $null -and $line -ne '') {
    $line = $reader.ReadLine()
    $guard++
    if ($guard -gt 100) { break }
  }

  $path = '/'
  if ($reqLine) {
    $parts = $reqLine -split ' '
    if ($parts.Count -ge 2) { $path = $parts[1] }
  }
  # 去掉查询串 (?v= / ?file= 等), 否则会进文件路径导致非法字符异常
  $qi = $path.IndexOf('?')
  if ($qi -ge 0) { $path = $path.Substring(0, $qi) }
  if ($path -eq '/') { $path = '/index.html' }

  $full = [System.IO.Path]::GetFullPath((Join-Path $rootFull ($path -replace '/', '\').TrimStart('\')))
  $bytes = $null; $status = '200 OK'; $ctype = 'application/octet-stream'
  # /viewer/ 前缀 → 新迪 3D 查看器根目录 (各自防目录穿越)
  $rootNow = $rootFull
  $rel = ($path -replace '/', '\').TrimStart('\')
  if ($viewerRootFull -and $rel.StartsWith('viewer\', [System.StringComparison]::OrdinalIgnoreCase)) {
    $rootNow = $viewerRootFull
    $rel = $rel.Substring('viewer\'.Length)
    $full = [System.IO.Path]::GetFullPath((Join-Path $rootNow $rel))
  }
  if ($full.StartsWith($rootNow, [System.StringComparison]::OrdinalIgnoreCase) -and (Test-Path $full -PathType Leaf)) {
    $bytes = [System.IO.File]::ReadAllBytes($full)
    $ext = [System.IO.Path]::GetExtension($full).ToLowerInvariant()
    if ($mime.ContainsKey($ext)) { $ctype = $mime[$ext] }
  } else {
    $status = '404 Not Found'
    $bytes = [System.Text.Encoding]::UTF8.GetBytes('404 Not Found')
    $ctype = 'text/plain; charset=utf-8'
  }
  $head = "HTTP/1.1 $status`r`nContent-Type: $ctype`r`nCache-Control: no-cache`r`nContent-Length: $($bytes.Length)`r`nConnection: close`r`n`r`n"
  $headBytes = [System.Text.Encoding]::ASCII.GetBytes($head)
  $stream.Write($headBytes, 0, $headBytes.Length)
  $stream.Write($bytes, 0, $bytes.Length)
  $stream.Flush()
} catch { }
$client.Close()
'@

# Runspace 池 (并发上限 16)
$pool = [runspacefactory]::CreateRunspacePool(1, 16)
$pool.Open()

$listener = New-Object System.Net.Sockets.TcpListener([System.Net.IPAddress]::Loopback, $Port)
$listener.Start()
Write-Output ("服务已启动: http://localhost:" + $Port + "  (停止: 关闭本窗口)")

while ($true) {
  $client = $null
  try {
    $client = $listener.AcceptTcpClient()
  } catch {
    # 监听异常不退出进程 (端口占用等), 2 秒后重试
    Start-Sleep -Seconds 2
    continue
  }
  try {
    $ps = [powershell]::Create()
    $ps.RunspacePool = $pool
    [void]$ps.AddScript($handlerText)
    [void]$ps.AddArgument($client)
    [void]$ps.AddArgument($rootFull)
    [void]$ps.AddArgument($viewerRootFull)
    [void]$ps.AddArgument($mime)
    [void]$ps.BeginInvoke()
  } catch {
    $client.Close()
  }
}
