param(
    [string]$InstallDir = "$env:USERPROFILE\AgentOrchestrator",
    [string]$ManifestUrl = "https://www.hetphoenixproject.nl/AgentOrchestrator/manifest.json",
    [switch]$SkipComponentInstall,
    [switch]$CleanInstall,
    [switch]$Unattended
)

$ErrorActionPreference = 'Stop'
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

# ── Visual helpers ───────────────────────────────────────────────────────────

$_stepNum = 0
$_totalSteps = 12

function Write-Step($msg) {
    $script:_stepNum++
    $pct = [math]::Min([int](($script:_stepNum / $script:_totalSteps) * 100), 100)
    $filled = [math]::Min([math]::Floor($pct / 5), 20)
    $empty  = 20 - $filled
    Write-Host ""
    Write-Host "  [$script:_stepNum/$script:_totalSteps]  $msg" -ForegroundColor Cyan
    Write-Host ("  " + ([string]::new([char]0x2588, $filled)) + ([string]::new([char]0x2591, $empty)) + "  ${pct}%") -ForegroundColor DarkCyan
    Write-Host ""
}

function Write-Ok($msg)   { Write-Host "  [OK]  $msg" -ForegroundColor Green }
function Write-Warn($msg) { Write-Host "  [WARN]  $msg" -ForegroundColor Yellow }
function Write-Err($msg)  { Write-Host "  [FAIL]  $msg" -ForegroundColor Red }

Write-Host ""
Write-Host "========================================================" -ForegroundColor Cyan
Write-Host "   AgentOrchestrator -- Online Installer / Updater        " -ForegroundColor Cyan
Write-Host "========================================================" -ForegroundColor Cyan
Write-Host ""

function Get-Sha256Hex([string]$Path) {
    return (Get-FileHash -Path $Path -Algorithm SHA256).Hash.ToLower()
}

function Test-Cmd($name) {
    return [bool](Get-Command $name -ErrorAction SilentlyContinue)
}

function Assert-DirectoryWritable([string]$PathToTest) {
    $probe = Join-Path $PathToTest (".write_test_" + [guid]::NewGuid().ToString('N') + ".tmp")
    try {
        Set-Content -Path $probe -Value "ok" -Encoding UTF8 -ErrorAction Stop
        Remove-Item -Path $probe -Force -ErrorAction SilentlyContinue
    } catch {
        throw "No write permission in '$PathToTest'. Run PowerShell as a user with write access or choose another install location."
    }
}

function Get-PythonVersionTuple([string]$PythonExe) {
    $out = & $PythonExe -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
    $parts = ([string]$out).Trim().Split('.')
    if ($parts.Count -lt 2) { return @{ major = 0; minor = 0; text = '0.0' } }
    return @{
        major = [int]$parts[0]
        minor = [int]$parts[1]
        text  = "{0}.{1}" -f [int]$parts[0], [int]$parts[1]
    }
}

function Assert-PortAvailable([int]$Port) {
    # Prefer process-aware detection when available.
    if (Get-Command Get-NetTCPConnection -ErrorAction SilentlyContinue) {
        try {
            $conn = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction Stop | Select-Object -First 1
            if ($conn) {
                $proc = Get-Process -Id $conn.OwningProcess -ErrorAction SilentlyContinue
                $pname = if ($proc) { $proc.ProcessName } else { 'unknown' }
                throw "Port $Port is already in use by process '$pname' (PID $($conn.OwningProcess)). Close that process and re-run the installer."
            }
        } catch {
            if ($_.Exception.Message -match '^Port\s+\d+\s+is already in use') { throw }
        }
    }

    # Fallback bind test.
    $listener = $null
    try {
        $ip = [System.Net.IPAddress]::Parse('127.0.0.1')
        $listener = [System.Net.Sockets.TcpListener]::new($ip, $Port)
        $listener.Start()
    } catch {
        throw "Port $Port is not available. Free the port and re-run the installer."
    } finally {
        if ($listener) {
            try { $listener.Stop() } catch {}
        }
    }
}

function Apply-BackendCompatibilityHotfix([string]$BackendFile) {
    if (-not (Test-Path $BackendFile)) { return $false }
    $raw = Get-Content -Path $BackendFile -Raw
    $updated = $raw

    # Python 3.11 compatibility: avoid backslash escapes inside f-string expressions.
    $updated = $updated.Replace("item.Location or '\u2014'", "item.Location or '-'")
    $updated = $updated.Replace("item.Location or '$([char]0x2014)'", "item.Location or '-'")
    # Fallback: normalize the full Location f-string regardless of the original fallback literal.
    $updated = [regex]::Replace(
        $updated,
        'f"Location:\s*\{item\.Location\s+or\s+[^}]+\}\\n"',
        'f"Location: {item.Location or ''-''}\\n"'
    )

    if ($updated -ne $raw) {
        Set-Content -Path $BackendFile -Value $updated -Encoding UTF8
        return $true
    }
    return $false
}

