# session-scaffold.ps1 -- scaffold 1 logic-session folder + _context skeleton (M1, S149).
# TAILOR tu hub AI_INFRA/scripts/session_scaffold.py (169 dong) -> SE PowerShell, 2-mode split @S146.
#
# MODE (exactly one):
#   -New     : allocate a NEW logic-session = max+1 (session-start ONLY). Optional -N pins an
#              explicit id; if that id already exists (folder OR _context) -> COLLISION
#              (exit 4, writes NOTHING).
#   -Ensure  : idempotent heal of the CURRENT session (pause/tiep). _context missing -> create;
#              _context present -> exit 0 NO-OP (mtime preserved). Optional -N pins the id;
#              default = max existing session.
#
# INVARIANTS:
#   - ts-moc = `git log -1 --format=%cI` of RepoRoot HEAD (git-committer). NO wall-clock: git
#     unavailable -> exit 3 (refuse Get-Date). Deliberate DEVIATION from hub tagged-FALLBACK --
#     spec M1 line 33 "khong wall-clock"; ts feeds C10 LATENESS measurement, must be deterministic.
#   - NEVER overwrite an existing _context (ANY mode) -- hard guard in Write-Context (exit 5).
#   - Pure-ASCII source (gotcha #30/#37 = PS 5.1 diacritics parser-fail). Vietnamese lives ONLY
#     in the external template .md, never in this .ps1.
#
# -RepoRoot = fault-inject seam: point at a scratch tree to prove teeth, not just happy-path.
# Exit codes: 0 ok/no-op | 2 usage | 3 git-unavailable | 4 collision | 5 defensive-overwrite-block.

param(
    [switch]$New,
    [switch]$Ensure,
    [int]$N,
    [string]$RepoRoot = "$PSScriptRoot\.."
)

$ErrorActionPreference = 'Stop'

# --- helpers ---
function Fail {
    param([string]$Msg, [int]$Code)
    Write-Output "session-scaffold: ERROR: $Msg"
    exit $Code
}

function Invoke-Git {
    # Run git with stderr discarded; return @{ Out; Code }. EAP=Continue so a non-repo tree
    # (git exit 128) does NOT throw under the script-wide Stop preference.
    param([string[]]$GitArgs)
    $old = $ErrorActionPreference
    $ErrorActionPreference = 'Continue'
    try {
        $out = & git @GitArgs 2>$null
        return @{ Out = (($out | Out-String)).Trim(); Code = $LASTEXITCODE }
    } finally {
        $ErrorActionPreference = $old
    }
}

