# Copyright (c) 2026 Progralink. Licensed under the DevCage License — see LICENSE.
#
# Utils.ps1 — Utility functions for the DevCage PowerShell launcher.
# Dot-sourced by devcage.ps1. Depends on: $script:AllLangs, $script:BlockedDestinations.

# ── Helper: validate IPv4 address (each octet 0-255) ─────────────
function Test-IPv4Address {
    param([string]$Ip)
    if ($Ip -notmatch '^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$') { return $false }
    $octets = $Ip -split '\.'
    foreach ($o in $octets) {
        if ([int]$o -gt 255) { return $false }
    }
    return $true
}

# ── Normalize workspace path (resolve + verify directory) ────────
function Normalize-WorkspacePath {
    param([string]$RawPath)
    # Expand tilde to user home directory
    if ($RawPath -eq "~") {
        $RawPath = $HOME
    } elseif ($RawPath.StartsWith("~/") -or $RawPath.StartsWith("~\")) {
        $RawPath = Join-Path $HOME $RawPath.Substring(2)
    }
    if (-not [System.IO.Path]::IsPathRooted($RawPath)) {
        $RawPath = Join-Path (Get-Location).Path $RawPath
    }
    $resolved = [System.IO.Path]::GetFullPath($RawPath)
    if (-not (Test-Path -LiteralPath $resolved -PathType Container)) {
        [Console]::Error.WriteLine("Error: workspace directory does not exist: $RawPath")
        exit 1
    }
    return $resolved
}

# ── Validate a custom volume spec (SOURCE:DEST[:MODE]) ───────────
# Returns validated "SOURCE:DEST:MODE" on success; returns $null on failure.
function Test-VolumeSpec {
    param([string]$Spec)
    $mode = "ro"

    # Windows: detect drive-letter prefix (e.g. C:\path) and parse accordingly
    if ($Spec -match '^([a-zA-Z]:\\[^:]*):(/[^:]*):?(.*)$') {
        $src = $Matches[1]
        $dest = $Matches[2]
        if ($Matches[3]) { $mode = $Matches[3] }
    } elseif ($Spec -match '^([^:]+):(/[^:]*):?(.*)$') {
        $src = $Matches[1]
        $dest = $Matches[2]
        if ($Matches[3]) { $mode = $Matches[3] }
    } else {
        Write-Host "Error: invalid volume spec '$Spec' -- expected SOURCE:DEST[:MODE]" -ForegroundColor Red
        return $null
    }

    # Tilde expansion on source
    if ($src -eq "~") {
        $src = $HOME
    } elseif ($src.StartsWith("~/") -or $src.StartsWith("~\")) {
        $src = Join-Path $HOME $src.Substring(2)
    }

    # Resolve source to absolute path
    if (-not [System.IO.Path]::IsPathRooted($src)) {
        $src = Join-Path (Get-Location).Path $src
    }
    $src = [System.IO.Path]::GetFullPath($src)

    # Check source exists
    if (-not (Test-Path -LiteralPath $src)) {
        Write-Host "Error: volume source does not exist: $src" -ForegroundColor Red
        return $null
    }

    # Validate destination is absolute
    if (-not $dest.StartsWith("/")) {
        Write-Host "Error: volume destination must be an absolute path (got '$dest')" -ForegroundColor Red
        return $null
    }

    # Normalize destination: remove trailing slashes and resolve ../ components
    # to prevent path-traversal bypass of blocked destinations.
    $dest = $dest.TrimEnd("/")
    if (-not $dest) { $dest = "/" }
    # Collapse double slashes to prevent bypass of blocked destinations
    while ($dest -match '//') { $dest = $dest -replace '//', '/' }
    if ($dest -match '\.\.' -or $dest -match '/\.(/|$)') {
        $parts = $dest.Split("/")
        $canon = [System.Collections.Generic.List[string]]::new()
        foreach ($seg in $parts) {
            if ($seg -eq "" -or $seg -eq ".") { continue }
            if ($seg -eq "..") {
                if ($canon.Count -gt 0) { $canon.RemoveAt($canon.Count - 1) }
            } else {
                $canon.Add($seg)
            }
        }
        $dest = "/" + ($canon -join "/")
        if ($dest -eq "/") { $dest = "/" }
    }

    # Check against blocked destinations (prefix match)
    foreach ($blocked in $script:BlockedDestinations) {
        if ($dest -eq $blocked -or $dest.StartsWith("$blocked/")) {
            Write-Host "Error: volume destination '$dest' is blocked (conflicts with '$blocked')" -ForegroundColor Red
            return $null
        }
    }

    # Validate mode
    if ($mode -ne "ro" -and $mode -ne "rw") {
        Write-Host "Error: volume mode must be 'ro' or 'rw' (got '$mode')" -ForegroundColor Red
        return $null
    }

    return "${src}:${dest}:${mode}"
}

# ── Language selection helpers ────────────────────────────────────
function Resolve-Lang {
    param([string]$LangName)
    switch ($LangName.ToLower()) {
        "java"          { return "java" }
        "php"           { return "php" }
        { $_ -in "go", "golang" }   { return "go" }
        "rust"          { return "rust" }
        { $_ -in "cpp", "c++", "c" } { return "cpp" }
        { $_ -in "python", "py" }    { return "python" }
        { $_ -in "javascript", "typescript", "js", "ts" } { return "javascript" }
        { $_ -in "dotnet", ".net", "csharp", "c#", "vb", "visualbasic", "f#", "fsharp" } { return "dotnet" }
        default         { return "" }
    }
}

function Get-LangBuildArgs {
    param([string]$LangsCsv)
    $result = @()
    $items = @($LangsCsv -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
    if ($items.Count -eq 1 -and $items[0].ToLower() -eq "all") {
        return $result  # all languages enabled — no DEV_*=false args
    }
    if ($items.Count -eq 1 -and $items[0].ToLower() -eq "none") {
        foreach ($lang in $script:AllLangs) {
            $upper = $lang.ToUpper()
            $result += @("--build-arg", "DEV_${upper}=false")
        }
        return $result
    }
    $enabled = @()
    foreach ($item in $items) {
        $lower = $item.ToLower()
        if ($lower -eq "none" -or $lower -eq "all") {
            Write-Host "Error: '$item' cannot be combined with other languages" -ForegroundColor Red
            exit 1
        }
        $canon = Resolve-Lang $item
        if (-not $canon) {
            Write-Host "Error: unknown language '$item'. Valid: java, php, go, rust, cpp, python, javascript, dotnet (or 'all'/'none')" -ForegroundColor Red
            exit 1
        }
        $enabled += $canon
    }
    foreach ($lang in $script:AllLangs) {
        if ($lang -notin $enabled) {
            $upper = $lang.ToUpper()
            $result += @("--build-arg", "DEV_${upper}=false")
        }
    }
    return $result
}

# ── Folder browse dialog (--path-browse) ─────────────────────────
# Shows a Windows folder picker dialog and returns the selected path.
# On cancel, returns $null. On error, falls back to console prompt.
function Show-BrowseFolder {
    param([string]$InitialDir = "")
    $SelectedPath = $null
    # Try WinForms FolderBrowserDialog
    try {
        Add-Type -AssemblyName System.Windows.Forms
        $dialog = New-Object System.Windows.Forms.FolderBrowserDialog
        $dialog.Description = "Select a project directory to open in DevCage"
        $dialog.ShowNewFolderButton = $true
        if ($InitialDir) { $dialog.SelectedPath = $InitialDir }
        # Create a hidden TopMost form as owner so the dialog appears in foreground
        $owner = New-Object System.Windows.Forms.Form
        $owner.TopMost = $true
        $owner.ShowInTaskbar = $false
        $owner.WindowState = [System.Windows.Forms.FormWindowState]::Minimized
        $result = $dialog.ShowDialog($owner)
        $owner.Dispose()
        if ($result -eq [System.Windows.Forms.DialogResult]::OK) {
            $SelectedPath = $dialog.SelectedPath
        } elseif ($result -eq [System.Windows.Forms.DialogResult]::Cancel) {
            return $null
        }
    } catch {
        # Fallback: COM Shell.Application
        try {
            $shell = New-Object -ComObject Shell.Application
            $folder = $shell.BrowseForFolder(0, "Select a project directory to open in DevCage", 0x0010, 0)
            if ($folder) {
                $SelectedPath = $folder.Self.Path
            } else {
                return $null
            }
        } catch {
            # Final fallback: console prompt
            if ([Environment]::UserInteractive -and -not [Console]::IsInputRedirected) {
                $SelectedPath = Read-Host "Enter project directory path"
                if (-not $SelectedPath) { return $null }
            } else {
                Write-Host "Error: --path-browse requires an interactive session but no GUI or console is available." -ForegroundColor Red
                exit 1
            }
        }
    }
    return $SelectedPath
}

# ── Print to stderr unless in quiet mode ($script:Quiet) ─────────
function Write-Info {
    param([string]$Message = "")
    if (-not $script:Quiet) { [Console]::Error.WriteLine($Message) }
}