function Test-BackendSyntax([string]$PythonExe, [string]$RootPath) {
    if (-not (Test-Path $PythonExe)) { return $false }
    $old = Get-Location
    try {
        Set-Location $RootPath
        & $PythonExe -c "import py_compile; py_compile.compile('backend/main.py', doraise=True)"
        return ($LASTEXITCODE -eq 0)
    } catch {
        return $false
    } finally {
        Set-Location $old
    }
}

function Stop-AgentProcesses([string]$RootPath) {
    $normalizedRoot = [System.IO.Path]::GetFullPath($RootPath).ToLower()
    $venvPython = [System.IO.Path]::GetFullPath((Join-Path $RootPath ".venv\Scripts\python.exe")).ToLower()
    $venvPythonw = [System.IO.Path]::GetFullPath((Join-Path $RootPath ".venv\Scripts\pythonw.exe")).ToLower()
    $killed = 0

    function Stop-AgentListener([int]$Port, [string[]]$Needles) {
        if (-not (Get-Command Get-NetTCPConnection -ErrorAction SilentlyContinue)) { return 0 }
        $stopped = 0
        try {
            $listeners = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction Stop
        } catch {
            return 0
        }
        foreach ($listener in $listeners) {
            $proc = Get-CimInstance Win32_Process -Filter "ProcessId = $($listener.OwningProcess)" -ErrorAction SilentlyContinue
            if (-not $proc) { continue }
            $name = ([string]$proc.Name).ToLower()
            if ($name -ne 'python.exe' -and $name -ne 'pythonw.exe') { continue }
            $cmd = ([string]$proc.CommandLine).ToLower()
            if ([string]::IsNullOrWhiteSpace($cmd)) { continue }
            $matchesNeedle = $false
            foreach ($needle in $Needles) {
                if ($cmd.Contains($needle.ToLower())) {
                    $matchesNeedle = $true
                    break
                }
            }
            if (-not $matchesNeedle) { continue }
            try {
                Stop-Process -Id $listener.OwningProcess -Force -ErrorAction Stop
                $stopped++
            } catch {
                # Ignore races where a process exits between list and kill.
            }
        }
        return $stopped
    }

    # Only stop Python processes that are clearly tied to this install root.
    $py = Get-CimInstance Win32_Process -Filter "Name='python.exe' OR Name='pythonw.exe'" -ErrorAction SilentlyContinue
    foreach ($p in $py) {
        $cmd = [string]$p.CommandLine
        $cmdLower = $cmd.ToLower()
        $exe = [string]$p.ExecutablePath
        $exeLower = $exe.ToLower()
        $matchesRoot = (-not [string]::IsNullOrWhiteSpace($cmd) -and $cmdLower.Contains($normalizedRoot))
        $matchesVenvExe = (-not [string]::IsNullOrWhiteSpace($exe) -and ($exeLower -eq $venvPython -or $exeLower -eq $venvPythonw))
        if ($matchesRoot -or $matchesVenvExe) {
            try {
                Stop-Process -Id $p.ProcessId -Force -ErrorAction Stop
                $killed++
            } catch {
                # Ignore races where a process exits between list and kill.
            }
        }
    }

    $killed += Stop-AgentListener -Port 8001 -Needles @('launcher_server.py')
    $killed += Stop-AgentListener -Port 8000 -Needles @('backend.main:app', '-m uvicorn')

    if ($killed -gt 0) {
        Write-Step "Server reset: stopped $killed running AgentOrchestrator Python process(es)."
        Start-Sleep -Seconds 1
    } else {
        Write-Step "Server reset: no running AgentOrchestrator Python process found."
    }
}

# ── Pre-flight: check port availability before downloading anything ──────────
function Test-PortFree([int]$Port) {
    if (Get-Command Get-NetTCPConnection -ErrorAction SilentlyContinue) {
        try {
            $conn = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction Stop | Select-Object -First 1
            if ($conn) {
                $proc = Get-Process -Id $conn.OwningProcess -ErrorAction SilentlyContinue
                $pname = if ($proc) { $proc.ProcessName } else { 'unknown' }
                Write-Warn "Port $Port is in use by $pname (PID $($conn.OwningProcess))."
                Write-Host "  An existing AgentOrchestrator instance will be stopped during install." -ForegroundColor DarkGray
            }
        } catch { }
    }
}

Test-PortFree -Port 8001
Test-PortFree -Port 8000

