param(
    [string]$CommitMessage = "",
    [string]$ServerUser = "deploy",
    [string]$ServerHost = "112.78.3.223",
    [string]$RemoteAppDir = "/home/deploy/po_internal_app",
    [string]$PublicBaseUrl = "https://texmind.xyz",
    [string]$IdentityFile = "",
    [switch]$SkipVersionBump,
    [switch]$OpenActions
)

$ErrorActionPreference = "Stop"

function Write-Step($message) {
    Write-Host ""
    Write-Host "==> $message" -ForegroundColor Cyan
}

function Require-Command($name) {
    $command = Get-Command $name -ErrorAction SilentlyContinue
    if (-not $command) {
        throw "Required command not found: $name"
    }
}

function Invoke-Checked($scriptBlock, $errorMessage) {
    & $scriptBlock
    if ($LASTEXITCODE -ne 0) {
        throw $errorMessage
    }
}

function Invoke-Git($arguments) {
    Invoke-Checked {
        & git -C $projectDir @arguments
    } "Git command failed: git $($arguments -join ' ')"
}

function Resolve-PythonCommand($projectDir) {
    $venvPython = Join-Path $projectDir ".venv\Scripts\python.exe"
    if (Test-Path $venvPython) {
        return @($venvPython)
    }
    if (Get-Command py -ErrorAction SilentlyContinue) {
        return @("py", "-3.11")
    }
    if (Get-Command python -ErrorAction SilentlyContinue) {
        return @("python")
    }
    throw "Could not find a working Python interpreter."
}

function Read-AppVersion($versionFile) {
    $match = Select-String -Path $versionFile -Pattern 'APP_VERSION = "([^"]+)"'
    if (-not $match) {
        throw "Could not read APP_VERSION from $versionFile"
    }
    return $match.Matches[0].Groups[1].Value
}

function Get-SshArgs($identityFile) {
    # The server prints a login banner on ordinary SSH sessions. Quiet mode keeps
    # most command output clean; uploads are still marker-trimmed after transfer.
    $args = @("-q")
    $resolvedIdentityFile = $identityFile
    if ([string]::IsNullOrWhiteSpace($resolvedIdentityFile)) {
        $defaultIdentity = Join-Path $HOME ".ssh\texmind_deploy_ed25519"
        if (Test-Path $defaultIdentity) {
            $resolvedIdentityFile = $defaultIdentity
        }
    }
    if (-not [string]::IsNullOrWhiteSpace($resolvedIdentityFile)) {
        $args += @("-i", $resolvedIdentityFile, "-o", "IdentitiesOnly=yes")
    }
    $args += @("-o", "BatchMode=yes", "-o", "ConnectTimeout=15")
    return $args
}

function Convert-ToSingleQuotedShellText($text) {
    return "'" + ([string]$text).Replace("'", "'\''") + "'"
}

function Invoke-SshUpload($sshExe, $sshArgs, $sshTarget, $localPath, $remotePath) {
    if (-not (Test-Path $localPath)) {
        throw "Local file not found: $localPath"
    }
    $remoteDir = ([System.IO.Path]::GetDirectoryName($remotePath)).Replace("\", "/")
    Invoke-Checked {
        & $sshExe @sshArgs $sshTarget "mkdir -p $(Convert-ToSingleQuotedShellText $remoteDir)"
    } "Failed to prepare remote directory: $remoteDir"

    $remoteCommand = "cat > $(Convert-ToSingleQuotedShellText $remotePath)"
    $argumentList = @($sshArgs + @($sshTarget, "bash", "-lc", $remoteCommand))

    $startInfo = New-Object System.Diagnostics.ProcessStartInfo
    $startInfo.FileName = $sshExe
    $startInfo.UseShellExecute = $false
    $startInfo.RedirectStandardInput = $true
    $startInfo.RedirectStandardError = $true
    $startInfo.RedirectStandardOutput = $true
    $escapedArguments = @()
    foreach ($argument in $argumentList) {
        $text = [string]$argument
        if ($text -match '\s|"') {
            $text = '"' + ($text.Replace('"', '\"')) + '"'
        }
        $escapedArguments += $text
    }
    $startInfo.Arguments = ($escapedArguments -join " ")

    $process = New-Object System.Diagnostics.Process
    $process.StartInfo = $startInfo
    [void]$process.Start()
    try {
        $inputStream = [System.IO.File]::OpenRead($localPath)
        try {
            $inputStream.CopyTo($process.StandardInput.BaseStream)
        }
        finally {
            $inputStream.Dispose()
            $process.StandardInput.Close()
        }
        $stdout = $process.StandardOutput.ReadToEnd()
        $stderr = $process.StandardError.ReadToEnd()
        $process.WaitForExit()
        if ($process.ExitCode -ne 0) {
            throw "Upload failed for $localPath -> $remotePath. $stderr"
        }
        if (-not [string]::IsNullOrWhiteSpace($stdout)) {
            Write-Host $stdout.Trim()
        }
    }
    finally {
        $process.Dispose()
    }
}

function Repair-RemoteUploadedFile($sshExe, $sshArgs, $sshTarget, $remotePath, $markerHex) {
    $script = @"
from pathlib import Path
path = Path(r'''$remotePath''')
marker = bytes.fromhex('$markerHex')
data = path.read_bytes()
index = data.find(marker)
print(f"{path} marker_index={index} size={len(data)}")
if index > 0:
    path.write_bytes(data[index:])
elif index < 0:
    raise SystemExit(f"marker not found in {path}")
"@
    Invoke-Checked {
        $script | & $sshExe @sshArgs $sshTarget "python3 -"
    } "Failed to repair remote upload: $remotePath"
}

function Get-FileSha256Lower($path) {
    return (Get-FileHash -Path $path -Algorithm SHA256).Hash.ToLower()
}

function Get-RemoteSha256($sshExe, $sshArgs, $sshTarget, $remotePath) {
    $line = (& $sshExe @sshArgs $sshTarget "sha256sum $(Convert-ToSingleQuotedShellText $remotePath)" | Select-Object -Last 1)
    if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($line)) {
        throw "Could not read remote sha256 for $remotePath"
    }
    return (($line -split "\s+")[0]).ToLower()
}