function Rel {
    param([string]$Path, [string]$Root)
    if ($Path.StartsWith($Root, [StringComparison]::OrdinalIgnoreCase)) {
        return $Path.Substring($Root.Length).TrimStart('\', '/')
    }
    return $Path
}

function Write-Context {
    # Render template -> _context, UTF-8 no-BOM + LF. HARD guard: never overwrite an existing
    # _context (any mode) -- this is the durable owner-evidence file.
    param([string]$TemplatePath, [string]$OutPath, [int]$SessionN, [string]$Ts, [string]$Head)
    if (Test-Path -LiteralPath $OutPath) {
        Fail "REFUSE-OVERWRITE: $OutPath exists (never overwrite _context)" 5
    }
    $tpl = [System.IO.File]::ReadAllText($TemplatePath, [System.Text.Encoding]::UTF8)
    $rendered = $tpl.Replace('{{N}}', "$SessionN").Replace('{{TS}}', $Ts).Replace('{{HEAD}}', $Head)
    $rendered = $rendered -replace "`r`n", "`n"
    $outDir = Split-Path -Parent $OutPath
    if (-not (Test-Path -LiteralPath $outDir)) {
        New-Item -ItemType Directory -Path $outDir -Force | Out-Null
    }
    $enc = New-Object System.Text.UTF8Encoding($false)   # $false = emit NO byte-order-mark
    [System.IO.File]::WriteAllText($OutPath, $rendered, $enc)
}

# --- validate mode (exactly one) ---
if ($New -and $Ensure)         { Fail "specify exactly one of -New / -Ensure (both given)" 2 }
if (-not $New -and -not $Ensure) { Fail "specify exactly one of -New / -Ensure (none given)" 2 }

$explicitN = $PSBoundParameters.ContainsKey('N')
if ($explicitN -and $N -lt 0)  { Fail "-N must be a non-negative integer (got $N)" 2 }

# --- resolve root + template ---
if (-not (Test-Path -LiteralPath $RepoRoot)) { Fail "RepoRoot not found: $RepoRoot" 2 }
$root = (Resolve-Path -LiteralPath $RepoRoot).Path
$templatePath = Join-Path $root '.claude\templates\session-context-template.md'
if (-not (Test-Path -LiteralPath $templatePath)) { Fail "template not found: $templatePath" 2 }
$sessionsRoot = Join-Path $root '.claude\sessions'

# --- compute max existing session-<N> ---
$maxN = 0
if (Test-Path -LiteralPath $sessionsRoot) {
    Get-ChildItem -LiteralPath $sessionsRoot -Directory -ErrorAction SilentlyContinue | ForEach-Object {
        if ($_.Name -match '^session-(\d+)$') {
            $k = [int]$Matches[1]
            if ($k -gt $maxN) { $maxN = $k }
        }
    }
}

# --- resolve target N per mode ---
if ($New) {
    if ($explicitN) { $targetN = $N } else { $targetN = $maxN + 1 }
} else {
    if ($explicitN) {
        $targetN = $N
    } else {
        if ($maxN -lt 1) { Fail "-Ensure: no existing session to heal (run -New first)" 2 }
        $targetN = $maxN
    }
}

$sessionDir    = Join-Path $sessionsRoot "session-$targetN"
$contextPath   = Join-Path $sessionDir "_context-s-$targetN.md"
$folderExists  = Test-Path -LiteralPath $sessionDir
$contextExists = Test-Path -LiteralPath $contextPath

# --- ts-moc from git HEAD (NO wall-clock; refuse if git down) ---
$r1 = Invoke-Git @('-C', $root, 'log', '-1', '--format=%cI')
$r2 = Invoke-Git @('-C', $root, 'log', '-1', '--format=%h')
if ($r1.Code -ne 0 -or $r2.Code -ne 0) {
    Fail "git HEAD unavailable at $root -- refusing wall-clock fallback (spec M1: no wall-clock)" 3
}
$ts   = $r1.Out
$head = $r2.Out
if ([string]::IsNullOrWhiteSpace($ts)) {
    Fail "git returned empty committer-ts -- refusing wall-clock fallback" 3
}

# --- mode: New (allocate; collision => write NOTHING) ---
if ($New) {
    if ($folderExists -or $contextExists) {
        $why = if ($contextExists) { "_context exists" } else { "session folder exists" }
        Write-Output "session-scaffold: COLLISION session-$targetN ($why) -- writing NOTHING (exit 4)."
        exit 4
    }
    Write-Context -TemplatePath $templatePath -OutPath $contextPath -SessionN $targetN -Ts $ts -Head $head
    Write-Output "session-scaffold: [New] created session-$targetN (max was $maxN)"
    Write-Output "  folder  = $(Rel $sessionDir $root)"
    Write-Output "  context = $(Rel $contextPath $root)"
    Write-Output "  ts-moc  = $ts  (HEAD $head)"
    exit 0
}

# --- mode: Ensure (idempotent heal) ---
if ($contextExists) {
    Write-Output "session-scaffold: [Ensure] session-$targetN _context present -- NO-OP (exit 0, mtime preserved)."
    exit 0
}
Write-Context -TemplatePath $templatePath -OutPath $contextPath -SessionN $targetN -Ts $ts -Head $head
$note = if ($folderExists) { "healed (folder existed, _context missing)" } else { "created (folder was missing)" }
Write-Output "session-scaffold: [Ensure] session-$targetN $note"
Write-Output "  context = $(Rel $contextPath $root)"
Write-Output "  ts-moc  = $ts  (HEAD $head)"
exit 0
