PoshDE.psm1

# PoshDE - PowerShell Desktop Environment
# https://github.com/Jakoby/PoshDE

$script:ModuleRoot = $PSScriptRoot
$script:Version    = '1.0.0'

# ============================================================================
# Global State
# ============================================================================
$script:PoshDE = @{
    CurrentTheme    = 'samurai'
    WebView2Ready   = $false
    WebView2DllPath = $null
    AppDataPath     = Join-Path $env:APPDATA "PoshDE"
}

# Ensure app data directory exists
if (-not (Test-Path $script:PoshDE.AppDataPath)) {
    New-Item -Path $script:PoshDE.AppDataPath -ItemType Directory -Force | Out-Null
}

# Load persisted config
$configPath = Join-Path $script:PoshDE.AppDataPath "config.json"
if (Test-Path $configPath) {
    try {
        $config = Get-Content $configPath -Raw | ConvertFrom-Json
        if ($config.CurrentTheme) { $script:PoshDE.CurrentTheme = $config.CurrentTheme }
    } catch { }
}

# ============================================================================
# Load Core Components
# ============================================================================
. "$script:ModuleRoot\Core\DependencyManager.ps1"
. "$script:ModuleRoot\Core\ThemeEngine.ps1"
. "$script:ModuleRoot\Core\PortManager.ps1"
. "$script:ModuleRoot\Core\ModuleTracker.ps1"
. "$script:ModuleRoot\Core\WindowManager.ps1"

# Check WebView2 on load
$script:PoshDE.WebView2Ready = Test-WebView2Installed

# ============================================================================
# Banner
# ============================================================================
function Show-PoshDEBanner {
    Write-Host ""
    Write-Host " PoshDE v$script:Version" -ForegroundColor Magenta
    Write-Host " PowerShell Desktop Environment" -ForegroundColor DarkGray
    Write-Host " ─────────────────────────────────────" -ForegroundColor DarkGray

    if ($script:PoshDE.WebView2Ready) {
        Write-Host " [+] WebView2 " -NoNewline -ForegroundColor Green
        Write-Host "Ready" -ForegroundColor Cyan
    } else {
        Write-Host " [-] WebView2 " -NoNewline -ForegroundColor Red
        Write-Host "Not installed — run " -NoNewline -ForegroundColor Yellow
        Write-Host "Install-PoshDependencies" -ForegroundColor Cyan
    }

    Write-Host " [*] Theme " -NoNewline -ForegroundColor DarkGray
    Write-Host $script:PoshDE.CurrentTheme -ForegroundColor Cyan

    $modules      = Get-PoshModules
    $installedCnt = @($modules | Where-Object Installed).Count
    Write-Host " [*] Modules " -NoNewline -ForegroundColor DarkGray
    Write-Host "$installedCnt / $($modules.Count) installed" -ForegroundColor Cyan

    $activePorts = @($script:PortConfig.Registry.Keys)
    if ($activePorts.Count -gt 0) {
        $running = 0
        foreach ($p in $activePorts) {
            $e = $script:PortConfig.Registry[$p]
            if ($e.PID -gt 0) {
                $proc = Get-Process -Id $e.PID -ErrorAction SilentlyContinue
                if ($proc -and -not $proc.HasExited) { $running++ }
            }
        }
        Write-Host " [*] Apps " -NoNewline -ForegroundColor DarkGray
        Write-Host "$running running" -ForegroundColor Cyan
    }

    Write-Host ""
}

Show-PoshDEBanner

# ============================================================================
# GUI — Open-PoshDE / Stop-PoshDE
# ============================================================================

$script:PoshDE_ActivePort        = $null
$script:PoshDE_WindowProcess     = $null
$script:PoshDE_ServerRunspace    = $null
$script:PoshDE_ServerPowerShell  = $null
$script:PoshDE_ServerAsyncResult = $null
$script:PoshDE_SharedState       = $null