function Upload-And-Verify($sshExe, $sshArgs, $sshTarget, $localPath, $remotePath, $markerHex) {
    Invoke-SshUpload $sshExe $sshArgs $sshTarget $localPath $remotePath
    Repair-RemoteUploadedFile $sshExe $sshArgs $sshTarget $remotePath $markerHex
    $localHash = Get-FileSha256Lower $localPath
    $remoteHash = Get-RemoteSha256 $sshExe $sshArgs $sshTarget $remotePath
    if ($localHash -ne $remoteHash) {
        throw "SHA256 mismatch for $remotePath. local=$localHash remote=$remoteHash"
    }
    Write-Host "Uploaded and verified: $remotePath" -ForegroundColor Green
}

function Wait-ForWorkflowRun($repo, $workflow, $branch, $headSha, $timeoutSeconds) {
    $deadline = (Get-Date).AddSeconds($timeoutSeconds)
    while ((Get-Date) -lt $deadline) {
        $runsJson = gh run list --repo $repo --workflow $workflow --branch $branch --limit 20 --json databaseId,headSha,status,conclusion,createdAt
        $runs = $runsJson | ConvertFrom-Json
        $match = $runs | Where-Object { $_.headSha -eq $headSha } | Sort-Object createdAt -Descending | Select-Object -First 1
        if ($match) {
            return [string]$match.databaseId
        }
        Start-Sleep -Seconds 5
    }
    throw "Timed out waiting for GitHub Actions run for commit $headSha."
}

function Wait-ForRunComplete($repo, $runId, $timeoutSeconds) {
    $deadline = (Get-Date).AddSeconds($timeoutSeconds)
    while ((Get-Date) -lt $deadline) {
        $json = gh run view $runId --repo $repo --json status,conclusion,jobs,url
        $payload = $json | ConvertFrom-Json
        $jobSummary = ($payload.jobs | ForEach-Object { "$($_.name):$($_.status):$($_.conclusion)" }) -join " | "
        Write-Host "Run $runId status=$($payload.status) conclusion=$($payload.conclusion) jobs=$jobSummary"
        if ($payload.status -eq "completed") {
            if ($payload.conclusion -ne "success") {
                throw "GitHub Actions run failed: $($payload.url)"
            }
            return $payload
        }
        Start-Sleep -Seconds 30
    }
    throw "Timed out waiting for GitHub Actions run $runId."
}

$projectDir = Split-Path -Parent $PSScriptRoot
$repo = "dust1987/Sunny_ERP_App"
$workflow = "build-macos.yml"
$releaseTag = "sunny-erp-macos-arm64-latest"
$versionFile = Join-Path $projectDir "app_core\version.py"
$repoUrl = "https://github.com/$repo"
$actionsUrl = "$repoUrl/actions"
$sshTarget = "$ServerUser@$ServerHost"
$sshArgs = @(Get-SshArgs $IdentityFile)
$sshExe = (Get-Command ssh -ErrorAction Stop).Source
$pythonCommand = @(Resolve-PythonCommand $projectDir)
$pythonExe = $pythonCommand[0]
$pythonArgs = @()
if ($pythonCommand.Length -gt 1) {
    $pythonArgs = $pythonCommand[1..($pythonCommand.Length - 1)]
}