$cacheBust = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
$manifestUri = "${ManifestUrl}?ts=${cacheBust}"
Write-Step "Checking for latest version"
$manifestRaw = Invoke-WebRequest -Uri $manifestUri -UseBasicParsing -Method Get
if (-not $manifestRaw -or -not $manifestRaw.Content) { throw "Manifest response is empty." }
$manifestText = $manifestRaw.Content.TrimStart([char]0xFEFF, [char]0xFFFE)
$manifest = $manifestText | ConvertFrom-Json

$latestVersion = [string]$manifest.latest_version
$downloadUrl = [string]$manifest.download_url
$expectedSha = [string]$manifest.sha256

if ([string]::IsNullOrWhiteSpace($latestVersion)) { throw "Manifest is missing latest_version." }
if ([string]::IsNullOrWhiteSpace($downloadUrl)) { throw "Manifest is missing download_url." }

Write-Step "Latest version: $latestVersion"
Write-Step "Download URL: $downloadUrl"

$tmpRoot = Join-Path $env:TEMP ("AgentOrchestrator-install-" + [guid]::NewGuid().ToString("N"))
$tmpZip = Join-Path $tmpRoot "package.zip"
$tmpExtract = Join-Path $tmpRoot "extract"
New-Item -ItemType Directory -Path $tmpExtract -Force | Out-Null

