param(
    [string]$SourcePath = (Split-Path -Parent $PSScriptRoot),
    [string]$InstallPath = (Join-Path $env:USERPROFILE 'TibiaPilotNG'),
    [ValidateSet('usb', 'mac_bt')]
    [string]$InputMethod = 'usb',
    [switch]$ValidateOnly
)

$ErrorActionPreference = 'Stop'
$driverUrl = 'https://www.amyuni.com/downloads/usbmmidd_v2.zip'

function Test-Administrator {
    $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
    $principal = [Security.Principal.WindowsPrincipal]::new($identity)
    return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}

function Refresh-Path {
    $machine = [Environment]::GetEnvironmentVariable('Path', 'Machine')
    $user = [Environment]::GetEnvironmentVariable('Path', 'User')
    $env:Path = "$machine;$user"
}

function Install-WingetPackage([string]$Id) {
    if (!(Get-Command winget.exe -ErrorAction SilentlyContinue)) {
        throw 'Windows App Installer (winget) fehlt. Bitte zuerst aus dem Microsoft Store installieren.'
    }
    # Schon installiert? Dann nichts tun. Beim zweiten Installer-Lauf ist
    # das der Normalfall - und genau hier scheiterte der Installer frueher:
    # `winget install` auf einem bereits aktuellen Paket liefert den Code
    # 0x8A15002B (-1978335189, "kein Upgrade verfuegbar"), den der alte
    # Code als Abbruch wertete. `winget list` sagt dagegen zuverlaessig per
    # Exit 0, dass das Paket vorhanden ist.
    & winget.exe list --exact --id $Id --accept-source-agreements --disable-interactivity *> $null
    if ($LASTEXITCODE -eq 0) { Refresh-Path; return }

    & winget.exe install --exact --id $Id --silent --disable-interactivity --accept-package-agreements --accept-source-agreements
    $code = $LASTEXITCODE
    # Gutartige Codes: 0 = frisch installiert; -1978335189 = bereits aktuell
    # (falls list das Paket unter anderer Quelle nicht fand). Beides ist ok.
    if ($code -ne 0 -and $code -ne -1978335189) {
        throw "Installation fehlgeschlagen: $Id (winget-Code $code)"
    }
    Refresh-Path
}

function Get-Python311 {
    $candidates = @(
        @(
            (Join-Path $env:LOCALAPPDATA 'Programs\Python\Python311\python.exe'),
            (Join-Path $env:ProgramFiles 'Python311\python.exe')
        ) | Where-Object { Test-Path $_ }
    )
    if (!$candidates) { throw 'Python 3.11 wurde nach der Installation nicht gefunden.' }
    return $candidates[0]
}