Require-Command git
Require-Command gh
Require-Command ssh

Write-Step "Project directory"
Write-Host $projectDir

Write-Step "Checking GitHub authentication"
Invoke-Checked {
    gh auth status
} "GitHub CLI is not authenticated. Run: gh auth login"

Write-Step "Checking git repository"
Invoke-Git @("rev-parse", "--is-inside-work-tree") | Out-Null
$currentBranch = (& git -C $projectDir branch --show-current).Trim()
if ([string]::IsNullOrWhiteSpace($currentBranch)) {
    throw "Could not detect current git branch."
}

$originalVersion = Read-AppVersion $versionFile
$buildVersion = $originalVersion
if (-not $SkipVersionBump) {
    Write-Step "Bumping APP_VERSION"
    $buildVersion = (& $pythonExe @pythonArgs (Join-Path $projectDir "scripts\bump_version.py") | Select-Object -Last 1).Trim()
    if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($buildVersion)) {
        throw "Version bump failed."
    }
    Write-Host "APP_VERSION: $originalVersion -> $buildVersion" -ForegroundColor Green
}
else {
    Write-Step "Skipping APP_VERSION bump"
    Write-Host "APP_VERSION: $buildVersion" -ForegroundColor Yellow
}

Write-Step "Staging changes"
Invoke-Git @("add", ".")
$statusLines = @(& git -C $projectDir status --short)
if ($LASTEXITCODE -ne 0) {
    throw "Failed to read git status."
}

if ($statusLines.Count -gt 0) {
    if ([string]::IsNullOrWhiteSpace($CommitMessage)) {
        $CommitMessage = "Build macOS arm64 version $buildVersion"
    }
    Write-Step "Creating commit"
    Write-Host $CommitMessage -ForegroundColor Green
    Invoke-Git @("commit", "-m", $CommitMessage)
}
else {
    Write-Step "No local changes to commit"
    Write-Host "The script will push and dispatch the current commit if needed." -ForegroundColor Yellow
}

$headSha = (& git -C $projectDir rev-parse HEAD).Trim()

Write-Step "Pushing to GitHub"
Invoke-Git @("push", "origin", $currentBranch)

Write-Step "Waiting for macOS 14 arm64 workflow"
try {
    $runId = Wait-ForWorkflowRun $repo $workflow $currentBranch $headSha 180
}
catch {
    Write-Warning "Push did not produce a visible workflow run in time. Dispatching $workflow manually."
    Invoke-Checked {
        gh workflow run $workflow --repo $repo --ref $currentBranch
    } "Failed to dispatch workflow $workflow."
    $runId = Wait-ForWorkflowRun $repo $workflow $currentBranch $headSha 180
}
$runPayload = Wait-ForRunComplete $repo $runId 1800
$jobNames = @($runPayload.jobs | ForEach-Object { [string]$_.name })
if ($jobNames.Count -ne 1 -or $jobNames[0] -notmatch "macos-14" -or $jobNames[0] -notmatch "arm64") {
    throw "Unexpected macOS workflow jobs: $($jobNames -join ', ')"
}

Write-Step "Downloading latest macOS arm64 release"
$downloadDir = Join-Path $projectDir ("dist\macos_release\v{0}_{1}" -f $buildVersion, (Get-Date -Format "yyyyMMdd_HHmmss"))
New-Item -ItemType Directory -Force -Path $downloadDir | Out-Null
Invoke-Checked {
    gh release download $releaseTag --repo $repo --pattern SunnyERPApp_macOS_arm64.zip --pattern RUN_FOR_TESTERS_macOS.txt --dir $downloadDir
} "Failed to download macOS release assets."
$zipPath = Join-Path $downloadDir "SunnyERPApp_macOS_arm64.zip"
$guidePath = Join-Path $downloadDir "RUN_FOR_TESTERS_macOS.txt"
if (-not (Test-Path $zipPath) -or -not (Test-Path $guidePath)) {
    throw "Downloaded release is missing zip or guide."
}
$zipSha256 = Get-FileSha256Lower $zipPath
$updatedAtUtc = (Get-Date).ToUniversalTime().ToString("o")
$metadata = [ordered]@{
    available = $true
    version = $buildVersion
    channel = "macos-arm64"
    filename = "SunnyERPApp_macOS_arm64.zip"
    size_bytes = (Get-Item $zipPath).Length
    sha256 = $zipSha256
    commit = $headSha
    github_run_id = $runId
    updated_at_utc = $updatedAtUtc
    download_url = "/downloads/macos/latest"
    guide_url = "/downloads/macos/guide"
    notes = "Latest macOS 14 Apple Silicon build."
}
$metadataPath = Join-Path $downloadDir "macos_latest.json"
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($metadataPath, ($metadata | ConvertTo-Json -Depth 5), $utf8NoBom)

