#requires -Version 5.1
<#
CubePlot ローカルサーバ(PowerShell版 — Pythonのインストール不要)
Windows標準搭載のPowerShellだけで動く簡易HTTPサーバ。仕様はserve.py(Python版)と同等:
  ・127.0.0.1 のみで待受(このPC以外からはアクセス不可・データは外部送信されない)
  ・キャッシュ無効/nosniff ヘッダ
  ・/health は {"status":"ok"} を返す
使い方(通常は start_server.bat が自動でこれを呼ぶ):
  powershell -NoProfile -ExecutionPolicy Bypass -File serve.ps1 [-Port 8347] [-Dir 配信フォルダ] [-Open]
実装メモ: HttpListenerは非管理者でURL ACLの問題があるため、権限不要のTcpListenerで
最小限のHTTP/1.1(GET/HEAD・Connection: close)を自前で話す。
#>
param(
  [int]$Port = 0,
  [string]$Dir = "",
  [switch]$Open,
  [switch]$NoOpen
)

# 右クリック→「PowerShellで実行」(引数なし)でもブラウザを自動で開く(Python版のbat経由と
# 同じ体験にする)。開きたくない場合のみ -NoOpen を指定
if (-not $PSBoundParameters.ContainsKey('Open') -and -not $NoOpen) { $Open = $true }
if ($NoOpen) { $Open = $false }

$ErrorActionPreference = 'Stop'
try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch { }
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path

# ポート: 引数 > config.json(スクリプトと同じ場所) > 既定8347
if ($Port -eq 0) {
  $Port = 8347
  $cfgPath = Join-Path $scriptDir 'config.json'
  if (Test-Path $cfgPath) {
    try {
      $cfg = Get-Content $cfgPath -Raw -Encoding UTF8 | ConvertFrom-Json
      if ($cfg.port -ge 1024 -and $cfg.port -le 65535) { $Port = [int]$cfg.port }
      else { Write-Host "警告: config.json の port が範囲外のため 8347 を使用します" }
    } catch { Write-Host "警告: config.json を読めません。既定ポート 8347 を使用します" }
  }
}

# 配信フォルダ: 引数 > スクリプトの隣に index.html があればそこ > ../dist
if ($Dir -eq "") {
  if ((Test-Path (Join-Path $scriptDir 'index.html')) -or (Test-Path (Join-Path $scriptDir 'CubePlot.html'))) { $Dir = $scriptDir }
  else { $Dir = Join-Path $scriptDir '..' | Join-Path -ChildPath 'dist' }
}
$root = (Resolve-Path $Dir).Path.TrimEnd([System.IO.Path]::DirectorySeparatorChar)
# 既定ドキュメント: 配布物はCubePlot.html、開発ビルドはindex.html
$defaultDoc = $null
foreach ($cand in @('CubePlot.html', 'index.html', 'CubePlot_free.html', 'cubeplot_free.html')) {
  if (Test-Path (Join-Path $root $cand)) { $defaultDoc = $cand; break }
}
if ($null -eq $defaultDoc) {
  Write-Host "エラー: $root に CubePlot.html(または index.html)がありません。"
  exit 1
}

$mime = @{
  '.html' = 'text/html; charset=utf-8'; '.htm' = 'text/html; charset=utf-8'
  '.csv'  = 'text/csv; charset=utf-8';  '.txt' = 'text/plain; charset=utf-8'
  '.js'   = 'text/javascript; charset=utf-8'; '.css' = 'text/css; charset=utf-8'
  '.json' = 'application/json; charset=utf-8'; '.svg' = 'image/svg+xml'
  '.png'  = 'image/png'; '.jpg' = 'image/jpeg'; '.jpeg' = 'image/jpeg'; '.gif' = 'image/gif'
  '.ico'  = 'image/x-icon'; '.pdf' = 'application/pdf'; '.mp4' = 'video/mp4'; '.webm' = 'video/webm'
}

$listener = $null
try {
  $listener = New-Object System.Net.Sockets.TcpListener([System.Net.IPAddress]::Loopback, $Port)
  $listener.Start()
} catch {
  Write-Host "エラー: ポート $Port は使用中か、起動できません。別の番号をお試しください:"
  Write-Host "    powershell -ExecutionPolicy Bypass -File serve.ps1 -Port $($Port + 1)"
  exit 1
}

