[CmdletBinding()]
param()

$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
$ManifestUrl = "__MANIFEST_URL__"
$ProviderUrl = "__PROVIDER_URL__"
[System.Net.ServicePointManager]::SecurityProtocol = (
    [System.Net.ServicePointManager]::SecurityProtocol -bor
    [System.Net.SecurityProtocolType]::Tls12
)

function Throw-InstallError {
    param(
        [string]$Code,
        [string]$Message,
        [object]$Detail = $null
    )

    $exception = [System.InvalidOperationException]::new($Message)
    $exception.Data["errorCode"] = $Code
    if ($null -ne $Detail) {
        $exception.Data["detail"] = $Detail
    }
    throw $exception
}

function Invoke-Python {
    param(
        [string]$Executable,
        [string[]]$Arguments
    )

    $previousErrorAction = $ErrorActionPreference
    try {
        $ErrorActionPreference = "Continue"
        $output = & $Executable @Arguments 2>&1
        $exitCode = $LASTEXITCODE
    }
    finally {
        $ErrorActionPreference = $previousErrorAction
    }
    return [ordered]@{
        exitCode = $exitCode
        output = @($output | ForEach-Object { "$_" })
    }
}

function Select-Python {
    $candidates = @()
    if ($env:ProgramData) {
        $candidates += [ordered]@{
            executable = [System.IO.Path]::Combine($env:ProgramData, "miniconda3", "python.exe")
            prefix = @()
        }
    }
    $candidates += [ordered]@{ executable = "py"; prefix = @("-3.11") }
    $candidates += [ordered]@{ executable = "python"; prefix = @() }
    $candidates += [ordered]@{ executable = "python3"; prefix = @() }

    foreach ($candidate in $candidates) {
        try {
            $arguments = @($candidate.prefix) + @(
                "-c",
                "import sys,tkinter; assert sys.version_info >= (3,11); print(sys.executable)"
            )
            $probe = Invoke-Python -Executable $candidate.executable -Arguments $arguments
            if ($probe.exitCode -eq 0 -and $probe.output.Count -gt 0) {
                $executable = $probe.output[-1].Trim()
                if ([System.IO.File]::Exists($executable)) {
                    return $executable
                }
            }
        }
        catch {
            continue
        }
    }
    Throw-InstallError "python_not_found" "Python 3.11+ with Tkinter was not found"
}

function Ensure-PyYaml {
    param([string]$Python)

    $probe = Invoke-Python -Executable $Python -Arguments @("-c", "import yaml")
    if ($probe.exitCode -eq 0) {
        return $false
    }
    $install = Invoke-Python -Executable $Python -Arguments @(
        "-m",
        "pip",
        "install",
        "--disable-pip-version-check",
        "--user",
        "PyYAML"
    )
    if ($install.exitCode -ne 0) {
        $tail = ($install.output | Select-Object -Last 12) -join "`n"
        Throw-InstallError "pyyaml_install_failed" "PyYAML installation failed" @{
            exitCode = $install.exitCode
            outputTail = $tail
        }
    }
    $verify = Invoke-Python -Executable $Python -Arguments @("-c", "import yaml")
    if ($verify.exitCode -ne 0) {
        Throw-InstallError "pyyaml_unavailable" "PyYAML is still unavailable after installation"
    }
    return $true
}

