using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Text;
using Microsoft.Win32.SafeHandles;

namespace MBN_STOCK_WEBVIEW.Infrastructure;

/// <summary>
/// Creates the app-owned operator-data directory without following an existing
/// reparse ancestor, replaces inherited ACLs with a closed per-user ACL, and pins
/// every directory component against rename/delete while a writable store is alive.
/// </summary>
public static class TrustedManualDirectory
{
    public static string PreparePrivateWritableDirectory(string directory)
    {
        var fullPath = NormalizeAbsolutePath(directory);
        ValidateExistingAncestorsBeforeCreate(fullPath);
        Directory.CreateDirectory(fullPath);
        S5025TrustedManualFileDataSource.EnsureDirectoryIsTrusted(fullPath);

        if (OperatingSystem.IsWindows())
        {
            ApplyPrivateWindowsAcl(fullPath);
            ValidatePrivateWindowsAcl(fullPath);
        }

        S5025TrustedManualFileDataSource.EnsureDirectoryIsTrusted(fullPath);
        return fullPath;
    }

    internal static TrustedDirectoryLease AcquirePrivateWritableLease(string directory)
    {
        var fullPath = NormalizeAbsolutePath(directory);
        S5025TrustedManualFileDataSource.EnsureDirectoryIsTrusted(fullPath);
        if (OperatingSystem.IsWindows())
        {
            ValidatePrivateWindowsAcl(fullPath);
        }

        return TrustedDirectoryLease.Acquire(fullPath);
    }

    internal static string NormalizeAbsolutePath(string directory)
    {
        if (string.IsNullOrWhiteSpace(directory) ||
            directory.IndexOf('\0') >= 0 ||
            !Path.IsPathFullyQualified(directory))
        {
            throw S5025TrustedManualFileDataSource.InvalidData();
        }

        return Path.TrimEndingDirectorySeparator(Path.GetFullPath(directory));
    }

    private static void ValidateExistingAncestorsBeforeCreate(string fullPath)
    {
        var current = new DirectoryInfo(fullPath);
        while (!current.Exists)
        {
            if (File.Exists(current.FullName))
            {
                throw S5025TrustedManualFileDataSource.InvalidData();
            }

            current = current.Parent ??
                throw S5025TrustedManualFileDataSource.InvalidData();
        }

        S5025TrustedManualFileDataSource.EnsureDirectoryIsTrusted(current.FullName);
    }

    [SupportedOSPlatform("windows")]
    private static void ApplyPrivateWindowsAcl(string fullPath)
    {
        var currentUser = GetCurrentUserSid();
        var security = new DirectorySecurity();
        security.SetOwner(currentUser);
        security.SetAccessRuleProtection(isProtected: true, preserveInheritance: false);

        const InheritanceFlags inheritance =
            InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit;
        AddFullControl(security, currentUser, inheritance);
        AddFullControl(security, new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null), inheritance);
        AddFullControl(
            security,
            new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null),
            inheritance);

        new DirectoryInfo(fullPath).SetAccessControl(security);
    }

    [SupportedOSPlatform("windows")]
    private static void ValidatePrivateWindowsAcl(string fullPath)
    {
        var currentUser = GetCurrentUserSid();
        var system = new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null);
        var administrators = new SecurityIdentifier(
            WellKnownSidType.BuiltinAdministratorsSid,
            null);
        var allowed = new HashSet<string>(StringComparer.Ordinal)
        {
            currentUser.Value,
            system.Value,
            administrators.Value
        };

        var security = new DirectoryInfo(fullPath).GetAccessControl(
            AccessControlSections.Access | AccessControlSections.Owner);
        var owner = security.GetOwner(typeof(SecurityIdentifier)) as SecurityIdentifier;
        if (!security.AreAccessRulesProtected ||
            owner is null ||
            !allowed.Contains(owner.Value))
        {
            throw S5025TrustedManualFileDataSource.InvalidData();
        }

        var currentUserHasFullControl = false;
        foreach (FileSystemAccessRule rule in security.GetAccessRules(
                     includeExplicit: true,
                     includeInherited: true,
                     targetType: typeof(SecurityIdentifier)))
        {
            var sid = (SecurityIdentifier)rule.IdentityReference;
            if (rule.AccessControlType != AccessControlType.Allow ||
                !allowed.Contains(sid.Value))
            {
                throw S5025TrustedManualFileDataSource.InvalidData();
            }

            if (sid.Equals(currentUser) &&
                (rule.FileSystemRights & FileSystemRights.FullControl) ==
                FileSystemRights.FullControl)
            {
                currentUserHasFullControl = true;
            }
        }

        if (!currentUserHasFullControl)
        {
            throw S5025TrustedManualFileDataSource.InvalidData();
        }
    }

    [SupportedOSPlatform("windows")]
    private static SecurityIdentifier GetCurrentUserSid() =>
        WindowsIdentity.GetCurrent().User ??
        throw S5025TrustedManualFileDataSource.InvalidData();

    [SupportedOSPlatform("windows")]
    private static void AddFullControl(
        DirectorySecurity security,
        SecurityIdentifier identity,
        InheritanceFlags inheritance) =>
        security.AddAccessRule(new FileSystemAccessRule(
            identity,
            FileSystemRights.FullControl,
            inheritance,
            PropagationFlags.None,
            AccessControlType.Allow));
}

internal sealed class TrustedDirectoryLease : IDisposable
{
    private const uint GenericRead = 0x80000000;
    private const uint OpenExisting = 3;
    private const uint FileFlagBackupSemantics = 0x02000000;
    private const uint FileFlagOpenReparsePoint = 0x00200000;
    private const uint FileNameNormalized = 0;