Write-Step "Uploading macOS release to server"
$remoteDistDir = "$RemoteAppDir/dist"
$uploads = @(
    @{ Local = $zipPath; Remote = "$remoteDistDir/SunnyERPApp_macOS_arm64.zip"; MarkerHex = "504b0304" },
    @{ Local = $guidePath; Remote = "$remoteDistDir/RUN_FOR_TESTERS_macOS.txt"; MarkerHex = "53756e6e792045525020417070" },
    @{ Local = $metadataPath; Remote = "$remoteDistDir/macos_latest.json"; MarkerHex = "7b" },
    @{ Local = (Join-Path $projectDir "app_core\version.py"); Remote = "$RemoteAppDir/app_core/version.py"; MarkerHex = "66726f6d205f5f6675747572655f5f" },
    @{ Local = (Join-Path $projectDir "local_server\app.py"); Remote = "$RemoteAppDir/local_server/app.py"; MarkerHex = "66726f6d205f5f6675747572655f5f" },
    @{ Local = (Join-Path $projectDir "local_server\web\index.html"); Remote = "$RemoteAppDir/local_server/web/index.html"; MarkerHex = "3c21646f63747970652068746d6c3e" },
    @{ Local = (Join-Path $projectDir "local_server\web\app.css"); Remote = "$RemoteAppDir/local_server/web/app.css"; MarkerHex = "3a726f6f74" },
    @{ Local = (Join-Path $projectDir "local_server\web\app.js"); Remote = "$RemoteAppDir/local_server/web/app.js"; MarkerHex = "636f6e73742053544f524147455f4b455953" }
)
foreach ($upload in $uploads) {
    Upload-And-Verify $sshExe $sshArgs $sshTarget $upload.Local $upload.Remote $upload.MarkerHex
}

Write-Step "Restarting backend"
$pidText = (& $sshExe @sshArgs $sshTarget "systemctl show -p MainPID --value po_internal_app" | Select-Object -Last 1).Trim()
if ($pidText -and $pidText -ne "0") {
    & $sshExe @sshArgs $sshTarget "kill $pidText"
}
Start-Sleep -Seconds 5
$newPidText = (& $sshExe @sshArgs $sshTarget "systemctl show -p MainPID --value po_internal_app" | Select-Object -Last 1).Trim()
if (-not $newPidText -or $newPidText -eq "0") {
    Invoke-Checked {
        & $sshExe @sshArgs $sshTarget "cd $(Convert-ToSingleQuotedShellText $RemoteAppDir) && nohup .venv/bin/python3.11 -m uvicorn local_server.app:app --host 127.0.0.1 --port 8000 >/home/deploy/po_internal_app/server_manual.log 2>&1 &"
    } "Failed to start backend manually."
    Start-Sleep -Seconds 5
}

Write-Step "Verifying public macOS release"
$macApiUrl = "$($PublicBaseUrl.TrimEnd('/'))/api/releases/macos/latest"
$healthUrl = "$($PublicBaseUrl.TrimEnd('/'))/health"
$macPayload = $null
for ($attempt = 1; $attempt -le 20; $attempt++) {
    try {
        Invoke-RestMethod -Uri $healthUrl -Method Get -TimeoutSec 20 | Out-Null
        $macPayload = Invoke-RestMethod -Uri $macApiUrl -Method Get -TimeoutSec 30
        break
    }
    catch {
        Write-Host "Waiting for backend/API after restart... attempt $attempt/20" -ForegroundColor Yellow
        Start-Sleep -Seconds 3
    }
}
if ($null -eq $macPayload) {
    throw "Could not verify public macOS release API after backend restart."
}
if ($macPayload.version -ne $buildVersion) {
    throw "macOS API returned version $($macPayload.version), expected $buildVersion."
}
if ($macPayload.sha256 -ne $zipSha256) {
    throw "macOS API returned sha256 $($macPayload.sha256), expected $zipSha256."
}

Write-Host ""
Write-Host "macOS build and publish completed." -ForegroundColor Green
Write-Host "Version: $buildVersion" -ForegroundColor Green
Write-Host "GitHub run: $($runPayload.url)" -ForegroundColor Green
Write-Host "Downloaded to: $downloadDir" -ForegroundColor Green
Write-Host "Server download: $($PublicBaseUrl.TrimEnd('/'))/downloads/macos/latest" -ForegroundColor Green
Write-Host "Server metadata: $macApiUrl" -ForegroundColor Green

if ($OpenActions) {
    Start-Process $actionsUrl
}
