using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Win32.SafeHandles;

namespace MBN_STOCK_WEBVIEW.Playout.Configuration;

/// <summary>
/// Resolves a read-only vendor asset from one already-trusted root. Windows handles
/// are used for the directory ancestry and the file itself so that path checks do not
/// silently follow a reparse point or accept a hard-linked file.
/// </summary>
internal static class TrustedPlayoutAssetPath
{
    public static string ValidateRoot(string root)
    {
        if (string.IsNullOrWhiteSpace(root) || root.IndexOf('\0') >= 0 ||
            !Path.IsPathFullyQualified(root))
        {
            throw Invalid();
        }

        var fullRoot = Path.TrimEndingDirectorySeparator(Path.GetFullPath(root));
        if (!Directory.Exists(fullRoot))
        {
            throw Invalid();
        }

        using var lease = TrustedReadOnlyDirectoryLease.Acquire(fullRoot);
        return fullRoot;
    }

    public static string ResolveRelativeFile(
        string root,
        string relativePath,
        IReadOnlySet<string> allowedExtensions)
    {
        ArgumentNullException.ThrowIfNull(allowedExtensions);
        relativePath = relativePath?.Trim() ?? string.Empty;
        if (relativePath.Length == 0 || relativePath.Length > 512 ||
            Path.IsPathRooted(relativePath) ||
            relativePath.Contains("..", StringComparison.Ordinal) ||
            relativePath.Contains(':') || relativePath.Any(char.IsControl) ||
            !allowedExtensions.Contains(Path.GetExtension(relativePath)))
        {
            throw Invalid();
        }

        var fullRoot = ValidateRoot(root);
        var candidate = Path.GetFullPath(Path.Combine(fullRoot, relativePath));
        return ResolveCanonicalFile(fullRoot, candidate);
    }

    public static string ResolveAbsoluteFile(
        string root,
        string absolutePath,
        IReadOnlySet<string> allowedExtensions)
    {
        ArgumentNullException.ThrowIfNull(allowedExtensions);
        if (string.IsNullOrWhiteSpace(absolutePath) || absolutePath.Length > 32_767 ||
            absolutePath.Any(char.IsControl) || !Path.IsPathFullyQualified(absolutePath) ||
            !allowedExtensions.Contains(Path.GetExtension(absolutePath)))
        {
            throw Invalid();
        }

        var fullRoot = ValidateRoot(root);
        var candidate = Path.GetFullPath(absolutePath);
        return ResolveCanonicalFile(fullRoot, candidate);
    }

    internal static void EnsureOpenedRegularFile(
        SafeFileHandle handle,
        string expectedPath)
    {
        if (!OperatingSystem.IsWindows())
        {
            var attributes = File.GetAttributes(expectedPath);
            if ((attributes & (FileAttributes.Directory | FileAttributes.ReparsePoint)) != 0)
            {
                throw Invalid();
            }

            return;
        }

        if (handle.IsInvalid ||
            !GetFileInformationByHandle(handle, out var information) ||
            (information.FileAttributes &
             (FileAttributes.Directory | FileAttributes.ReparsePoint)) != 0 ||
            information.NumberOfLinks != 1 ||
            (((long)information.FileSizeHigh << 32) | information.FileSizeLow) <= 0 ||
            !string.Equals(
                GetFinalPath(handle),
                Path.GetFullPath(expectedPath),
                StringComparison.OrdinalIgnoreCase))
        {
            throw Invalid();
        }
    }

    private static string ResolveCanonicalFile(string fullRoot, string candidate)
    {
        var rootPrefix = fullRoot + Path.DirectorySeparatorChar;
        if (!candidate.StartsWith(rootPrefix, StringComparison.OrdinalIgnoreCase) ||
            !File.Exists(candidate))
        {
            throw Invalid();
        }

        try
        {
            // The lease excludes FILE_SHARE_DELETE for every ancestor while the file
            // handle is checked. The file handle itself is also bound to the canonical
            // final path and must have exactly one link.
            using var lease = TrustedReadOnlyDirectoryLease.Acquire(
                Path.GetDirectoryName(candidate) ?? throw Invalid());
            using var stream = new FileStream(
                candidate,
                FileMode.Open,
                FileAccess.Read,
                FileShare.Read,
                bufferSize: 1,
                FileOptions.RandomAccess);
            EnsureOpenedRegularFile(stream.SafeFileHandle, candidate);
            return candidate;
        }
        catch (TrustedPlayoutAssetException)
        {
            throw;
        }
        catch
        {
            throw Invalid();
        }
    }

    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 Invalid();
    }

    private static TrustedPlayoutAssetException Invalid() => new();

    private const uint FileNameNormalized = 0;

    [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;
    }
}

internal sealed class TrustedReadOnlyDirectoryLease : 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> _handles;

    private TrustedReadOnlyDirectoryLease(IReadOnlyList<SafeFileHandle> handles)
    {
        _handles = handles;
    }

    public static TrustedReadOnlyDirectoryLease Acquire(string directory)
    {
        if (!OperatingSystem.IsWindows())
        {
            var attributes = File.GetAttributes(directory);
            if ((attributes & (FileAttributes.Directory | FileAttributes.ReparsePoint)) !=
                FileAttributes.Directory)
            {
                throw new TrustedPlayoutAssetException();
            }

            return new TrustedReadOnlyDirectoryLease(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());
                }

                try
                {
                    EnsureDirectDirectory(handle, ancestor);
                }
                catch
                {
                    handle.Dispose();
                    throw;
                }
                handles.Add(handle);
            }

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

            throw new TrustedPlayoutAssetException();
        }
    }

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

    private static void EnsureDirectDirectory(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 new TrustedPlayoutAssetException();
        }
    }

    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 new TrustedPlayoutAssetException();
    }

    [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;
    }
}

internal sealed class TrustedPlayoutAssetException : Exception;