function Sync-Project([string]$Source, [string]$Destination) {
    if (([IO.Path]::GetFullPath($Source)).TrimEnd('\') -eq ([IO.Path]::GetFullPath($Destination)).TrimEnd('\')) {
        return
    }
    # Idempotent: robocopy /E aktualisiert ein vorhandenes Ziel in place.
    # Frueher stand hier ein "Zielordner existiert bereits"-throw. Der
    # machte den Installer nach JEDEM Abbruch unbrauchbar: Wurde er einmal
    # unterbrochen (geschlossenes Fenster, Reboot, haengendes winget),
    # schlug ein erneuter Doppelklick sofort fehl, statt die Installation
    # zu Ende zu fuehren - ein halbfertiger Zustand liess sich nur von Hand
    # retten. Ein zweiter Durchlauf muss die Installation vervollstaendigen
    # koennen; alle Einzelschritte sind ohnehin idempotent.
    New-Item $Destination -ItemType Directory -Force | Out-Null
    & robocopy.exe $Source $Destination /E /COPY:DAT /DCOPY:DAT /R:2 /W:1 /XJ `
        /XD '.venv' '__pycache__' '.pytest_cache' '.current-minimap' '.minimap-backup' `
        /XF 'file.json' '*.pilotng' '*.pilotscript' '.hid-bridge-token' '.input-method' `
            '.pilotng-*' '.ethernet-restart-done' '.*.json' '.*.png' '.*.log' '.*.txt' '.*.zip' | Out-Null
    if ($LASTEXITCODE -ge 8) { throw 'Programmdateien konnten nicht kopiert werden.' }
}

function Install-VirtualDisplay {
    $root = Join-Path $env:ProgramData 'TibiaPilotNG\usbmmidd'
    $archive = Join-Path $root 'usbmmidd_v2.zip'
    $folder = Join-Path $root 'usbmmidd_v2'
    $installer = Join-Path $folder 'deviceinstaller64.exe'
    if (!(Test-Path $installer)) {
        New-Item $root -ItemType Directory -Force | Out-Null
        Invoke-WebRequest -Uri $driverUrl -OutFile $archive
        Expand-Archive -Path $archive -DestinationPath $root -Force
    }
    if (!(Test-Path $installer)) { throw 'Der virtuelle Bildschirm-Treiber ist unvollstaendig.' }
    $present = Get-PnpDevice -ErrorAction SilentlyContinue | Where-Object { $_.InstanceId -match 'usbmmIdd' }
    if (!$present) {
        & $installer install 'usbmmIdd.inf' 'usbmmidd'
        if ($LASTEXITCODE -ne 0) { throw 'Der virtuelle Bildschirm-Treiber konnte nicht installiert werden.' }
    }
    & $installer enableidd 1
    if ($LASTEXITCODE -ne 0) { throw 'Der virtuelle Bildschirm konnte nicht aktiviert werden.' }
}

function Install-OBSProfile([string]$Repo) {
    $assets = Join-Path $Repo 'installer\obs'
    $obsProfile = Join-Path $env:APPDATA 'obs-studio\basic\profiles\TibiaPilotNG'
    $scenes = Join-Path $env:APPDATA 'obs-studio\basic\scenes'
    if (!(Test-Path (Join-Path $assets 'basic.ini')) -or !(Test-Path (Join-Path $assets 'TibiaPilotNG.json'))) {
        throw 'OBS-Vorlage fehlt im Installerpaket.'
    }
    New-Item $obsProfile, $scenes -ItemType Directory -Force | Out-Null
    Copy-Item (Join-Path $assets 'basic.ini') (Join-Path $obsProfile 'basic.ini') -Force
    Copy-Item (Join-Path $assets 'TibiaPilotNG.json') (Join-Path $scenes 'TibiaPilotNG.json') -Force
}

function Invoke-SelfTest([string]$Repo) {
    & "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" `
        -NoProfile -NonInteractive -ExecutionPolicy Bypass `
        -File (Join-Path $Repo 'tools\Test-PilotNG-Installation.ps1') -Repo $Repo
    return $LASTEXITCODE
}

function Install-DefaultConfig([string]$Repo) {
    # Bot-Einstellungen (Wegpunkte, Heilschwellen, Hotkeys, Backpacks).
    # Eine bereits vorhandene Konfiguration wird NIE ueberschrieben - nur
    # ein frischer Rechner bekommt den mitgelieferten Stand.
    $target = Join-Path $Repo 'file.json'
    if (Test-Path $target) { return }
    $default = Join-Path $Repo 'installer\config\file.default.json'
    if (Test-Path $default) { Copy-Item $default $target -Force }
}

$SourcePath = (Resolve-Path $SourcePath).Path
if (!(Test-Path (Join-Path $SourcePath 'pyproject.toml'))) {
    throw 'Der Installer muss aus einem vollstaendigen TibiaPilotNG-Paket gestartet werden.'
}
$expectedInstallPath = Join-Path $env:USERPROFILE 'TibiaPilotNG'
if (([IO.Path]::GetFullPath($InstallPath)).TrimEnd('\') -ne ([IO.Path]::GetFullPath($expectedInstallPath)).TrimEnd('\')) {
    # Haeufigste Ursache: Die UAC-Abfrage wurde mit einem ANDEREN
    # Admin-Konto bestaetigt. Dann zeigt %USERPROFILE% im erhoehten Lauf
    # auf dessen Profil, und Minimap-Vorlage, Bot-Konfiguration und
    # geplante Aufgaben landeten beim falschen Benutzer. Lieber klar
    # abbrechen als still ein fremdes Profil einrichten.
    throw ("PilotNG muss in $expectedInstallPath installiert werden. " +
        "Laeuft der Installer gerade unter einem anderen Konto? Die " +
        "Windows-Adminabfrage muss mit DEMSELBEN Benutzer bestaetigt " +
        "werden, der spaeter auch bottet.")
}

if ($ValidateOnly) {
    exit (Invoke-SelfTest $InstallPath)
}

# Deutsche Tastatur JETZT setzen - bewusst vor der Rechteerhoehung, damit
# es fuer den angemeldeten Benutzer greift und nicht fuer ein evtl. anderes
# Admin-Konto. Ohne de-DE tippt der Arduino (KeyboardLayout_de_DE) falsche
# Zeichen, und der Login mit E-Mail-Adresse scheitert. Der Aufruf ist
# idempotent und laeuft ohne Adminrechte.
$germanKeyboard = Join-Path $SourcePath 'tools\Set-KeyboardLayout-German.ps1'
if (Test-Path $germanKeyboard) {
    & "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" `
        -NoProfile -NonInteractive -ExecutionPolicy Bypass -File $germanKeyboard | Out-Null
}

if (!(Test-Administrator)) {
    $arguments = "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`" -SourcePath `"$SourcePath`" -InstallPath `"$InstallPath`" -InputMethod $InputMethod"
    Start-Process -FilePath "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -Verb RunAs -ArgumentList $arguments
    exit 0
}

# Ab hier laeuft der eigentliche, erhoehte Durchlauf. Alles mitschneiden,
# damit ein Abbruch nicht spurlos bleibt: Der Installer starb einmal
# zwischen zwei Schritten (Fenster weg), und hinterher war nicht mehr
# erkennbar, woran. Der Mitschnitt landet in einer Datei, die auch ueber-
# lebt, wenn das Fenster verschwindet.
$logDir = Join-Path $env:LOCALAPPDATA 'TibiaPilotNG'
New-Item $logDir -ItemType Directory -Force | Out-Null
$installLog = Join-Path $logDir ('install-{0}.log' -f (Get-Date -Format 'yyyyMMdd-HHmmss'))
try { Start-Transcript -Path $installLog | Out-Null } catch {}
Write-Output "TibiaPilotNG-Installation gestartet: $(Get-Date -Format o)"

# Bricht ein Schritt ab, steht die Ursache samt Stelle im Mitschnitt -
# statt nur im fluechtigen Fenster. break gibt den Fehler danach weiter,
# damit Install-PilotNG.cmd weiterhin mit "pause" stehen bleibt.
trap {
    Write-Output "INSTALLER ABGEBROCHEN: $($_.Exception.Message)"
    Write-Output $_.ScriptStackTrace
    try { Stop-Transcript | Out-Null } catch {}
    break
}

Sync-Project $SourcePath $InstallPath

# Mitgelieferte Bot-Einstellungen uebernehmen (nur auf frischem Rechner).
Install-DefaultConfig $InstallPath

# Vollstaendige Minimap-Vorlage aus dem Paket entpacken. Braucht weder
# Internet noch den Client - die Karte liegt fertig im Installer. Ab dem
# ersten Bot-Start spielt tools\Launch-Tibia.py sie ueber die Live-Karte.
# Fehler meldet das Skript per throw; das propagiert hier dank
# ErrorActionPreference=Stop von selbst.
& (Join-Path $InstallPath 'tools\Install-TibiaMap.ps1') -Repo $InstallPath | Out-Null

Install-WingetPackage 'Git.Git'
Install-WingetPackage 'ArduinoSA.CLI'
$arduinoCli = & (Join-Path $InstallPath 'tools\Install-ArduinoToolchain.ps1')
if (!$arduinoCli) { throw 'Arduino-Toolchain konnte nicht eingerichtet werden.' }
if (!(Test-Path (Join-Path $InstallPath '.venv\Scripts\python.exe'))) {
    Install-WingetPackage 'Python.Python.3.11'
    $python = Get-Python311
    & $python -m venv (Join-Path $InstallPath '.venv')
    if ($LASTEXITCODE -ne 0) { throw 'Python-Umgebung konnte nicht erstellt werden.' }
}

$python = Join-Path $InstallPath '.venv\Scripts\python.exe'
& $python -m pip install --disable-pip-version-check --no-input --upgrade pip
if ($LASTEXITCODE -ne 0) { throw 'pip konnte nicht aktualisiert werden.' }
& $python -m pip install --disable-pip-version-check --no-input $InstallPath
if ($LASTEXITCODE -ne 0) { throw 'Python-Abhaengigkeiten konnten nicht installiert werden.' }

Install-WingetPackage 'OBSProject.OBSStudio'
Install-WingetPackage 'UB-Mannheim.TesseractOCR'
Install-OBSProfile $InstallPath
& (Join-Path $InstallPath 'tools\Initialize-OBSWebSocket.ps1')
if ($LASTEXITCODE -ne 0) { throw 'OBS-WebSocket konnte nicht eingerichtet werden.' }
Install-VirtualDisplay

# Das lokale KI-Modell (Ollama) ist optional und mehrere GB gross. Statt es
# ungefragt zu ziehen, fragt ein Fenster im Stil des Bot-Overlays. Nur bei
# ausdruecklichem Ja wird installiert; auf einem Rechner ohne Desktop
# (z. B. Fernwartung) faellt die Auswahl automatisch auf "ueberspringen".
$consent = Join-Path $InstallPath 'tools\Show-OllamaConsent.ps1'
$installOllama = $false
if (Test-Path $consent) {
    & "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" `
        -NoProfile -ExecutionPolicy Bypass -STA -File $consent | Out-Null
    $installOllama = ($LASTEXITCODE -eq 0)
}
if ($installOllama) {
    try {
        & (Join-Path $InstallPath 'tools\Install-Llm.ps1')
        if ($LASTEXITCODE -ne 0) { throw "Exit-Code $LASTEXITCODE" }
    } catch {
        Write-Warning "LLM-Setup uebersprungen ($($_.Exception.Message)). Der Smart-Recovery KI-Blick bleibt deaktiviert; tools\Install-Llm.ps1 kann jederzeit nachgeholt werden."
    }
} else {
    Write-Output 'Ollama uebersprungen. Der Bot laeuft vollstaendig; der KI-Blick laesst sich jederzeit mit tools\Install-Llm.ps1 nachholen.'
}

& (Join-Path $InstallPath 'tools\Install-DesktopBridge.ps1') -Install
if ($LASTEXITCODE -ne 0) { throw 'Desktop-Bridge konnte nicht eingerichtet werden.' }
& (Join-Path $InstallPath 'tools\Install-PilotNG-OneClick.ps1') -AllowUninitialized
if ($LASTEXITCODE -ne 0) { throw 'Start-Tasks konnten nicht eingerichtet werden.' }

$inputFile = Join-Path $InstallPath '.input-method'
if (!(Test-Path $inputFile)) { Set-Content $inputFile $InputMethod -NoNewline -Encoding ascii }

$tibia = Join-Path $env:LOCALAPPDATA 'Tibia\Tibia.exe'
if (!(Test-Path $tibia)) {
    Start-Process 'https://www.tibia.com/account/?subtopic=downloadclient'
    Add-Type -AssemblyName System.Windows.Forms
    do {
        $choice = [System.Windows.Forms.MessageBox]::Show(
            "Die offizielle Tibia-Seite wurde geöffnet. Installiere Tibia und klicke danach auf Wiederholen. Der PilotNG-Installer setzt dann automatisch fort.",
            'TibiaPilotNG Installation',
            [System.Windows.Forms.MessageBoxButtons]::RetryCancel,
            [System.Windows.Forms.MessageBoxIcon]::Information
        )
        if ($choice -eq [System.Windows.Forms.DialogResult]::Cancel) {
            throw 'Installation abgebrochen: Tibia Client fehlt.'
        }
    } while (!(Test-Path $tibia))
}

# Jetzt ist der Client sicher da: die Vollkarte auch direkt in seinen
# Live-Ordner spiegeln, damit sie vom ersten Start an vollstaendig ist.
& (Join-Path $InstallPath 'tools\Install-TibiaMap.ps1') -Repo $InstallPath | Out-Null

if ((Invoke-SelfTest $InstallPath) -ne 0) { throw 'Installations-Selbsttest fehlgeschlagen.' }

$tibiaProfile = Join-Path $env:LOCALAPPDATA 'Tibia\packages\Tibia\conf\clientoptions.json'
if (!(Test-Path $tibiaProfile)) {
    Start-Process $tibia
    Write-Warning 'Tibia wurde gestartet. Einmal normal bis zur Charakterauswahl anmelden; danach startet der Bot ueber die Desktop-Verknuepfung.'
} else {
    Write-Output 'Installation abgeschlossen. Die drei TibiaPilotNG-Verknuepfungen liegen auf Desktop und im Startmenue.'
}

Write-Output "TibiaPilotNG-Installation erfolgreich beendet: $(Get-Date -Format o)"
try { Stop-Transcript | Out-Null } catch {}