try {
        $downloadUri = "${downloadUrl}?ts=${cacheBust}"
    Write-Step "Downloading package"
    Write-Host "  URL: $downloadUrl" -ForegroundColor DarkGray
    Write-Host "  This may take a minute depending on your connection..." -ForegroundColor DarkGray
    Invoke-WebRequest -Uri $downloadUri -OutFile $tmpZip

    if (-not [string]::IsNullOrWhiteSpace($expectedSha)) {
        $actualSha = Get-Sha256Hex $tmpZip
        if ($actualSha -ne $expectedSha.ToLower()) {
            throw "Checksum mismatch. Expected: $expectedSha, got: $actualSha"
        }
        Write-Step "Checksum verified."
    }

    Write-Step "Extracting package"
    Expand-Archive -Path $tmpZip -DestinationPath $tmpExtract -Force

    $entries = Get-ChildItem -Path $tmpExtract
    if ($entries.Count -eq 1 -and $entries[0].PSIsContainer) {
        $sourceRoot = $entries[0].FullName
    } else {
        $sourceRoot = $tmpExtract
    }

    if (-not (Test-Path $InstallDir)) {
        Write-Step "Creating install directory: $InstallDir"
        New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
    }

    Assert-DirectoryWritable -PathToTest $InstallDir

    Write-Step "Stopping running instances"
    Stop-AgentProcesses -RootPath $InstallDir

    # Determine whether to preserve existing settings.
    $preserveSettings = $true
    $existingSettings = Join-Path $InstallDir "settings.json"
    if ($CleanInstall) {
        $preserveSettings = $false
        Write-Warn "Clean install -- existing settings will be overwritten."
    } elseif (Test-Path $existingSettings) {
        if ($Unattended) {
            # Unattended mode: always keep existing settings (safe default)
            Write-Ok "Keeping existing settings (unattended mode)."
        } else {
            Write-Host ""
            Write-Host "  Existing settings detected in $InstallDir" -ForegroundColor Yellow
            Write-Host "  [K] Keep existing settings (default)" -ForegroundColor Green
            Write-Host "  [C] Clean install (reset all settings)" -ForegroundColor Red
            $choice = Read-Host "  Choose [K/c]"
            if ($choice -match '^[Cc]$') {
                $preserveSettings = $false
                Write-Warn "Clean install selected -- existing settings will be overwritten."
            } else {
                Write-Ok "Keeping existing settings."
            }
        }
    }

    # Preserve local user/runtime data if present.
    $protectedExact = @(
        "settings.json",
        "google_token.json",
        "schedules.json",
        "soul.md",
        "identity.md",
        "user.md",
        ".auth_token"
    )
    $protectedPrefixes = @("memory\\", "output\\", "downloads\\", "attachments\\")

    Write-Step "Installing files"
    Write-Host "  Target: $InstallDir" -ForegroundColor DarkGray
    Get-ChildItem -Path $sourceRoot -Recurse | ForEach-Object {
        $src = $_.FullName
        $rel = $src.Substring($sourceRoot.Length).TrimStart([char[]]@([char]'\', [char]'/'))
        if ([string]::IsNullOrWhiteSpace($rel)) { return }

        $relNorm = $rel.Replace('/', '\\')
        $lower = $relNorm.ToLower()

        $isProtected = $false
        if ($preserveSettings) {
            if ($protectedExact -contains $lower) { $isProtected = $true }
            foreach($prefix in $protectedPrefixes) {
                if ($lower.StartsWith($prefix)) { $isProtected = $true; break }
            }
            if ($isProtected) {
                $dst = Join-Path $InstallDir $relNorm
                if (Test-Path $dst) { return }
            }
        }

        $dst = Join-Path $InstallDir $relNorm
        if ($_.PSIsContainer) {
            New-Item -ItemType Directory -Path $dst -Force | Out-Null
        } else {
            $dstDir = Split-Path -Parent $dst
            if (-not (Test-Path $dstDir)) {
                New-Item -ItemType Directory -Path $dstDir -Force | Out-Null
            }
            Copy-Item -Path $src -Destination $dst -Force
        }
    }

    $installedBackend = Join-Path $InstallDir "backend\main.py"
    $patched = Apply-BackendCompatibilityHotfix -BackendFile $installedBackend
    if ($patched) {
        Write-Step "Applied backend compatibility hotfixes."
    }

    $launcher = Join-Path $InstallDir "launch_agent.bat"
    if (Test-Path $launcher) {
        Write-Step "Installed to: $InstallDir"
        if (-not $SkipComponentInstall) {
            $installer = Join-Path $InstallDir "install_windows.ps1"
            if (Test-Path $installer) {
                Write-Step "Running component installer..."
                powershell -ExecutionPolicy Bypass -File $installer -ProjectRoot $InstallDir
            }
        }

        $venvPy = Join-Path $InstallDir ".venv\Scripts\python.exe"
        if (Test-Path $venvPy) {
            $pyVer = Get-PythonVersionTuple -PythonExe $venvPy
            if ($pyVer.major -lt 3 -or ($pyVer.major -eq 3 -and $pyVer.minor -lt 11)) {
                throw "Python 3.11+ is required, but venv uses $($pyVer.text)."
            }
            if (-not (Test-BackendSyntax -PythonExe $venvPy -RootPath $InstallDir)) {
                Write-Step "Backend syntax check failed, applying repair and re-validating..."
                $null = Apply-BackendCompatibilityHotfix -BackendFile $installedBackend
                if (-not (Test-BackendSyntax -PythonExe $venvPy -RootPath $InstallDir)) {
                    throw "Backend syntax validation failed after automatic repair. Install aborted before launch."
                }
            }
        }

        # Create Desktop and Start Menu shortcuts
        $shortcutName = "AgentOrchestrator.lnk"
        $description  = "Start AgentOrchestrator server and open the app"
        try {
            $shell = New-Object -ComObject WScript.Shell

            # Desktop shortcut
            $desktopPath = [Environment]::GetFolderPath('Desktop')
            if ($desktopPath) {
                $desktopLnk = Join-Path $desktopPath $shortcutName
                $lnk = $shell.CreateShortcut($desktopLnk)
                $lnk.TargetPath       = $launcher
                $lnk.WorkingDirectory = $InstallDir
                $lnk.Description      = $description
                $lnk.Save()
                Write-Ok "Desktop shortcut created."
            }

            # Start Menu shortcut
            $startMenuDir = Join-Path ([Environment]::GetFolderPath('Programs')) "AgentOrchestrator"
            if (-not (Test-Path $startMenuDir)) { New-Item -ItemType Directory -Path $startMenuDir -Force | Out-Null }
            $startLnk = Join-Path $startMenuDir $shortcutName
            $lnk2 = $shell.CreateShortcut($startLnk)
            $lnk2.TargetPath       = $launcher
            $lnk2.WorkingDirectory = $InstallDir
            $lnk2.Description      = $description
            $lnk2.Save()
            Write-Ok "Start Menu shortcut created."

            [System.Runtime.InteropServices.Marshal]::ReleaseComObject($shell) | Out-Null
        } catch {
            Write-Warn "Could not create shortcuts: $_"
        }

        Write-Host "" -ForegroundColor DarkGray
        Write-Host "  Launching app..." -ForegroundColor Cyan
        Start-Process -FilePath $launcher -WorkingDirectory $InstallDir | Out-Null

        Write-Host ""
        Write-Host "========================================================" -ForegroundColor Green
        Write-Host "   Installation complete!                                " -ForegroundColor Green
        Write-Host "========================================================" -ForegroundColor Green
        Write-Host ""
        Write-Host "  AgentOrchestrator has been installed to:" -ForegroundColor White
        Write-Host "    $InstallDir" -ForegroundColor White
        Write-Host ""
        Write-Host "  The app should open in your browser automatically." -ForegroundColor DarkGray
        Write-Host "  If not, open: http://127.0.0.1:8001" -ForegroundColor DarkGray
        Write-Host ""
    } else {
        throw "Install appears incomplete: launch_agent.bat not found in $InstallDir"
    }
}
finally {
    if (Test-Path $tmpRoot) {
        Remove-Item -Path $tmpRoot -Recurse -Force -ErrorAction SilentlyContinue
    }
}