$script:PoshDE_ServerScript = {
    param(
        [int]       $Port,
        [string]    $PoshDEPath,
        [hashtable] $SharedState
    )

    Import-Module $PoshDEPath -ErrorAction Stop

    # ── Response helpers ─────────────────────────────────────────────────────

    function Send-Json {
        param($Ctx, $Data, [int]$Code = 200)
        $json = $Data | ConvertTo-Json -Depth 10 -Compress
        $buf  = [System.Text.Encoding]::UTF8.GetBytes($json)
        $Ctx.Response.StatusCode      = $Code
        $Ctx.Response.ContentType     = 'application/json; charset=utf-8'
        $Ctx.Response.ContentLength64 = $buf.Length
        $Ctx.Response.OutputStream.Write($buf, 0, $buf.Length)
        $Ctx.Response.Close()
    }

    function Send-File {
        param($Ctx, [string]$Path, [string]$ContentType)
        if (Test-Path $Path) {
            $bytes = [System.IO.File]::ReadAllBytes($Path)
            $Ctx.Response.StatusCode      = 200
            $Ctx.Response.ContentType     = $ContentType
            $Ctx.Response.ContentLength64 = $bytes.Length
            $Ctx.Response.OutputStream.Write($bytes, 0, $bytes.Length)
        } else {
            $buf = [System.Text.Encoding]::UTF8.GetBytes("Not Found: $Path")
            $Ctx.Response.StatusCode      = 404
            $Ctx.Response.ContentLength64 = $buf.Length
            $Ctx.Response.OutputStream.Write($buf, 0, $buf.Length)
        }
        $Ctx.Response.Close()
    }

    function Read-JsonBody {
        param($Req)
        $reader = New-Object System.IO.StreamReader($Req.InputStream)
        $body   = $reader.ReadToEnd(); $reader.Close()
        return $body | ConvertFrom-Json
    }

    # ── Data helpers ─────────────────────────────────────────────────────────

    function Get-ModuleList {
        $list = [System.Collections.Generic.List[object]]::new()
        foreach ($mod in $SharedState.KnownModules) {
            $found     = Get-Module -ListAvailable -Name $mod.Name -ErrorAction SilentlyContinue
            $installed = $null -ne $found
            $version   = if ($installed) {
                ($found | Sort-Object Version -Descending | Select-Object -First 1).Version.ToString()
            } else { $null }
            $list.Add(@{
                name        = $mod.Name
                description = $mod.Description
                repo        = $mod.Repo
                installed   = $installed
                version     = $version
            })
        }
        return @($list)
    }

    function Get-PortList {
        $path = $SharedState.PortRegistryPath
        if (-not (Test-Path $path)) { return @() }
        try {
            $json = Get-Content $path -Raw | ConvertFrom-Json
        } catch { return @() }
        $list = [System.Collections.Generic.List[object]]::new()
        foreach ($prop in $json.PSObject.Properties) {
            $portNum = [int]$prop.Name
            $entry   = $prop.Value
            $status  = 'unknown'
            if ($entry.PID -gt 0) {
                $proc   = Get-Process -Id $entry.PID -ErrorAction SilentlyContinue
                $status = if ($proc -and -not $proc.HasExited) { 'running' } else { 'dead' }
            } else {
                $status = 'starting'
            }
            $started = try { ([datetime]$entry.Started).ToString('h:mm tt') } catch { '—' }
            $list.Add(@{
                port    = $portNum
                app     = $entry.App
                pid     = $entry.PID
                status  = $status
                started = $started
            })
        }
        return @($list | Sort-Object { $_.port })
    }

    function Get-ThemeList {
        $names = @('samurai','cyberpunk','matrix','midnight','hacker','default')
        $list  = [System.Collections.Generic.List[object]]::new()
        foreach ($name in $names) {
            $t = Get-PoshTheme -Name $name
            $list.Add(@{
                name        = $name
                description = $t.description
                primary     = $t.colors.primary
                secondary   = $t.colors.secondary
                accent      = $t.colors.accent
                bgDark      = $t.colors.bgDark
                bgMedium    = $t.colors.bgMedium
            })
        }
        return @($list)
    }

    # Map of module name -> launch scriptblock
    $LaunchMap = @{
        'PoshWallpaper'  = { Open-PoshWallpaper }
        'PoshConsole'    = { Start-PoshConsole }
        'PoshPresenter'  = { Start-PoshDemo }
    }

    function Invoke-LaunchApp {
        param([string]$ModuleName)

        $openCmd = $LaunchMap[$ModuleName]
        if (-not $openCmd) {
            return @{ success=$false; error="No launch command defined for $ModuleName" }
        }

        # Check if already running via port registry
        $path = $SharedState.PortRegistryPath
        if (Test-Path $path) {
            try {
                $json = Get-Content $path -Raw | ConvertFrom-Json
                foreach ($prop in $json.PSObject.Properties) {
                    if ($prop.Value.App -eq $ModuleName) {
                        $pid = $prop.Value.PID
                        if ($pid -gt 0) {
                            $proc = Get-Process -Id $pid -ErrorAction SilentlyContinue
                            if ($proc -and -not $proc.HasExited) {
                                return @{ success=$false; error="$ModuleName is already running" }
                            }
                        }
                    }
                }
            } catch { }
        }

        try {
            Import-Module $ModuleName -ErrorAction Stop
            & $openCmd
            return @{ success=$true; module=$ModuleName }
        } catch {
            return @{ success=$false; error=$_.Exception.Message }
        }
    }

    function Stop-AppByPort {
        param([int]$TargetPort)
        $path = $SharedState.PortRegistryPath
        if (-not (Test-Path $path)) { return @{ success=$false; error='Registry not found' } }
        try { $json = Get-Content $path -Raw | ConvertFrom-Json } catch { return @{ success=$false; error='Could not read registry' } }

        $entry = $json.PSObject.Properties | Where-Object { [int]$_.Name -eq $TargetPort } | Select-Object -First 1
        if (-not $entry) { return @{ success=$false; error="Port $TargetPort not in registry" } }

        if ($entry.Value.PID -gt 0) {
            $proc = Get-Process -Id $entry.Value.PID -ErrorAction SilentlyContinue
            if ($proc -and -not $proc.HasExited) { try { $proc.Kill(); $proc.WaitForExit(3000) } catch { } }
        }

        # Rewrite registry without this entry
        $newReg = @{}
        foreach ($p in $json.PSObject.Properties) {
            if ([int]$p.Name -ne $TargetPort) { $newReg[$p.Name] = $p.Value }
        }
        try { $newReg | ConvertTo-Json -Depth 5 | Set-Content $path -Encoding UTF8 } catch { }
        return @{ success=$true }
    }

    # ── HTTP Listener ─────────────────────────────────────────────────────────

    $listener = New-Object System.Net.HttpListener
    $listener.Prefixes.Add("http://localhost:$Port/")
    $listener.Prefixes.Add("http://127.0.0.1:$Port/")
    $listener.Start()
    $SharedState.Ready = $true

    try {
        while ($SharedState.Running) {
            $task = $listener.GetContextAsync()
            while (-not $task.Wait(500)) { if (-not $SharedState.Running) { break } }
            if (-not $SharedState.Running) { break }
            if (-not $task.IsCompleted -or $task.IsFaulted -or $task.IsCanceled) { continue }

            $ctx     = $task.Result
            $req     = $ctx.Request
            $urlPath = $req.Url.LocalPath

            $ctx.Response.Headers.Add('Access-Control-Allow-Origin',  '*')
            $ctx.Response.Headers.Add('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
            $ctx.Response.Headers.Add('Access-Control-Allow-Headers', 'Content-Type')

            try {
                if ($req.HttpMethod -eq 'OPTIONS') {
                    $ctx.Response.StatusCode = 200; $ctx.Response.Close(); continue
                }

                switch -Regex ($urlPath) {
                    '^/$|^/index\.html$' {
                        Send-File $ctx (Join-Path $SharedState.ModuleRoot 'index.html') 'text/html; charset=utf-8'
                    }
                    '^/api/theme$' {
                        $t = Get-PoshTheme
                        Send-Json $ctx @{ name=$t.name; css=(Export-PoshThemeCSS); colors=$t.colors }
                    }
                    '^/api/themes$' {
                        Send-Json $ctx @{ themes = @(Get-ThemeList) }
                    }
                    '^/api/modules$' {
                        Send-Json $ctx @{ modules = @(Get-ModuleList) }
                    }
                    '^/api/ports$' {
                        Send-Json $ctx @{ ports = @(Get-PortList) }
                    }
                    '^/api/theme/set$' {
                        if ($req.HttpMethod -ne 'POST') { Send-Json $ctx @{ error='POST required' } -Code 405; break }
                        $body = Read-JsonBody $req
                        $name = [string]$body.name
                        Set-PoshTheme -Name $name
                        Send-Json $ctx @{ success=$true; theme=$name }
                    }
                    '^/api/app/stop$' {
                        if ($req.HttpMethod -ne 'POST') { Send-Json $ctx @{ error='POST required' } -Code 405; break }
                        $body   = Read-JsonBody $req
                        $result = Stop-AppByPort -TargetPort ([int]$body.port)
                        Send-Json $ctx $result
                    }
                    '^/api/app/launch$' {
                        if ($req.HttpMethod -ne 'POST') { Send-Json $ctx @{ error='POST required' } -Code 405; break }
                        $body   = Read-JsonBody $req
                        $result = Invoke-LaunchApp -ModuleName ([string]$body.module)
                        Send-Json $ctx $result
                    }
                    default {
                        Send-Json $ctx @{ error="Not found: $urlPath" } -Code 404
                    }
                }
            } catch {
                $msg = $_.Exception.Message
                try {
                    $buf = [System.Text.Encoding]::UTF8.GetBytes("{`"error`":`"$msg`"}")
                    $ctx.Response.StatusCode      = 500
                    $ctx.Response.ContentType     = 'application/json'
                    $ctx.Response.ContentLength64 = $buf.Length
                    $ctx.Response.OutputStream.Write($buf, 0, $buf.Length)
                } catch { }
                try { $ctx.Response.Close() } catch { }
            }
        }
    } finally {
        $listener.Stop()
        $listener.Close()
    }
}

function Open-PoshDE {
    <#
    .SYNOPSIS
        Open the PoshDE dashboard GUI.
    .DESCRIPTION
        Launches a WebView2 window showing module status, running apps,
        theme switcher, and port registry.
    .PARAMETER NoWindow
        Start the HTTP server only without opening a window.
    .EXAMPLE
        Open-PoshDE
    #>

    [CmdletBinding()]
    param([switch]$NoWindow)

    if ($script:PoshDE_SharedState -and $script:PoshDE_SharedState.Running) {
        Write-Host " PoshDE GUI already running on port $script:PoshDE_ActivePort" -ForegroundColor Yellow
        return
    }

    $dllPath = Get-WebView2DllPath
    if (-not $dllPath) { Write-Error "WebView2 not ready. Run Install-PoshDependencies."; return }

    $port = Register-PoshPort -App 'PoshDE'
    if (-not $port) { Write-Error "Could not allocate port."; return }
    $script:PoshDE_ActivePort = $port

    $script:PoshDE_SharedState = [hashtable]::Synchronized(@{
        Running          = $true
        Ready            = $false
        ModuleRoot       = $script:ModuleRoot
        PortRegistryPath = Join-Path $script:PoshDE.AppDataPath 'port-registry.json'
        ConfigPath       = Join-Path $script:PoshDE.AppDataPath 'config.json'
        KnownModules     = $script:PoshKnownModules
    })

    $script:PoshDE_ServerRunspace = [runspacefactory]::CreateRunspace()
    $script:PoshDE_ServerRunspace.ApartmentState = 'STA'
    $script:PoshDE_ServerRunspace.ThreadOptions  = 'ReuseThread'
    $script:PoshDE_ServerRunspace.Open()

    $script:PoshDE_ServerPowerShell = [PowerShell]::Create()
    $script:PoshDE_ServerPowerShell.Runspace = $script:PoshDE_ServerRunspace
    $script:PoshDE_ServerPowerShell.AddScript($script:PoshDE_ServerScript)                             | Out-Null
    $script:PoshDE_ServerPowerShell.AddParameter('Port',        $port)                                 | Out-Null
    $script:PoshDE_ServerPowerShell.AddParameter('PoshDEPath',  (Get-Module PoshDE).ModuleBase)        | Out-Null
    $script:PoshDE_ServerPowerShell.AddParameter('SharedState', $script:PoshDE_SharedState)            | Out-Null
    $script:PoshDE_ServerAsyncResult = $script:PoshDE_ServerPowerShell.BeginInvoke()

    $waited = 0
    while (-not $script:PoshDE_SharedState.Ready -and $waited -lt 5000) {
        Start-Sleep -Milliseconds 100; $waited += 100
    }

    Write-Host ""
    Write-Host " ╔══════════════════════════════════════╗" -ForegroundColor Magenta
    Write-Host " ║ 🔱 PoshDE v$script:Version ║" -ForegroundColor Magenta
    Write-Host " ╚══════════════════════════════════════╝" -ForegroundColor Magenta
    Write-Host " [*] Port $port" -ForegroundColor Yellow
    Write-Host " [*] Theme $($script:PoshDE.CurrentTheme)" -ForegroundColor Cyan
    Write-Host ""

    if (-not $NoWindow) {
        $script:PoshDE_WindowProcess = New-PoshWindow -AppName 'PoshDE' -Port $port -Width 1100 -Height 740
        if ($script:PoshDE_WindowProcess) {
            Update-PoshPortPID -Port $port -PID $script:PoshDE_WindowProcess.Id
            Write-Host " [+] Window PID $($script:PoshDE_WindowProcess.Id)" -ForegroundColor Green
        }
    } else {
        Write-Host " [*] Server http://localhost:$port" -ForegroundColor Yellow
    }

    Write-Host ""
    Write-Host " Run Stop-PoshDE to close." -ForegroundColor DarkGray
    Write-Host ""
}

function Stop-PoshDE {
    <#
    .SYNOPSIS
        Close the PoshDE dashboard GUI.
    .EXAMPLE
        Stop-PoshDE
    #>

    [CmdletBinding()]
    param()

    Write-Host " Stopping PoshDE GUI..." -ForegroundColor Yellow

    if ($script:PoshDE_SharedState) { $script:PoshDE_SharedState.Running = $false }

    if ($script:PoshDE_WindowProcess -and -not $script:PoshDE_WindowProcess.HasExited) {
        $script:PoshDE_WindowProcess.Kill()
        $script:PoshDE_WindowProcess.WaitForExit(3000)
    }

    if ($script:PoshDE_ServerPowerShell -and $script:PoshDE_ServerAsyncResult) {
        try { $script:PoshDE_ServerPowerShell.EndInvoke($script:PoshDE_ServerAsyncResult) | Out-Null } catch { }
        $script:PoshDE_ServerPowerShell.Dispose()
    }

    if ($script:PoshDE_ServerRunspace) {
        $script:PoshDE_ServerRunspace.Close()
        $script:PoshDE_ServerRunspace.Dispose()
    }

    if ($script:PoshDE_ActivePort) { Unregister-PoshPort -App 'PoshDE' }

    $script:PoshDE_ActivePort        = $null
    $script:PoshDE_WindowProcess     = $null
    $script:PoshDE_ServerRunspace    = $null
    $script:PoshDE_ServerPowerShell  = $null
    $script:PoshDE_ServerAsyncResult = $null
    $script:PoshDE_SharedState       = $null

    Write-Host " Done." -ForegroundColor Green
}

# ============================================================================
# Exports
# ============================================================================
Export-ModuleMember -Function @(
    # Dependencies
    'Install-PoshDependencies'
    'Test-PoshDependencies'
    'Get-PoshDependencyStatus'
    'Get-WebView2DllPath'

    # Themes
    'Get-PoshTheme'
    'Set-PoshTheme'
    'Get-PoshThemes'
    'Export-PoshThemeCSS'

    # Port Management
    'Register-PoshPort'
    'Unregister-PoshPort'
    'Update-PoshPortPID'
    'Get-PoshPortRegistry'
    'Clear-PoshOrphanedPorts'
    'Stop-PoshApp'
    'Stop-AllPoshApps'

    # Module Tracking
    'Get-PoshModules'

    # Window Management
    'New-PoshWindow'

    # GUI
    'Open-PoshDE'
    'Stop-PoshDE'
)