    private readonly IReadOnlyList<SafeFileHandle> _directoryHandles;

    private TrustedDirectoryLease(IReadOnlyList<SafeFileHandle> directoryHandles)
    {
        _directoryHandles = directoryHandles;
    }

    public static TrustedDirectoryLease Acquire(string directory)
    {
        if (!OperatingSystem.IsWindows())
        {
            return new TrustedDirectoryLease(Array.Empty<SafeFileHandle>());
        }

        var ancestors = new List<string>();
        for (var current = new DirectoryInfo(directory); current is not null; current = current.Parent)
        {
            ancestors.Add(current.FullName);
        }
        ancestors.Reverse();

        var handles = new List<SafeFileHandle>(ancestors.Count);
        try
        {
            foreach (var ancestor in ancestors)
            {
                var handle = CreateFileW(
                    ancestor,
                    GenericRead,
                    FileShare.Read | FileShare.Write,
                    IntPtr.Zero,
                    OpenExisting,
                    FileFlagBackupSemantics | FileFlagOpenReparsePoint,
                    IntPtr.Zero);
                if (handle.IsInvalid)
                {
                    handle.Dispose();
                    throw new Win32Exception(Marshal.GetLastWin32Error());
                }

                EnsureDirectoryHandleIsDirect(handle, ancestor);
                handles.Add(handle);
            }

            return new TrustedDirectoryLease(handles.AsReadOnly());
        }
        catch
        {
            foreach (var handle in handles)
            {
                handle.Dispose();
            }

            throw S5025TrustedManualFileDataSource.InvalidData();
        }
    }

    public static void EnsureRegularFileHandle(
        SafeFileHandle handle,
        string expectedPath,
        long maximumLength,
        bool allowEmpty)
    {
        if (!OperatingSystem.IsWindows())
        {
            return;
        }

        if (!GetFileInformationByHandle(handle, out var information) ||
            (information.FileAttributes &
             (FileAttributes.Directory | FileAttributes.ReparsePoint)) != 0 ||
            information.NumberOfLinks != 1)
        {
            throw S5025TrustedManualFileDataSource.InvalidData();
        }

        var length = ((long)information.FileSizeHigh << 32) | information.FileSizeLow;
        if (length > maximumLength || (!allowEmpty && length <= 0))
        {
            throw S5025TrustedManualFileDataSource.InvalidData();
        }

        var resolved = GetFinalPath(handle);
        var expected = Path.GetFullPath(expectedPath);
        if (!string.Equals(resolved, expected, StringComparison.OrdinalIgnoreCase))
        {
            throw S5025TrustedManualFileDataSource.InvalidData();
        }
    }

    public void Dispose()
    {
        foreach (var handle in _directoryHandles.Reverse())
        {
            handle.Dispose();
        }
    }

    private static void EnsureDirectoryHandleIsDirect(SafeFileHandle handle, string expectedPath)
    {
        if (!GetFileInformationByHandle(handle, out var information) ||
            (information.FileAttributes & FileAttributes.Directory) == 0 ||
            (information.FileAttributes & FileAttributes.ReparsePoint) != 0 ||
            !string.Equals(
                GetFinalPath(handle),
                Path.GetFullPath(expectedPath),
                StringComparison.OrdinalIgnoreCase))
        {
            throw S5025TrustedManualFileDataSource.InvalidData();
        }
    }

    private static string GetFinalPath(SafeFileHandle handle)
    {
        var capacity = 512;
        while (capacity <= 32_768)
        {
            var buffer = new StringBuilder(capacity);
            var length = GetFinalPathNameByHandleW(
                handle,
                buffer,
                (uint)buffer.Capacity,
                FileNameNormalized);
            if (length == 0)
            {
                throw new Win32Exception(Marshal.GetLastWin32Error());
            }

            if (length < buffer.Capacity)
            {
                var path = buffer.ToString();
                if (path.StartsWith("\\\\?\\UNC\\", StringComparison.OrdinalIgnoreCase))
                {
                    path = "\\\\" + path[8..];
                }
                else if (path.StartsWith("\\\\?\\", StringComparison.Ordinal))
                {
                    path = path[4..];
                }

                return Path.GetFullPath(path);
            }

            capacity = checked((int)length + 1);
        }

        throw S5025TrustedManualFileDataSource.InvalidData();
    }

    [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    private static extern SafeFileHandle CreateFileW(
        string fileName,
        uint desiredAccess,
        FileShare shareMode,
        IntPtr securityAttributes,
        uint creationDisposition,
        uint flagsAndAttributes,
        IntPtr templateFile);

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool GetFileInformationByHandle(
        SafeFileHandle file,
        out ByHandleFileInformation information);

    [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    private static extern uint GetFinalPathNameByHandleW(
        SafeFileHandle file,
        StringBuilder path,
        uint pathLength,
        uint flags);

    [StructLayout(LayoutKind.Sequential)]
    private struct ByHandleFileInformation
    {
        public FileAttributes FileAttributes;
        public FileTime CreationTime;
        public FileTime LastAccessTime;
        public FileTime LastWriteTime;
        public uint VolumeSerialNumber;
        public uint FileSizeHigh;
        public uint FileSizeLow;
        public uint NumberOfLinks;
        public uint FileIndexHigh;
        public uint FileIndexLow;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct FileTime
    {
        public uint Low;
        public uint High;
    }
}