try {
    if (-not $IsWindows -and $PSVersionTable.PSEdition -eq "Core") {
        Throw-InstallError "windows_required" "This installer only supports native Windows"
    }
    if ($env:OS -ne "Windows_NT") {
        Throw-InstallError "windows_required" "This installer only supports native Windows"
    }
    $configRoot = [System.IO.Path]::Combine($HOME, ".unidesk")
    $providerRoot = [System.IO.Path]::Combine($configRoot, "unideskcprovider")
    $configPath = [System.IO.Path]::Combine($configRoot, "provider.yaml")
    $providerPath = [System.IO.Path]::Combine($providerRoot, "unideskcprovider.py")
    [System.IO.Directory]::CreateDirectory($providerRoot) | Out-Null
    if (-not [System.IO.File]::Exists($configPath)) {
        Throw-InstallError "config_missing" "Provider user YAML is missing" @{
            configPath = $configPath
        }
    }

    $manifest = Invoke-RestMethod -UseBasicParsing -Uri $ManifestUrl -Method Get
    if (
        $manifest.schemaVersion -ne 1 -or
        "$($manifest.latestVersion)" -notmatch "^[0-9]+(?:\.[0-9]+){1,3}$" -or
        [long]$manifest.bytes -le 0 -or
        "$($manifest.sha256)" -notmatch "^[a-f0-9]{64}$"
    ) {
        Throw-InstallError "invalid_manifest" "Provider release manifest is invalid"
    }

    $downloadPath = [System.IO.Path]::Combine($providerRoot, "unideskcprovider.py.download-$PID")
    Invoke-WebRequest -UseBasicParsing -Uri $ProviderUrl -OutFile $downloadPath
    $providerBytes = [System.IO.File]::ReadAllBytes($downloadPath)
    $digest = (Get-FileHash -LiteralPath $downloadPath -Algorithm SHA256).Hash.ToLowerInvariant()
    if ($providerBytes.LongLength -ne [long]$manifest.bytes -or $digest -ne "$($manifest.sha256)") {
        Throw-InstallError "artifact_integrity_failed" "Provider artifact bytes or SHA-256 did not match the manifest" @{
            expectedBytes = [long]$manifest.bytes
            actualBytes = $providerBytes.LongLength
            expectedSha256 = "$($manifest.sha256)"
            actualSha256 = $digest
        }
    }
    $source = [System.Text.Encoding]::UTF8.GetString($providerBytes)
    if (
        -not $source.Contains('APP_NAME = "UniDesk Windows Provider"') -or
        -not $source.Contains("APP_VERSION = `"$($manifest.latestVersion)`"")
    ) {
        Throw-InstallError "artifact_identity_failed" "Downloaded artifact is not the declared UniDesk Windows Provider version"
    }

    $python = Select-Python
    $pyYamlInstalled = Ensure-PyYaml -Python $python
    $compile = Invoke-Python -Executable $python -Arguments @("-m", "py_compile", $downloadPath)
    if ($compile.exitCode -ne 0) {
        Throw-InstallError "provider_compile_failed" "Downloaded Provider failed Python compilation" @{
            exitCode = $compile.exitCode
            outputTail = (($compile.output | Select-Object -Last 12) -join "`n")
        }
    }

    $configProbeCode = @'
import importlib.util
import json
import sys
from importlib.machinery import SourceFileLoader

loader = SourceFileLoader("unideskcprovider_install_probe", sys.argv[1])
spec = importlib.util.spec_from_loader(loader.name, loader)
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
loader.exec_module(module)
config = module.ProviderConfig(module.Path(sys.argv[2]))
print(json.dumps({
    "id": config.id,
    "server": config.server_url,
    "credentialFile": str(config.raw["credentialFile"]),
    "credentialPath": str(config.credential_path),
    "autoStart": config.auto_start,
}))
'@
    $configProbePath = [System.IO.Path]::Combine(
        $providerRoot,
        "config-probe-$PID.py"
    )
    [System.IO.File]::WriteAllText(
        $configProbePath,
        $configProbeCode,
        [System.Text.UTF8Encoding]::new($false)
    )
    $configProbe = Invoke-Python -Executable $python -Arguments @(
        $configProbePath,
        $downloadPath,
        $configPath
    )
    [System.IO.File]::Delete($configProbePath)
    if ($configProbe.exitCode -ne 0 -or $configProbe.output.Count -eq 0) {
        Throw-InstallError "invalid_provider_config" "Provider user YAML or credential is invalid" @{
            configPath = $configPath
            outputTail = (($configProbe.output | Select-Object -Last 12) -join "`n")
        }
    }
    try {
        $providerConfig = $configProbe.output[-1] | ConvertFrom-Json
    }
    catch {
        Throw-InstallError "invalid_provider_config" "Provider config probe returned invalid JSON" @{
            configPath = $configPath
        }
    }

    if ([System.IO.File]::Exists($providerPath)) {
        [System.IO.File]::Replace(
            $downloadPath,
            $providerPath,
            "$providerPath.install.bak",
            $true
        )
    }
    else {
        [System.IO.File]::Move($downloadPath, $providerPath)
    }

    $pythonw = [System.IO.Path]::Combine(
        [System.IO.Path]::GetDirectoryName($python),
        "pythonw.exe"
    )
    if (-not [System.IO.File]::Exists($pythonw)) {
        $pythonw = $python
    }
    $processInfo = [System.Diagnostics.ProcessStartInfo]::new()
    $processInfo.FileName = $pythonw
    $processInfo.Arguments = "`"$providerPath`""
    $processInfo.WorkingDirectory = $providerRoot
    $processInfo.UseShellExecute = $true
    $child = [System.Diagnostics.Process]::Start($processInfo)
    if ($null -eq $child) {
        Throw-InstallError "provider_start_failed" "Windows Provider process did not start"
    }

    [ordered]@{
        ok = $true
        operation = "unideskcprovider.install"
        providerId = "$($providerConfig.id)"
        version = "$($manifest.latestVersion)"
        configPath = $configPath
        providerPath = $providerPath
        credentialPresent = $true
        artifactBytes = $providerBytes.LongLength
        artifactSha256 = $digest
        python = $python
        pyYamlInstalled = $pyYamlInstalled
        autoStart = [bool]$providerConfig.autoStart
        startedPid = $child.Id
    } | ConvertTo-Json -Compress
}
catch {
    if ($downloadPath -and [System.IO.File]::Exists($downloadPath)) {
        [System.IO.File]::Delete($downloadPath)
    }
    if ($configProbePath -and [System.IO.File]::Exists($configProbePath)) {
        [System.IO.File]::Delete($configProbePath)
    }
    $errorCode = "install_failed"
    $detail = $null
    if ($_.Exception.Data.Contains("errorCode")) {
        $errorCode = "$($_.Exception.Data["errorCode"])"
    }
    if ($_.Exception.Data.Contains("detail")) {
        $detail = $_.Exception.Data["detail"]
    }
    [Console]::Error.WriteLine(
        ([ordered]@{
            ok = $false
            operation = "unideskcprovider.install"
            errorCode = $errorCode
            message = $_.Exception.Message
            detail = $detail
        } | ConvertTo-Json -Compress -Depth 6)
    )
    exit 1
}
