@echo off
setlocal
cd /d "%~dp0"
set "APPROOT=%CD%"
set "PAYLOAD=%TEMP%\liverylab_garage_cleanup_%RANDOM%%RANDOM%.ps1"
set "LL_SELF=%~f0"
set "LL_PAYLOAD=%PAYLOAD%"
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "$self=$env:LL_SELF; $payload=$env:LL_PAYLOAD; $lines=Get-Content -LiteralPath $self -Encoding UTF8; $marker=($lines | Select-String -Pattern '^# POWERSHELL_PAYLOAD_BEGIN$' | Select-Object -First 1).LineNumber; if (-not $marker) { throw 'PowerShell payload marker missing.' }; $lines[$marker..($lines.Count-1)] | Set-Content -LiteralPath $payload -Encoding UTF8"
if errorlevel 1 (
  echo.
  echo Could not extract the embedded garage cleanup script.
  echo.
  pause
  exit /b 1
)
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%PAYLOAD%" -AppRoot "%APPROOT%"
set "EXITCODE=%ERRORLEVEL%"
del "%PAYLOAD%" >nul 2>nul
echo.
pause
exit /b %EXITCODE%
# POWERSHELL_PAYLOAD_BEGIN
param(
    [string]$AppRoot = "",
    [switch]$DryRun
)

$ErrorActionPreference = "Stop"

function Write-Info($Message) {
    Write-Host $Message -ForegroundColor Cyan
}

function Write-Warn($Message) {
    Write-Host $Message -ForegroundColor Yellow
}

function Write-Bad($Message) {
    Write-Host $Message -ForegroundColor Red
}

