param(
    [switch]$Install,
    [string]$Root = (Join-Path $env:ProgramData 'TibiaPilotNG\DesktopBridge')
)

$ErrorActionPreference = 'Stop'

if ($Install) {
    $target = Join-Path $Root 'Install-DesktopBridge.ps1'
    New-Item $Root -ItemType Directory -Force | Out-Null
    New-Item (Join-Path $Root 'inbox') -ItemType Directory -Force | Out-Null
    New-Item (Join-Path $Root 'outbox') -ItemType Directory -Force | Out-Null
    Copy-Item $PSCommandPath $target -Force

    $admins = ([Security.Principal.SecurityIdentifier]'S-1-5-32-544').Translate([Security.Principal.NTAccount]).Value
    $system = ([Security.Principal.SecurityIdentifier]'S-1-5-18').Translate([Security.Principal.NTAccount]).Value
    $user = "$env:COMPUTERNAME\$env:USERNAME"
    icacls.exe $Root /inheritance:r /grant:r "${admins}:(OI)(CI)(F)" "${system}:(OI)(CI)(F)" "${user}:(OI)(CI)(F)" | Out-Null

    $action = New-ScheduledTaskAction `
        -Execute "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" `
        -Argument "-NoProfile -NonInteractive -ExecutionPolicy Bypass -STA -WindowStyle Hidden -File `"$target`""
    $trigger = New-ScheduledTaskTrigger -AtLogOn -User $user
    $principal = New-ScheduledTaskPrincipal -UserId $user -LogonType Interactive -RunLevel Highest
    $settings = New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit (New-TimeSpan -Days 3650) -Hidden
    Register-ScheduledTask -TaskName 'TibiaPilotNG-DesktopBridge' -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null
    Start-ScheduledTask -TaskName 'TibiaPilotNG-DesktopBridge'
    Write-Output 'TibiaPilotNG Desktop Bridge installiert.'
    return
}

Add-Type -AssemblyName UIAutomationClient
Add-Type -AssemblyName UIAutomationTypes

$inbox = Join-Path $Root 'inbox'
$outbox = Join-Path $Root 'outbox'
New-Item $inbox -ItemType Directory -Force | Out-Null
New-Item $outbox -ItemType Directory -Force | Out-Null

$created = $false
$mutex = New-Object System.Threading.Mutex($true, 'Global\TibiaPilotNG-DesktopBridge', [ref]$created)
if (!$created) { return }

function Write-Result([string]$Id, [bool]$Ok, $Data, [string]$ErrorMessage = '') {
    $temporary = Join-Path $outbox "$Id.tmp"
    $result = [ordered]@{ ok = $Ok; id = $Id; data = $Data; error = $ErrorMessage }
    $result | ConvertTo-Json -Depth 5 -Compress | Set-Content $temporary -Encoding UTF8
    Move-Item $temporary (Join-Path $outbox "$Id.json") -Force
}

function Invoke-Background($Command) {
    $targetPid = [int]$Command.pid
    $automationId = [string]$Command.automationId
    if ($targetPid -le 0 -or [string]::IsNullOrWhiteSpace($automationId) -or $automationId.Length -gt 512) {
        throw 'Ungueltige Hintergrundaktion.'
    }
    $process = Get-Process -Id $targetPid -ErrorAction Stop
    if ($process.MainWindowHandle -eq 0) { throw 'Zielfenster ist nicht bereit.' }
    $root = [Windows.Automation.AutomationElement]::FromHandle($process.MainWindowHandle)
    if (!$root) { throw 'Accessibility-Baum ist nicht verfuegbar.' }
    $elements = $root.FindAll([Windows.Automation.TreeScope]::Descendants, [Windows.Automation.Condition]::TrueCondition)
    $target = $null
    foreach ($element in $elements) {
        if ($element.Current.AutomationId -eq $automationId) {
            $target = $element
            break
        }
    }
    if (!$target) { throw 'Tibia-Play-Schaltflaeche wurde nicht gefunden.' }
    $pattern = $null
    if (!$target.TryGetCurrentPattern([Windows.Automation.InvokePattern]::Pattern, [ref]$pattern)) {
        throw 'Tibia-Play-Schaltflaeche ist nicht aufrufbar.'
    }
    $pattern.Invoke()
    return @{ pid = $targetPid; title = $process.MainWindowTitle }
}

function Get-UiTree($Command) {
    $process = Get-Process -Id ([int]$Command.pid) -ErrorAction Stop
    if ($process.MainWindowHandle -eq 0) { throw 'Zielfenster ist nicht bereit.' }
    $root = [Windows.Automation.AutomationElement]::FromHandle($process.MainWindowHandle)
    if (!$root) { throw 'Accessibility-Baum ist nicht verfuegbar.' }
    $elements = $root.FindAll([Windows.Automation.TreeScope]::Descendants, [Windows.Automation.Condition]::TrueCondition)
    $items = @()
    foreach ($element in $elements) {
        $current = $element.Current
        if ($current.ControlType.Id -in @(
            [Windows.Automation.ControlType]::Edit.Id,
            [Windows.Automation.ControlType]::Button.Id
        )) {
            $items += [ordered]@{
                automationId = $current.AutomationId
                controlType = $current.ControlType.ProgrammaticName
                name = $current.Name
                isPassword = $current.IsPassword
                isEnabled = $current.IsEnabled
            }
        }
    }
    return @{ elements = $items }
}

function Set-UiValue($Command) {
    $process = Get-Process -Id ([int]$Command.pid) -ErrorAction Stop
    if ($process.MainWindowHandle -eq 0) { throw 'Zielfenster ist nicht bereit.' }
    $automationId = [string]$Command.automationId
    $text = [string]$Command.text
    $root = [Windows.Automation.AutomationElement]::FromHandle($process.MainWindowHandle)
    if (!$root) { throw 'Accessibility-Baum ist nicht verfuegbar.' }
    $elements = $root.FindAll([Windows.Automation.TreeScope]::Descendants, [Windows.Automation.Condition]::TrueCondition)
    foreach ($element in $elements) {
        if ($element.Current.AutomationId -eq $automationId) {
            $pattern = $null
            if (!$element.TryGetCurrentPattern([Windows.Automation.ValuePattern]::Pattern, [ref]$pattern)) {
                throw "Feld '$automationId' ist nicht beschreibbar."
            }
            $pattern.SetValue($text)
            return @{ automationId = $automationId }
        }
    }
    throw "Feld '$automationId' wurde nicht gefunden."
}

# --- Programme in der interaktiven Sitzung starten -------------------------
# Frueher lief jedes Tool ueber eine eigene geplante Aufgabe: anlegen,
# starten, im 250-ms-Takt abfragen, wieder abmelden - gemessen 2,2 Sekunden
# Leerlauf, BEVOR ueberhaupt etwas passiert. Die Bridge laeuft ohnehin schon
# in der angemeldeten Sitzung, also startet sie die Programme direkt.
#
# Der Prozess darf die Warteschlange nicht blockieren (ein Login wartet bis
# zu 90 s auf den Server). Darum: starten, vormerken, und die Antwort erst
# schreiben, wenn er fertig ist. Der Aufrufer wartet ohnehin auf die Datei
# in der Outbox - er merkt von der Verzoegerung nichts.
$script:pending = @{}
$maximumOutput = 200000

function Start-Run([string]$Id, $Command) {
    $exe = [string]$Command.exe
    if ([string]::IsNullOrWhiteSpace($exe)) { throw 'Kein Programm angegeben.' }
    if (!(Test-Path $exe)) { throw "Programm wurde nicht gefunden: $exe" }
    $startInfo = New-Object Diagnostics.ProcessStartInfo
    $startInfo.FileName = $exe
    if ($Command.arguments) { $startInfo.Arguments = [string]$Command.arguments }
    if ($Command.workingDirectory) {
        $startInfo.WorkingDirectory = [string]$Command.workingDirectory
    }
    $startInfo.UseShellExecute = $false
    $startInfo.CreateNoWindow = $true
    if ($Command.detached) {
        # Dauerlaeufer (z. B. OBS): starten und sofort bestaetigen. Auf ein
        # Programmende zu warten, das nie kommt, wuerde nur ins Zeitlimit
        # laufen. Ohne Umleitung, damit kein Ausgabepuffer volllaeuft.
        $process = [Diagnostics.Process]::Start($startInfo)
        Write-Result $Id $true ([ordered]@{
            exitCode = 0
            stdout = ''
            stderr = ''
            processId = $process.Id
        })
        return
    }
    $startInfo.RedirectStandardOutput = $true
    $startInfo.RedirectStandardError = $true
    $process = [Diagnostics.Process]::Start($startInfo)
    $seconds = if ($Command.timeoutSeconds) { [int]$Command.timeoutSeconds } else { 300 }
    $script:pending[$Id] = @{
        process = $process
        # Asynchron lesen, sonst blockiert ein volles Pipe-Puffer den Prozess.
        stdout = $process.StandardOutput.ReadToEndAsync()
        stderr = $process.StandardError.ReadToEndAsync()
        deadline = (Get-Date).AddSeconds($seconds)
    }
}

function Complete-Runs {
    $finished = $false
    foreach ($id in @($script:pending.Keys)) {
        $job = $script:pending[$id]
        if ($job.process.HasExited) {
            $script:pending.Remove($id)
            $finished = $true
            $out = $job.stdout.Result
            $err = $job.stderr.Result
            if ($out.Length -gt $maximumOutput) { $out = $out.Substring(0, $maximumOutput) }
            if ($err.Length -gt $maximumOutput) { $err = $err.Substring(0, $maximumOutput) }
            Write-Result $id $true ([ordered]@{
                exitCode = $job.process.ExitCode
                stdout = $out
                stderr = $err
            })
            $job.process.Dispose()
        }
        elseif ((Get-Date) -gt $job.deadline) {
            $script:pending.Remove($id)
            $finished = $true
            try { $job.process.Kill() } catch {}
            Write-Result $id $false $null 'Das Programm hat die Zeitvorgabe ueberschritten.'
        }
    }
    return $finished
}

try {
    # Schnell reagieren, solange gearbeitet wird; im Leerlauf ruhig bleiben,
    # damit die dauerhaft laufende Bridge keine CPU verheizt.
    $idleSince = Get-Date
    while ($true) {
        $busy = $false
        foreach ($file in @(Get-ChildItem $inbox -Filter '*.json' -File | Sort-Object LastWriteTimeUtc)) {
            $busy = $true
            $id = $file.BaseName
            try {
                $command = Get-Content $file.FullName -Raw | ConvertFrom-Json
                switch ([string]$command.action) {
                    'ping' { Write-Result $id $true @{ user = $env:USERNAME; computer = $env:COMPUTERNAME } }
                    'invokeBackground' { Write-Result $id $true (Invoke-Background $command) }
                    'dumpTree' { Write-Result $id $true (Get-UiTree $command) }
                    'setValueBackground' { Write-Result $id $true (Set-UiValue $command) }
                    # Antwort folgt erst, wenn das Programm beendet ist.
                    'run' { Start-Run $id $command }
                    default { throw 'Unbekannte Desktop-Bridge-Aktion.' }
                }
            } catch {
                Write-Result $id $false $null $_.Exception.Message
            } finally {
                Remove-Item $file.FullName -Force -ErrorAction SilentlyContinue
            }
        }
        if (Complete-Runs) { $busy = $true }
        if ($busy -or $script:pending.Count -gt 0) { $idleSince = Get-Date }
        $quiet = ((Get-Date) - $idleSince).TotalSeconds -gt 3
        Start-Sleep -Milliseconds $(if ($quiet) { 50 } else { 5 })
    }
} finally {
    $mutex.ReleaseMutex()
    $mutex.Dispose()
}