$url = "http://127.0.0.1:$Port/"
Write-Host "+---------------------------------------------+"
Write-Host "|  CubePlot ローカルサーバ (PowerShell版)     |"
Write-Host "+---------------------------------------------+"
Write-Host "  URL:        $url"
Write-Host "  配信元:     $root"
Write-Host "  埋め込み用: ${url}?minimal=1"
Write-Host "  ※ このPC以外からはアクセスできません(127.0.0.1のみ)"
Write-Host "  停止: Ctrl+C (またはこの窓を閉じる)"
Write-Host ""

if ($Open) { Start-Process $url }

function Send-Bytes([System.IO.Stream]$stream, [int]$code, [string]$status, [string]$ctype, [byte[]]$body, [bool]$headOnly) {
  $headers = "HTTP/1.1 $code $status`r`n" +
    "Content-Type: $ctype`r`n" +
    "Content-Length: $($body.Length)`r`n" +
    "Cache-Control: no-store`r`n" +
    "X-Content-Type-Options: nosniff`r`n" +
    "Referrer-Policy: no-referrer`r`n" +
    "Connection: close`r`n`r`n"
  $hb = [System.Text.Encoding]::ASCII.GetBytes($headers)
  $stream.Write($hb, 0, $hb.Length)
  if (-not $headOnly -and $body.Length -gt 0) { $stream.Write($body, 0, $body.Length) }
  $stream.Flush()
}

$sep = [System.IO.Path]::DirectorySeparatorChar

while ($true) {
  # Ctrl+Cを効かせるため、接続が来るまで短いスリープでポーリングする
  # (AcceptTcpClient()でブロックするとPowerShell 5.1はCtrl+Cを受け付けない)
  while (-not $listener.Pending()) { Start-Sleep -Milliseconds 150 }
  $client = $listener.AcceptTcpClient()
  try {
    $client.ReceiveTimeout = 5000
    $stream = $client.GetStream()
    $reader = New-Object System.IO.StreamReader($stream, [System.Text.Encoding]::ASCII, $false, 1024, $true)
    $requestLine = $reader.ReadLine()
    if ([string]::IsNullOrEmpty($requestLine)) { continue }
    while ($true) {  # 残りのヘッダを読み捨てる(GETなのでボディはない)
      $line = $reader.ReadLine()
      if ($null -eq $line -or $line -eq '') { break }
    }
    $parts = $requestLine -split ' '
    if ($parts.Count -lt 2) { continue }
    $method = $parts[0]
    $rawPath = ($parts[1] -split '\?')[0]

    if ($method -ne 'GET' -and $method -ne 'HEAD') {
      Send-Bytes $stream 405 'Method Not Allowed' 'text/plain; charset=utf-8' ([System.Text.Encoding]::UTF8.GetBytes('405 Method Not Allowed')) $false
      continue
    }
    $isHead = ($method -eq 'HEAD')

    if ($rawPath -eq '/health') {
      Send-Bytes $stream 200 'OK' 'application/json; charset=utf-8' ([System.Text.Encoding]::UTF8.GetBytes('{"status":"ok","app":"CubePlot"}')) $isHead
      Write-Host "  200 /health"
      continue
    }

    $path = [System.Uri]::UnescapeDataString($rawPath)
    if ($path -eq '/' -or $path -eq '') { $path = '/' + $defaultDoc }
    # パストラバーサル防止: 正規化した実パスが配信フォルダ配下であることを確認
    $rel = $path.TrimStart('/') -replace '/', $sep
    $full = $null
    try { $full = [System.IO.Path]::GetFullPath((Join-Path $root $rel)) } catch { $full = $null }
    if ($null -eq $full -or -not ($full -eq $root -or $full.StartsWith($root + $sep))) {
      Send-Bytes $stream 403 'Forbidden' 'text/plain; charset=utf-8' ([System.Text.Encoding]::UTF8.GetBytes('403 Forbidden')) $isHead
      Write-Host "  403 $rawPath"
      continue
    }
    if (-not (Test-Path $full -PathType Leaf)) {
      Send-Bytes $stream 404 'Not Found' 'text/plain; charset=utf-8' ([System.Text.Encoding]::UTF8.GetBytes('404 Not Found')) $isHead
      Write-Host "  404 $rawPath"
      continue
    }
    $ext = [System.IO.Path]::GetExtension($full).ToLowerInvariant()
    $ctype = if ($mime.ContainsKey($ext)) { $mime[$ext] } else { 'application/octet-stream' }
    $body = [System.IO.File]::ReadAllBytes($full)
    Send-Bytes $stream 200 'OK' $ctype $body $isHead
    Write-Host "  200 $rawPath"
  } catch {
    # 接続断など個別リクエストの失敗はサーバ全体を止めない
  } finally {
    $client.Close()
  }
}