function Normalize-Slashes($Value) {
    if ($null -eq $Value) { return "" }
    return ([string]$Value).Replace("/", "\")
}

function Split-GamePath($Value) {
    return @(Normalize-Slashes $Value -split "\\" | Where-Object { $_ -ne "" })
}

function Normalize-GameRel($Value) {
    $parts = Split-GamePath $Value
    if ($parts.Count -eq 0) { return "" }
    $lower = @($parts | ForEach-Object { $_.ToLowerInvariant() })
    foreach ($anchor in @("content", "system", "uiresources", "editor", "cfg")) {
        $index = [Array]::IndexOf($lower, $anchor)
        if ($index -ge 0) {
            return ($parts[$index..($parts.Count - 1)] -join "\")
        }
    }
    return ($parts -join "\")
}

function Is-GameRelLike($Value) {
    $text = Normalize-Slashes $Value
    if ([string]::IsNullOrWhiteSpace($text)) { return $false }
    $lower = $text.ToLowerInvariant()
    if ($lower -match "(^|\\)(content|system|uiresources|editor|cfg)\\") { return $true }
    if ($lower -match "\.(cardesign|design|visual|carfinalstate|mechanicalpreset|material|texture|texturemips|carpart|compatiblepart|mesh|loc)$") { return $true }
    return $false
}

function Resolve-GameRel([string]$GameRoot, [string]$Rel) {
    $relNorm = Normalize-GameRel $Rel
    if ([string]::IsNullOrWhiteSpace($relNorm)) { return $null }
    $path = $GameRoot
    foreach ($part in (Split-GamePath $relNorm)) {
        $path = Join-Path $path $part
    }
    return $path
}

function Test-PathInside([string]$Child, [string]$Parent) {
    try {
        $childFull = [System.IO.Path]::GetFullPath($Child).TrimEnd("\")
        $parentFull = [System.IO.Path]::GetFullPath($Parent).TrimEnd("\")
        return $childFull.StartsWith($parentFull, [System.StringComparison]::OrdinalIgnoreCase)
    } catch {
        return $false
    }
}

function Test-PathSafe([string]$Path) {
    if ([string]::IsNullOrWhiteSpace($Path)) { return $false }
    try {
        return Test-Path -LiteralPath $Path
    } catch {
        return $false
    }
}

function Safe-Name($Value, $Fallback = "item") {
    if ($null -eq $Value) { $text = "" } else { $text = [string]$Value }
    $text = [regex]::Replace($text, "[^0-9A-Za-z._-]+", "_").Trim("_", "-", ".")
    if ([string]::IsNullOrWhiteSpace($text)) { return $Fallback }
    return $text
}

function Get-PropValue($Object, [string[]]$Names, $Fallback = "") {
    if ($null -eq $Object) { return $Fallback }
    foreach ($name in $Names) {
        if ($Object.PSObject.Properties.Name -contains $name) {
            $value = $Object.$name
            if ($null -ne $value -and -not [string]::IsNullOrWhiteSpace([string]$value)) {
                return $value
            }
        }
    }
    return $Fallback
}

function Get-RelativePathCompat([string]$BasePath, [string]$ChildPath) {
    try {
        $baseFull = [System.IO.Path]::GetFullPath($BasePath).TrimEnd("\") + "\"
        $childFull = [System.IO.Path]::GetFullPath($ChildPath)
        $baseUri = New-Object System.Uri($baseFull)
        $childUri = New-Object System.Uri($childFull)
        return [System.Uri]::UnescapeDataString($baseUri.MakeRelativeUri($childUri).ToString()).Replace("/", "\")
    } catch {
        return [System.IO.Path]::GetFileName($ChildPath)
    }
}

function Read-JsonFile($Path, $Fallback = $null) {
    if (-not (Test-Path -LiteralPath $Path)) { return $Fallback }
    $raw = Get-Content -LiteralPath $Path -Raw -Encoding UTF8
    if ([string]::IsNullOrWhiteSpace($raw)) { return $Fallback }
    return $raw | ConvertFrom-Json
}

function Write-Utf8NoBomFile($Path, [string]$Content) {
    $encoding = New-Object System.Text.UTF8Encoding($false)
    [System.IO.File]::WriteAllText($Path, $Content, $encoding)
}

function Add-UniqueRef([System.Collections.Generic.List[string]]$Refs, [System.Collections.Generic.HashSet[string]]$Seen, $Value) {
    $rel = Normalize-GameRel $Value
    if ([string]::IsNullOrWhiteSpace($rel)) { return }
    $key = $rel.ToLowerInvariant()
    if ($Seen.Add($key)) {
        $Refs.Add($rel) | Out-Null
    }
}

function Add-StringRefsFromObject([System.Collections.Generic.List[string]]$Refs, [System.Collections.Generic.HashSet[string]]$Seen, $Object) {
    if ($null -eq $Object) { return }
    if ($Object -is [string]) {
        if (Is-GameRelLike $Object) {
            Add-UniqueRef $Refs $Seen $Object
        }
        return
    }
    if ($Object -is [System.Collections.IEnumerable] -and -not ($Object -is [string])) {
        foreach ($item in $Object) {
            Add-StringRefsFromObject $Refs $Seen $item
        }
        return
    }
    if ($Object.PSObject -and $Object.PSObject.Properties) {
        foreach ($prop in $Object.PSObject.Properties) {
            Add-StringRefsFromObject $Refs $Seen $prop.Value
        }
    }
}

function Add-KnownSkinRefs([System.Collections.Generic.List[string]]$Refs, [System.Collections.Generic.HashSet[string]]$Seen, $Skin) {
    if ($null -eq $Skin) { return }
    $carContent = Normalize-GameRel ([string](Get-PropValue $Skin @("car_content_path", "carContentPath") ""))
    $skinId = [string](Get-PropValue $Skin @("skin_id", "skinId") "")
    if ($carContent -and $skinId) {
        Add-UniqueRef $Refs $Seen "$carContent\skins\$skinId"
        Add-UniqueRef $Refs $Seen "$carContent\skins\$skinId.cardesign"
        Add-UniqueRef $Refs $Seen "$carContent\skins\$skinId.design"
        Add-UniqueRef $Refs $Seen "$carContent\presets\$skinId.visualcarpreset"
    }

    $files = $null
    if ($Skin.PSObject.Properties.Name -contains "files") { $files = $Skin.files }
    if ($files) {
        foreach ($key in @(
            "cardesign",
            "design",
            "skin_folder",
            "material",
            "visual_preset",
            "carfinalstate",
            "carfinalstates",
            "thumbnail",
            "thumbnail_mips",
            "thumbnail_mips_list",
            "thumbnails",
            "thumbnail_png"
        )) {
            if ($files.PSObject.Properties.Name -contains $key) {
                Add-StringRefsFromObject $Refs $Seen $files.$key
            }
        }
        foreach ($key in @("mechanical_preset", "mechanical_presets")) {
            if ($files.PSObject.Properties.Name -contains $key) {
                $mechanicalValues = @($files.$key)
                foreach ($value in $mechanicalValues) {
                    $rel = Normalize-GameRel $value
                    $lower = $rel.ToLowerInvariant()
                    if ($rel -and ($lower.Contains("liverylab") -or ($skinId -and $lower.Contains($skinId.ToLowerInvariant())))) {
                        Add-UniqueRef $Refs $Seen $rel
                    }
                }
            }
        }
    }

    if ($Skin.PSObject.Properties.Name -contains "numberplate_bake") {
        $bake = $Skin.numberplate_bake
        if ($bake -and ($bake.PSObject.Properties.Name -contains "surfaces")) {
            foreach ($surface in @($bake.surfaces.PSObject.Properties.Value)) {
                foreach ($key in @("textureRel", "mipsRel")) {
                    if ($surface -and ($surface.PSObject.Properties.Name -contains $key)) {
                        Add-UniqueRef $Refs $Seen $surface.$key
                    }
                }
            }
        }
    }

    if ($Skin.PSObject.Properties.Name -contains "driver_profile") {
        $profile = $Skin.driver_profile
        if ($profile -and ($profile.PSObject.Properties.Name -contains "appliedDriverFinalState")) {
            Add-UniqueRef $Refs $Seen $profile.appliedDriverFinalState
        }
        if ($carContent -and $skinId) {
            Add-UniqueRef $Refs $Seen "$carContent\skins\$skinId\driver"
        }
    }

    foreach ($plateProp in @("road_plate", "roadPlate")) {
        if ($Skin.PSObject.Properties.Name -contains $plateProp) {
            $plateRefs = New-Object System.Collections.Generic.List[string]
            $plateSeen = New-Object System.Collections.Generic.HashSet[string]([System.StringComparer]::OrdinalIgnoreCase)
            Add-StringRefsFromObject $plateRefs $plateSeen $Skin.$plateProp
            foreach ($rel in $plateRefs) {
                $lower = $rel.ToLowerInvariant()
                if ($lower.Contains("liverylab")) {
                    Add-UniqueRef $Refs $Seen $rel
                }
            }
        }
    }
}

function Move-ToQuarantine([string]$Path, [string]$SourceRoot, [string]$TargetRoot, [string]$Kind, [bool]$DryRunValue) {
    $result = [ordered]@{
        kind = $Kind
        source = $Path
        target = ""
        status = "missing"
        error = ""
    }
    if (-not (Test-Path -LiteralPath $Path)) {
        return [pscustomobject]$result
    }
    if (-not (Test-PathInside $Path $SourceRoot)) {
        $result.status = "refused_outside_root"
        return [pscustomobject]$result
    }
    try {
        $relative = Get-RelativePathCompat ([System.IO.Path]::GetFullPath($SourceRoot)) ([System.IO.Path]::GetFullPath($Path))
    } catch {
        $relative = Safe-Name ([System.IO.Path]::GetFileName($Path))
    }
    $target = Join-Path $TargetRoot $relative
    $targetDir = Split-Path -Parent $target
    $base = [System.IO.Path]::GetFileNameWithoutExtension($target)
    $ext = [System.IO.Path]::GetExtension($target)
    $unique = $target
    $index = 1
    while (Test-Path -LiteralPath $unique) {
        $unique = Join-Path $targetDir ("{0}_{1}{2}" -f $base, $index, $ext)
        $index += 1
    }
    $result.target = $unique
    if ($DryRunValue) {
        $result.status = "dry_run"
        return [pscustomobject]$result
    }
    try {
        New-Item -ItemType Directory -Force -Path $targetDir | Out-Null
        Move-Item -LiteralPath $Path -Destination $unique -Force
        $result.status = "moved"
    } catch {
        $result.status = "error"
        $result.error = $_.Exception.Message
    }
    return [pscustomobject]$result
}

function Get-SavedCarsDirs {
    $profileRoot = Join-Path $env:USERPROFILE "Saved Games\ACE\ProfileData"
    if (-not (Test-Path -LiteralPath $profileRoot)) { return @() }
    return @(Get-ChildItem -LiteralPath $profileRoot -Directory -ErrorAction SilentlyContinue | ForEach-Object {
        $dir = Join-Path $_.FullName "OpenData\SavedCars"
        if (Test-Path -LiteralPath $dir) { $dir }
    })
}

function Get-SavedCarGuid($Path) {
    $name = [System.IO.Path]::GetFileNameWithoutExtension([string]$Path)
    $match = [regex]::Match($name, "([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})")
    if ($match.Success) { return $match.Groups[1].Value }
    return $name
}

function Test-SkipSavedCarPath($Path) {
    $text = Normalize-Slashes $Path
    $lower = $text.ToLowerInvariant()
    if ($lower.Contains("\_acevo_backups\")) { return $true }
    if ($lower.EndsWith(".bak")) { return $true }
    return $false
}

function Get-SavedCarThumbnailPaths($Guid) {
    $root = Join-Path $env:USERPROFILE "Saved Games\ACE\SavedCars\Thumbnails"
    if ([string]::IsNullOrWhiteSpace($Guid) -or -not (Test-Path -LiteralPath $root)) { return @() }
    return @(Get-ChildItem -LiteralPath $root -File -ErrorAction SilentlyContinue | Where-Object {
        $_.Name.ToLowerInvariant().Contains($Guid.ToLowerInvariant())
    } | ForEach-Object { $_.FullName })
}

function Test-SavedCarPossibleBroken([string]$Text, [string]$GameRoot) {
    $matches = [regex]::Matches($Text, "(?i)(content[\\/][^`"'\s<>]+?\.(?:cardesign|design|visual|carfinalstate|mechanicalpreset|material|texture|texturemips|carpart|compatiblepart|mesh))")
    $missing = New-Object System.Collections.Generic.List[string]
    $seen = New-Object System.Collections.Generic.HashSet[string]([System.StringComparer]::OrdinalIgnoreCase)
    foreach ($match in $matches) {
        $rel = Normalize-GameRel $match.Groups[1].Value
        if ([string]::IsNullOrWhiteSpace($rel)) { continue }
        if (-not $seen.Add($rel)) { continue }
        $path = Resolve-GameRel $GameRoot $rel
        if ($path -and -not (Test-PathSafe $path)) {
            $missing.Add($rel) | Out-Null
        }
    }
    return @($missing)
}

if ([string]::IsNullOrWhiteSpace($AppRoot)) {
    $AppRoot = (Get-Location).Path
}
$AppRoot = [System.IO.Path]::GetFullPath($AppRoot)

Write-Host ""
Write-Host "AC EVO Garage Cleanup for LiveryLab" -ForegroundColor White
Write-Host "App folder: $AppRoot"
if ($DryRun) { Write-Warn "DRY RUN: no files will be moved or changed." }
Write-Host ""

$exeProcesses = @(Get-Process -Name "Assetto Corsa EVO", "AssettoCorsaEVO", "AssettoCorsaEVO-Win64-Shipping" -ErrorAction SilentlyContinue)
if ($exeProcesses.Count -gt 0) {
    Write-Bad "AC EVO appears to be running. Close the game before running this cleanup."
    exit 2
}

$libraryCandidates = @(
    (Join-Path $AppRoot "LiveryLibrary\my_skins.json"),
    (Join-Path $AppRoot "release\LiveryLibrary\my_skins.json")
)
$settingsCandidates = @(
    (Join-Path $AppRoot "UserData\settings.json"),
    (Join-Path $AppRoot "release\UserData\settings.json")
)
$userDataCandidates = @(
    (Join-Path $AppRoot "UserData"),
    (Join-Path $AppRoot "release\UserData")
)
$tyreBrandCandidates = @(
    (Join-Path $AppRoot "LiveryLibrary\TyreBrands"),
    (Join-Path $AppRoot "release\LiveryLibrary\TyreBrands")
)

$mySkinsPath = @($libraryCandidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1)
if (-not $mySkinsPath) {
    Write-Bad "Could not find LiveryLibrary\my_skins.json. Put this cleanup next to LiveryLab.exe or the portable LiveryLab folder."
    exit 3
}
$settingsPath = @($settingsCandidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1)
$userDataRoot = @($userDataCandidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1)
if (-not $userDataRoot) {
    $userDataRoot = Join-Path $AppRoot "UserData"
}
$tyreBrandRoot = @($tyreBrandCandidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1)

$registryDoc = Read-JsonFile $mySkinsPath @()
if ($registryDoc -is [System.Collections.IEnumerable] -and -not ($registryDoc -is [string]) -and -not ($registryDoc.PSObject.Properties.Name -contains "my_skins")) {
    $skins = @($registryDoc)
    $registryHasWrapper = $false
} elseif ($registryDoc -and ($registryDoc.PSObject.Properties.Name -contains "my_skins")) {
    $skins = @($registryDoc.my_skins)
    $registryHasWrapper = $true
} else {
    $skins = @()
    $registryHasWrapper = $false
}
$settings = if ($settingsPath) { Read-JsonFile $settingsPath $null } else { $null }

$rootCandidates = New-Object System.Collections.Generic.List[string]
if ($settings -and $settings.default_root_folder) { $rootCandidates.Add([string]$settings.default_root_folder) | Out-Null }
foreach ($skin in $skins) {
    if ($skin.game_root) { $rootCandidates.Add([string]$skin.game_root) | Out-Null }
    if ($skin.gameRoot) { $rootCandidates.Add([string]$skin.gameRoot) | Out-Null }
}
$gameRoot = @($rootCandidates | Where-Object {
    $_ -and (Test-Path -LiteralPath $_) -and ((Test-Path -LiteralPath (Join-Path $_ "content")) -or (Test-Path -LiteralPath (Join-Path $_ "content.kspkg")))
} | Select-Object -First 1)

if (-not $gameRoot) {
    Write-Bad "Could not find a valid AC EVO root from settings or my_skins.json."
    exit 4
}
$gameRoot = [System.IO.Path]::GetFullPath($gameRoot)

$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$sessionDir = Join-Path $userDataRoot "GarageCleanup\$timestamp"
$quarantineRoot = Join-Path $sessionDir "quarantine"
$manifestPath = Join-Path $sessionDir "garage_cleanup_manifest.json"
if (-not $DryRun) {
    New-Item -ItemType Directory -Force -Path $quarantineRoot | Out-Null
}

$managedSkins = @($skins | Where-Object {
    $_ -and $_.PSObject.Properties.Name -contains "files"
})

$refs = New-Object System.Collections.Generic.List[string]
$seenRefs = New-Object System.Collections.Generic.HashSet[string]([System.StringComparer]::OrdinalIgnoreCase)
foreach ($skin in $managedSkins) {
    Add-KnownSkinRefs $refs $seenRefs $skin
    $carContent = Normalize-GameRel ([string](Get-PropValue $skin @("car_content_path", "carContentPath") ""))
    $skinId = [string](Get-PropValue $skin @("skin_id", "skinId") "")
    if ($carContent -and $skinId) {
        Add-UniqueRef $refs $seenRefs "$carContent\skins\$skinId"
        $partsDir = Resolve-GameRel $gameRoot "$carContent\presets\parts"
        if ($partsDir -and (Test-Path -LiteralPath $partsDir)) {
            Get-ChildItem -LiteralPath $partsDir -File -ErrorAction SilentlyContinue | Where-Object {
                $_.Name.ToLowerInvariant().StartsWith("liverylab_") -and $_.Name.ToLowerInvariant().Contains("plate_r")
            } | ForEach-Object {
                Add-UniqueRef $refs $seenRefs (Normalize-GameRel $_.FullName)
            }
        }
    }
}

if ($tyreBrandRoot -and (Test-Path -LiteralPath $tyreBrandRoot)) {
    Get-ChildItem -LiteralPath $tyreBrandRoot -Filter "manifest.json" -Recurse -File -ErrorAction SilentlyContinue | ForEach-Object {
        try {
            $manifest = Read-JsonFile $_.FullName $null
            if ($manifest -and $manifest.type -eq "liverylab_custom_tyre_brand") {
                foreach ($rel in @($manifest.files)) {
                    if ($rel) {
                        Add-UniqueRef $refs $seenRefs $rel
                    }
                }
                if ($manifest.tyretexturesRel) {
                    Add-UniqueRef $refs $seenRefs $manifest.tyretexturesRel
                }
            }
        } catch {}
    }
}

$savedCarMatches = New-Object System.Collections.Generic.List[object]
$possibleBroken = New-Object System.Collections.Generic.List[object]
$savedCarDirs = @(Get-SavedCarsDirs)
$refKeys = @($refs | ForEach-Object {
    $r = (Normalize-Slashes $_).ToLowerInvariant()
    @($r, $r.Replace("\", "/"))
} | Where-Object { $_ } | Select-Object -Unique)

foreach ($dir in $savedCarDirs) {
    Get-ChildItem -LiteralPath $dir -Filter "*.carfinalstate*" -Recurse -File -ErrorAction SilentlyContinue | ForEach-Object {
        $path = $_.FullName
        if (Test-SkipSavedCarPath $path) { return }
        try {
            $text = [System.IO.File]::ReadAllText($path, [System.Text.Encoding]::UTF8).ToLowerInvariant()
        } catch {
            try {
                $text = [System.Text.Encoding]::UTF8.GetString([System.IO.File]::ReadAllBytes($path)).ToLowerInvariant()
            } catch {
                return
            }
        }
        $matched = @($refKeys | Where-Object { $_ -and $text.Contains($_) } | Select-Object -First 12)
        if ($text.Contains("liverylab_") -and $text.Contains("plate_r")) {
            $matched += "heuristic:liverylab_plate_r"
        }
        if ($matched.Count -gt 0) {
            $savedCarMatches.Add([pscustomobject]@{
                path = $path
                guid = Get-SavedCarGuid $path
                matchedRefs = @($matched | Select-Object -Unique)
            }) | Out-Null
        } else {
            $missingRefs = @(Test-SavedCarPossibleBroken $text $gameRoot)
            if ($missingRefs.Count -gt 0) {
                $possibleBroken.Add([pscustomobject]@{
                    path = $path
                    guid = Get-SavedCarGuid $path
                    missingRefs = @($missingRefs | Select-Object -First 20)
                }) | Out-Null
            }
        }
    }
}

Write-Info "Found AC EVO root: $gameRoot"
Write-Info "Known LiveryLab skins in registry: $($managedSkins.Count)"
Write-Info "Known game references collected: $($refs.Count)"
Write-Info "Matching LiveryLab SavedCars: $($savedCarMatches.Count)"
Write-Info "Possibly broken SavedCars: $($possibleBroken.Count)"
Write-Info "Game-root content cleanup: disabled"
Write-Host ""

if ($savedCarMatches.Count -gt 0) {
    Write-Host "Matching SavedCars:" -ForegroundColor White
    $savedCarMatches | Select-Object -First 20 | ForEach-Object { Write-Host " - $($_.path)" }
    if ($savedCarMatches.Count -gt 20) { Write-Host " - ... plus $($savedCarMatches.Count - 20) more" }
    Write-Host ""
}
if ($possibleBroken.Count -gt 0) {
    Write-Warn "Possibly broken SavedCars were found. These are not proven LiveryLab entries, but reference missing game files."
    $possibleBroken | Select-Object -First 10 | ForEach-Object { Write-Host " ? $($_.path)" }
    if ($possibleBroken.Count -gt 10) { Write-Host " ? ... plus $($possibleBroken.Count - 10) more" }
    Write-Host ""
}

Write-Warn "This will quarantine matching SavedCars and thumbnails under:"
Write-Host "  $quarantineRoot"
Write-Warn "Only SavedCars and their thumbnails are quarantined. AC EVO game-root files are not changed."
Write-Host ""
$confirm = Read-Host "Type CLEANUP to continue"
if ($confirm -ne "CLEANUP") {
    Write-Warn "Aborted by user. No changes made."
    exit 0
}

$includeBroken = $false
if ($possibleBroken.Count -gt 0) {
    $brokenConfirm = Read-Host "Type BROKEN to also quarantine possibly broken SavedCars, or press Enter to leave them"
    $includeBroken = ($brokenConfirm -eq "BROKEN")
}

if (-not $DryRun) {
    New-Item -ItemType Directory -Force -Path $quarantineRoot | Out-Null
}

$movedSavedCars = New-Object System.Collections.Generic.List[object]
$savedRoot = Join-Path $env:USERPROFILE "Saved Games\ACE"
foreach ($hit in $savedCarMatches) {
    $movedSavedCars.Add((Move-ToQuarantine $hit.path $savedRoot (Join-Path $quarantineRoot "SavedGamesACE") "savedcar_liverylab" ([bool]$DryRun))) | Out-Null
    foreach ($thumb in @(Get-SavedCarThumbnailPaths $hit.guid)) {
        $movedSavedCars.Add((Move-ToQuarantine $thumb $savedRoot (Join-Path $quarantineRoot "SavedGamesACE") "savedcar_thumbnail" ([bool]$DryRun))) | Out-Null
    }
}
if ($includeBroken) {
    foreach ($hit in $possibleBroken) {
        $movedSavedCars.Add((Move-ToQuarantine $hit.path $savedRoot (Join-Path $quarantineRoot "SavedGamesACE") "savedcar_possible_broken" ([bool]$DryRun))) | Out-Null
        foreach ($thumb in @(Get-SavedCarThumbnailPaths $hit.guid)) {
            $movedSavedCars.Add((Move-ToQuarantine $thumb $savedRoot (Join-Path $quarantineRoot "SavedGamesACE") "savedcar_thumbnail_possible_broken" ([bool]$DryRun))) | Out-Null
        }
    }
}

$registryBackup = ""
$registryUpdated = $false
if (-not $DryRun -and (Test-Path -LiteralPath $mySkinsPath)) {
    $registryBackup = Join-Path $sessionDir "my_skins.before_garage_cleanup.json"
    Copy-Item -LiteralPath $mySkinsPath -Destination $registryBackup -Force
    foreach ($skin in $skins) {
        if ($skin -and ($skin.PSObject.Properties.Name -contains "active")) {
            $skin.active = $false
            $registryUpdated = $true
        }
        if ($skin -and ($skin.PSObject.Properties.Name -contains "gameSync")) {
            $skin.gameSync = $null
            $registryUpdated = $true
        }
    }
    if ($registryUpdated) {
        if ($registryHasWrapper) {
            $registryDoc.my_skins = @($skins)
            Write-Utf8NoBomFile $mySkinsPath ($registryDoc | ConvertTo-Json -Depth 80)
        } else {
            Write-Utf8NoBomFile $mySkinsPath ($skins | ConvertTo-Json -Depth 80)
        }
    }
}

$manifest = [ordered]@{
    action = "ac_evo_garage_cleanup"
    startedAt = $timestamp
    dryRun = [bool]$DryRun
    appRoot = $AppRoot
    gameRoot = $gameRoot
    mySkinsPath = $mySkinsPath
    settingsPath = $settingsPath
    managedSkinCount = $managedSkins.Count
    collectedRefs = @($refs.ToArray())
    savedCarMatches = @($savedCarMatches.ToArray())
    possibleBrokenSavedCars = @($possibleBroken.ToArray())
    possibleBrokenIncluded = $includeBroken
    movedSavedCars = @($movedSavedCars.ToArray())
    registryBackup = $registryBackup
    registryUpdated = $registryUpdated
    contentPackageUntouched = $true
    gameRootContentCleanupDisabled = $true
    completedAt = (Get-Date).ToString("s")
}

if (-not $DryRun) {
    New-Item -ItemType Directory -Force -Path $sessionDir | Out-Null
    Write-Utf8NoBomFile $manifestPath ($manifest | ConvertTo-Json -Depth 100)
}

Write-Host ""
Write-Info "Garage cleanup complete."
Write-Info "SavedCars moved: $(@($movedSavedCars | Where-Object { $_.status -in @('moved', 'dry_run') }).Count)"
Write-Info "Game-root files changed: 0"
if (-not $DryRun) {
    Write-Info "Manifest: $manifestPath"
}
Write-Host ""
Write-Host "You can now start LiveryLab or AC EVO again." -ForegroundColor White
