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

$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 Resolve-PythonCommand($projectDir) {
    $venvPython = Join-Path $projectDir ".venv\Scripts\python.exe"
    if (Test-Path $venvPython) {
        return @($venvPython)
    }

    $py311 = Get-Command py -ErrorAction SilentlyContinue
    if ($py311) {
        return @("py", "-3.11")
    }

    $python = Get-Command python -ErrorAction SilentlyContinue
    if ($python) {
        return @("python")
    }

    throw "Could not find a working Python interpreter."
}

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

function Invoke-SshUploadFallback($sshExe, $sshArgs, $sshTarget, $localPath, $remotePath) {
    if (-not (Test-Path $localPath)) {
        throw "Local file not found: $localPath"
    }

    $remoteCommand = "cat > $remotePath"
    $argumentList = @($sshArgs + @($sshTarget, $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) {
            $message = "SSH upload fallback failed for $localPath -> $remotePath"
            if (-not [string]::IsNullOrWhiteSpace($stderr)) {
                $message += ": $stderr"
            }
            throw $message
        }
        if (-not [string]::IsNullOrWhiteSpace($stdout)) {
            Write-Host $stdout.Trim()
        }
    }
    finally {
        $process.Dispose()
    }
}

function Copy-FileToRemote($scpExe, $sshExe, $sshArgs, $sshTarget, $localPath, $remotePath) {
    # The production server disables the SFTP subsystem used by modern scp.
    # Stream uploads over ssh directly so the release script works from .bat without noisy native stderr failures.
    Invoke-SshUploadFallback $sshExe $sshArgs $sshTarget $localPath $remotePath
    $global:LASTEXITCODE = 0
}

