[CmdletBinding()]
param(
    [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
    [ValidateNotNullOrEmpty()]
    [string[]]$Path,

    [Parameter()]
    [string]$ExportJsonPath
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

if ([System.Environment]::OSVersion.Platform -ne [System.PlatformID]::Win32NT) {
    throw 'Dieses Demo-Script kann nur unter Windows ausgefuehrt werden.'
}

$nativeMethods = @"
using System;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;

public static class NtfsNativeMethods
{
    [StructLayout(LayoutKind.Sequential)]
    public struct BY_HANDLE_FILE_INFORMATION
    {
        public uint FileAttributes;
        public System.Runtime.InteropServices.ComTypes.FILETIME CreationTime;
        public System.Runtime.InteropServices.ComTypes.FILETIME LastAccessTime;
        public System.Runtime.InteropServices.ComTypes.FILETIME LastWriteTime;
        public uint VolumeSerialNumber;
        public uint FileSizeHigh;
        public uint FileSizeLow;
        public uint NumberOfLinks;
        public uint FileIndexHigh;
        public uint FileIndexLow;
    }

    public const uint FILE_SHARE_READ = 0x00000001;
    public const uint FILE_SHARE_WRITE = 0x00000002;
    public const uint FILE_SHARE_DELETE = 0x00000004;
    public const uint OPEN_EXISTING = 3;
    public const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;

    [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    public static extern SafeFileHandle CreateFile(
        string lpFileName,
        uint dwDesiredAccess,
        uint dwShareMode,
        IntPtr lpSecurityAttributes,
        uint dwCreationDisposition,
        uint dwFlagsAndAttributes,
        IntPtr hTemplateFile);

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool GetFileInformationByHandle(
        SafeFileHandle hFile,
        out BY_HANDLE_FILE_INFORMATION lpFileInformation);
}
"@

if (-not ('NtfsNativeMethods' -as [type])) {
    Add-Type -TypeDefinition $nativeMethods
}

function Get-Md5Hex {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Text
    )

    $md5 = [System.Security.Cryptography.MD5]::Create()
    try {
        $bytes = [System.Text.Encoding]::UTF8.GetBytes($Text)
        $hash = $md5.ComputeHash($bytes)
        return ([System.BitConverter]::ToString($hash)).Replace('-', '').ToLowerInvariant()
    }
    finally {
        $md5.Dispose()
    }
}

function Get-NtfsStableFolderId {
    param(
        [Parameter(Mandatory = $true)]
        [string]$LiteralPath
    )

    $item = Get-Item -LiteralPath $LiteralPath -Force
    if (-not $item.PSIsContainer) {
        throw "Der Pfad '$LiteralPath' ist kein Ordner."
    }

    $handle = [NtfsNativeMethods]::CreateFile(
        $item.FullName,
        0,
        [NtfsNativeMethods]::FILE_SHARE_READ -bor [NtfsNativeMethods]::FILE_SHARE_WRITE -bor [NtfsNativeMethods]::FILE_SHARE_DELETE,
        [IntPtr]::Zero,
        [NtfsNativeMethods]::OPEN_EXISTING,
        [NtfsNativeMethods]::FILE_FLAG_BACKUP_SEMANTICS,
        [IntPtr]::Zero)

    if ($handle.IsInvalid) {
        $lastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
        throw "CreateFile fehlgeschlagen fuer '$($item.FullName)' mit Win32-Fehler $lastError."
    }

    try {
        $info = New-Object NtfsNativeMethods+BY_HANDLE_FILE_INFORMATION
        if (-not [NtfsNativeMethods]::GetFileInformationByHandle($handle, [ref]$info)) {
            $lastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
            throw "GetFileInformationByHandle fehlgeschlagen fuer '$($item.FullName)' mit Win32-Fehler $lastError."
        }

        $fileIndex = ([UInt64]$info.FileIndexHigh -shl 32) -bor [UInt64]$info.FileIndexLow
        $stableAnchor = ('{0:x8}:{1:x16}' -f $info.VolumeSerialNumber, $fileIndex)

        [pscustomobject]@{
            Path                  = $item.FullName
            Name                  = $item.Name
            VolumeSerialNumberHex = ('0x{0:x8}' -f $info.VolumeSerialNumber)
            FileIndexHex          = ('0x{0:x16}' -f $fileIndex)
            StableAnchor          = $stableAnchor
            StableDataAreaId      = (Get-Md5Hex -Text $stableAnchor)
            CreatedUtc            = $item.CreationTimeUtc.ToString('o')
            LastWriteUtc          = $item.LastWriteTimeUtc.ToString('o')
        }
    }
    finally {
        $handle.Dispose()
    }
}

$results = foreach ($currentPath in $Path) {
    Get-NtfsStableFolderId -LiteralPath $currentPath
}

if ($PSBoundParameters.ContainsKey('ExportJsonPath')) {
    $exportDirectory = Split-Path -Parent $ExportJsonPath
    if (-not [string]::IsNullOrWhiteSpace($exportDirectory) -and -not (Test-Path -LiteralPath $exportDirectory)) {
        New-Item -ItemType Directory -Path $exportDirectory | Out-Null
    }

    $results | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $ExportJsonPath -Encoding UTF8
}

$results

<#
Demo fuer Rename/Move auf demselben NTFS-Volume:

1. Vorher ausfuehren:
   .\Get-NtfsStableFolderId.ps1 -Path 'C:\Daten\TeamA' -ExportJsonPath '.\before.json'

2. Ordner umbenennen oder innerhalb desselben Volumes verschieben.

3. Nachher erneut ausfuehren:
   .\Get-NtfsStableFolderId.ps1 -Path 'C:\Daten\Fachbereich\TeamA_Renamed' -ExportJsonPath '.\after.json'

4. Vergleichen:
   - `Path` ist anders
   - `StableAnchor` bleibt gleich
   - `StableDataAreaId` bleibt gleich

Wichtige Einschraenkung:

- Rename und Move auf demselben NTFS-Volume behalten die Identitaet.
- Copy-and-delete oder Move ueber ein anderes Volume erzeugen in der Regel eine neue Identitaet.
- Auf manchen Remote-/DFS-Szenarien kann der Dateiserver diese Metadaten nicht verlaesslich liefern.
#>