function Get-SshArgs($identityFile) {
    # The server prints a login banner on ordinary SSH sessions. Quiet mode keeps
    # stream uploads byte-clean while still allowing command stdout through.
    $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 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 Write-AppVersion($versionFile, $version) {
    $content = Get-Content -Path $versionFile -Raw
    $updated = [regex]::Replace($content, 'APP_VERSION = "([^"]+)"', "APP_VERSION = ""$version""", 1)
    $utf8NoBom = New-Object System.Text.UTF8Encoding($false)
    [System.IO.File]::WriteAllText($versionFile, $updated, $utf8NoBom)
}

function Get-RelativeZipPath($baseDir, $path) {
    $base = (Resolve-Path $baseDir).Path.TrimEnd("\/") + [System.IO.Path]::DirectorySeparatorChar
    $resolved = (Resolve-Path $path).Path
    if (-not $resolved.StartsWith($base, [System.StringComparison]::OrdinalIgnoreCase)) {
        throw "Path is outside base directory: $resolved"
    }
    return $resolved.Substring($base.Length).Replace("\", "/")
}

function Assert-ReleaseFolderComplete($releaseDir, $requiredRelativePaths) {
    if (-not (Test-Path $releaseDir)) {
        throw "Missing release directory: $releaseDir"
    }
    foreach ($relativePath in $requiredRelativePaths) {
        $localPath = Join-Path $releaseDir ($relativePath.Replace("/", [System.IO.Path]::DirectorySeparatorChar))
        if (-not (Test-Path $localPath)) {
            throw "Release folder is missing required file or folder: $relativePath"
        }
    }
}

function Assert-ZipMatchesReleaseFolder($packageZip, $distDir, $releaseDir, $requiredZipEntries) {
    Add-Type -AssemblyName System.IO.Compression.FileSystem
    if (-not (Test-Path $packageZip)) {
        throw "Missing package zip: $packageZip"
    }
    $archive = [System.IO.Compression.ZipFile]::OpenRead((Resolve-Path $packageZip).Path)
    try {
        $entrySet = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
        foreach ($entry in $archive.Entries) {
            if (-not [string]::IsNullOrWhiteSpace($entry.Name)) {
                [void]$entrySet.Add($entry.FullName)
            }
        }

        foreach ($requiredEntry in $requiredZipEntries) {
            if (-not $entrySet.Contains($requiredEntry)) {
                throw "Package zip is missing required entry: $requiredEntry"
            }
        }

        $missingEntries = @()
        foreach ($file in Get-ChildItem -Path $releaseDir -Recurse -File) {
            $entryName = Get-RelativeZipPath $distDir $file.FullName
            if (-not $entrySet.Contains($entryName)) {
                $missingEntries += $entryName
            }
        }
        if ($missingEntries.Count -gt 0) {
            throw "Package zip is missing release file(s): $($missingEntries -join ', ')"
        }
    }
    finally {
        $archive.Dispose()
    }
}

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

$projectDir = Split-Path -Parent $PSScriptRoot
$distDir = Join-Path $projectDir "dist"
$releaseDir = Join-Path $distDir "SunnyERPApp"
$packageZip = Join-Path $distDir "SunnyERPApp_package.zip"
$versionFile = Join-Path $projectDir "app_core\version.py"
$requiredReleasePaths = @(
    "SunnyERPApp.exe",
    "app_core",
    "app_core/version.py",
    "app_modules",
    "app_modules/__init__.py",
    "app_modules/production_module.py",
    "app_modules/po_module.py",
    "app_modules/bom_module.py",
    "app_modules/translation_module.py",
    "local_server",
    "local_server/app.py",
    "database.py",
    "requirements.txt",
    "settings"
)
$requiredZipEntries = @(
    "SunnyERPApp/SunnyERPApp.exe",
    "SunnyERPApp/app_core/version.py",
    "SunnyERPApp/app_modules/__init__.py",
    "SunnyERPApp/app_modules/production_module.py",
    "SunnyERPApp/app_modules/po_module.py",
    "SunnyERPApp/app_modules/bom_module.py",
    "SunnyERPApp/app_modules/translation_module.py",
    "SunnyERPApp/local_server/app.py",
    "SunnyERPApp/database.py",
    "SunnyERPApp/requirements.txt"
)
$backendSourceFiles = @(
    (Join-Path $projectDir "app_core\version.py"),
    (Join-Path $projectDir "local_server\app.py"),
    (Join-Path $projectDir "local_server\web\index.html"),
    (Join-Path $projectDir "local_server\web\app.css"),
    (Join-Path $projectDir "local_server\web\app.js"),
    (Join-Path $projectDir "local_server\web\favicon.png"),
    (Join-Path $projectDir "database.py")
)
$bumpVersionScript = Join-Path $projectDir "scripts\bump_version.py"
$latestApiUrl = "$($PublicBaseUrl.TrimEnd('/'))/api/releases/latest"
$manifestUrl = "$($PublicBaseUrl.TrimEnd('/'))/manifests/desktop.json"
$remoteDistDir = "$RemoteAppDir/dist"
$sshTarget = "$ServerUser@$ServerHost"
$sshArgs = @(Get-SshArgs $IdentityFile)
$pythonCommand = @(Resolve-PythonCommand $projectDir)
$pythonExe = $pythonCommand[0]
$pythonArgs = @()
if ($pythonCommand.Length -gt 1) {
    $pythonArgs = $pythonCommand[1..($pythonCommand.Length - 1)]
}

Require-Command ssh
Require-Command scp
$sshExe = (Get-Command ssh -ErrorAction Stop).Source
$scpExe = (Get-Command scp -ErrorAction Stop).Source

$originalVersion = Read-AppVersion $versionFile
$expectedVersion = $originalVersion
$versionBumped = $false
$publishCompleted = $false

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

    if (-not $SkipVersionBump) {
        Write-Step "Bumping desktop app version"
        $newVersion = (& $pythonExe @pythonArgs $bumpVersionScript).Trim()
        if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($newVersion)) {
            throw "Failed to bump app version."
        }
        $versionBumped = $true
        Write-Host "New version: $newVersion" -ForegroundColor Green
    }
    else {
        Write-Step "Skipping version bump"
    }

    $expectedVersion = Read-AppVersion $versionFile
    Write-Step "Target release version"
    Write-Host $expectedVersion -ForegroundColor Green

    if (-not $SkipBuild) {
        Write-Step "Building latest desktop release"
        Push-Location $projectDir
        try {
            Invoke-Checked { & (Join-Path $projectDir "scripts\build_app_only.bat") } "Build failed."
        }
        finally {
            Pop-Location
        }
    }
    else {
        Write-Step "Skipping build step"
    }

    if (-not (Test-Path $packageZip)) {
        throw "Missing package zip: $packageZip"
    }
    if (-not (Test-Path $releaseDir)) {
        throw "Missing release directory: $releaseDir"
    }

    Write-Step "Verifying local release package contents"
    Assert-ReleaseFolderComplete $releaseDir $requiredReleasePaths
    Assert-ZipMatchesReleaseFolder $packageZip $distDir $releaseDir $requiredZipEntries
    Write-Host "Local release folder and package zip include all release files." -ForegroundColor Green

    Write-Step "Ensuring remote dist directory exists"
    Invoke-Checked {
        ssh @sshArgs $sshTarget "mkdir -p $remoteDistDir"
    } "Failed to prepare remote dist directory."

    Write-Step "Uploading release package zip"
    Invoke-Checked {
        Copy-FileToRemote $scpExe $sshExe $sshArgs $sshTarget $packageZip "$remoteDistDir/$(Split-Path $packageZip -Leaf)"
    } "Failed to upload package zip."

    Write-Step "Refreshing remote release folder from uploaded zip"
    $extractCommand = "bash -lc 'set -e; cd $remoteDistDir; rm -rf SunnyERPApp; unzip -oq SunnyERPApp_package.zip; test -d SunnyERPApp; echo $remoteDistDir/SunnyERPApp'"
    Invoke-Checked {
        ssh @sshArgs $sshTarget $extractCommand
    } "Failed to refresh remote release folder from zip."

    Write-Step "Verifying remote release package contents"
    $remoteRequiredTests = @()
    foreach ($relativePath in $requiredReleasePaths) {
        $remoteRequiredTests += "test -e " + (Convert-ToSingleQuotedShellText "$remoteDistDir/SunnyERPApp/$relativePath")
    }
    foreach ($zipEntry in $requiredZipEntries) {
        $remoteRequiredTests += "unzip -Z1 " + (Convert-ToSingleQuotedShellText "$remoteDistDir/SunnyERPApp_package.zip") + " | grep -Fx -- " + (Convert-ToSingleQuotedShellText $zipEntry) + " >/dev/null"
    }
    $remoteVerifyCommand = "bash -lc " + (Convert-ToSingleQuotedShellText ("set -e; " + ($remoteRequiredTests -join "; ") + "; echo remote-release-ok"))
    Invoke-Checked {
        ssh @sshArgs $sshTarget $remoteVerifyCommand
    } "Remote release package is missing required files."

    Write-Step "Syncing backend source files"
    foreach ($backendSourceFile in $backendSourceFiles) {
        $resolvedBackendSourceFile = (Resolve-Path $backendSourceFile).Path
        $projectDirWithSeparator = $projectDir.TrimEnd("\/") + [System.IO.Path]::DirectorySeparatorChar
        if ($resolvedBackendSourceFile.StartsWith($projectDirWithSeparator, [System.StringComparison]::OrdinalIgnoreCase)) {
            $relativePath = $resolvedBackendSourceFile.Substring($projectDirWithSeparator.Length).Replace("\", "/")
        }
        else {
            throw "Backend source file is outside project directory: $resolvedBackendSourceFile"
        }
        $remoteFile = "$RemoteAppDir/$relativePath"
        $remoteDir = [System.IO.Path]::GetDirectoryName($remoteFile).Replace("\", "/")
        Invoke-Checked {
            ssh @sshArgs $sshTarget "mkdir -p $remoteDir"
        } "Failed to prepare remote backend directory for $relativePath."
        Invoke-Checked {
            Copy-FileToRemote $scpExe $sshExe $sshArgs $sshTarget $backendSourceFile $remoteFile
        } "Failed to sync backend source file $relativePath."
    }

    Write-Step "Restarting backend service"
    $restartCommand = 'bash -lc ''pid=$(systemctl show -p MainPID --value po_internal_app); if [ -n "$pid" ] && [ "$pid" != "0" ]; then kill "$pid"; fi'''
    Invoke-Checked {
        ssh @sshArgs $sshTarget $restartCommand
    } "Failed to restart backend process."

    Write-Step "Waiting for backend to report version $expectedVersion"
    $backendReady = $false
    for ($attempt = 1; $attempt -le 15; $attempt++) {
        Start-Sleep -Seconds 2
        try {
            $backendPayload = Invoke-RestMethod -Uri $latestApiUrl -Method Get -TimeoutSec 20
            if ($backendPayload.version -eq $expectedVersion) {
                $backendReady = $true
                break
            }
        }
        catch {
        }
    }
    if (-not $backendReady) {
        throw "Backend did not report expected version $expectedVersion in time."
    }

    Write-Step "Checking remote dist contents"
    Invoke-Checked {
        ssh @sshArgs $sshTarget "ls -lah $remoteDistDir && echo --- && ls -lah $remoteDistDir/SunnyERPApp | head -40"
    } "Failed to inspect remote dist contents."

    Write-Step "Verifying public latest release API"
    $latestPayload = Invoke-RestMethod -Uri $latestApiUrl -Method Get -TimeoutSec 20
    $manifestPayload = Invoke-RestMethod -Uri $manifestUrl -Method Get -TimeoutSec 20

    if ($latestPayload.version -ne $expectedVersion) {
        throw "Latest release API returned version $($latestPayload.version), expected $expectedVersion."
    }
    if ($manifestPayload.version -ne $expectedVersion) {
        throw "Desktop manifest returned version $($manifestPayload.version), expected $expectedVersion."
    }

    $publishCompleted = $true

    Write-Host ""
    Write-Host "Latest release payload:" -ForegroundColor Green
    $latestPayload | ConvertTo-Json -Depth 6

    Write-Host ""
    Write-Host "Manifest payload:" -ForegroundColor Green
    $manifestPayload | ConvertTo-Json -Depth 6

    Write-Host ""
    Write-Host "Publish flow completed." -ForegroundColor Green
    Write-Host "Published version: $expectedVersion" -ForegroundColor Green
    Write-Host "Next check on client machines:" -ForegroundColor Yellow
    Write-Host "- Open CitiFox ERP App"
    Write-Host "- Go to Setting"
    Write-Host "- Click Test Connection"
    Write-Host "- Click Check Updates"
}
catch {
    if ($versionBumped -and -not $publishCompleted) {
        Write-Warning "Publish failed. Restoring APP_VERSION back to $originalVersion."
        Write-AppVersion $versionFile $originalVersion
    }
    throw
}
