using C4IT.Logging;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.DirectoryServices.AccountManagement;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace LiamNtfs
{
    public class cNtfsBase
    {
        private const int ErrorSessionCredentialConflict = 1219;
        private const int ErrorNotConnected = 2250;

        private cNtfsLogonInfo privLogonInfo = null;
        private int scanningDepth;
        public PrincipalContext adContext = null;
        public Exception LastException { get; private set; } = null;
        public string LastErrorMessage { get; private set; } = null;

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void ResetError()
        {
            LastException = null;
            LastErrorMessage = null;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void SetErrorException(string Action, Exception E, LogLevels lev = LogLevels.Error)
        {
            LastException = E;
            LastErrorMessage = Action + ": " + E.Message;
            cLogManager.LogEntry(Action, lev);
        }

        private async Task<bool> privLogonAsync(cNtfsLogonInfo LogonInfo)
        {
            try
            {
                //TODO: remove dummy delay?
                await Task.Delay(0);
                ResetError();
                var netResource = new NetResource()
                {
                    Scope = ResourceScope.GlobalNetwork,
                    ResourceType = ResourceType.Disk,
                    DisplayType = ResourceDisplaytype.Share,
                    RemoteName = LogonInfo.TargetNetworkName
                };

                var result = WNetAddConnection2(
                    netResource,
                    LogonInfo.UserSecret,
                    LogonInfo.User,
                    0);

                if (result == ErrorSessionCredentialConflict)
                {
                    var originalResult = result;
                    var cancelResult = WNetCancelConnection2(LogonInfo.TargetNetworkName, 0, true);

                    if (cancelResult == 0 || cancelResult == ErrorNotConnected)
                    {
                        result = WNetAddConnection2(
                            netResource,
                            LogonInfo.UserSecret,
                            LogonInfo.User,
                            0);

                        if (result == 0)
                        {
                            cLogManager.LogEntry(
                                $"NTFS login retry succeeded after SMB conflict on '{LogonInfo.TargetNetworkName}'. Initial add returned {originalResult} ({BuildWin32Message(originalResult)}), cancel returned {cancelResult} ({BuildWin32Message(cancelResult)}).",
                                LogLevels.Debug);
                        }
                        else
                        {
                            throw CreateNtfsLoginException(LogonInfo, originalResult, cancelResult, result);
                        }
                    }
                    else
                    {
                        throw CreateNtfsLoginException(LogonInfo, originalResult, cancelResult, null);
                    }
                }

                if (result != 0)
                {
                    throw CreateNtfsLoginException(LogonInfo, result, null, null);
                }
                var FSLogon =  true;

                adContext = new PrincipalContext(ContextType.Domain, LogonInfo.Domain, LogonInfo.User, new NetworkCredential("", LogonInfo.UserSecret).Password);

                return FSLogon && adContext != null;
            }
            catch (Exception E)
            {
                SetErrorException("exception error while Ntfs login", E, LogLevels.Debug);
                cLogManager.LogException(E, LogLevels.Debug);
            }

            return false;
        }

        private static Exception CreateNtfsLoginException(cNtfsLogonInfo logonInfo, int addResult, int? cancelResult, int? retryResult)
        {
            var stages = new List<string>
            {
                $"WNetAddConnection2={addResult} ({BuildWin32Message(addResult)})"
            };

            if (cancelResult.HasValue)
                stages.Add($"WNetCancelConnection2('{logonInfo?.TargetNetworkName}')={cancelResult.Value} ({BuildWin32Message(cancelResult.Value)})");

            if (retryResult.HasValue)
                stages.Add($"RetryAdd={retryResult.Value} ({BuildWin32Message(retryResult.Value)})");

            var details = string.Join(", ", stages);
            var message = $"NTFS login to '{logonInfo?.TargetNetworkName}' for user '{logonInfo?.User}' failed. {details}.";

            if (addResult == ErrorSessionCredentialConflict)
            {
                message += " This usually indicates an existing SMB/DFS session to the same server with a different credential context.";

                if (cancelResult == ErrorNotConnected)
                    message += " The exact DFS/share path was not registered as a disconnectable connection, so the conflicting session is likely held under a different remote name or DFS target.";
            }

            var nativeResult = retryResult ?? cancelResult ?? addResult;
            return new InvalidOperationException(message, new Win32Exception(nativeResult));
        }

        private static string BuildWin32Message(int errorCode)
        {
            return new Win32Exception(errorCode).Message;
        }

        private async Task<bool> privRelogon()
        {
            if (privLogonInfo == null)
                return false;
            var RetVal = await privLogonAsync(privLogonInfo);
            return RetVal;
        }

        public async Task<bool> LogonAsync(cNtfsLogonInfo LogonInfo)
        {
            var RetVal = await privLogonAsync(LogonInfo);
            if (RetVal == true)
                privLogonInfo = LogonInfo;
            return RetVal;
        }

        [DllImport("mpr.dll")]
        private static extern int WNetAddConnection2(NetResource netResource, string password, string username, int flags);
        
        [DllImport("mpr.dll")]
        private static extern int WNetCancelConnection2(string name, int flags,
        bool force);
        public async Task<cNtfsCollectionBase> RequestFoldersListAsync(string rootPath, int depth)
        {
            ResetError();
            try
            {
                await Task.Delay(0);
                if (depth == 0)
                    return null;

                var Result = privRequestFoldersListAsync(rootPath, depth);
                if (Result != null)
                {
                    var RetVal = new cNtfsCollectionBase(Result.Count);
                    foreach (var Entry in Result)
                    {
                        var res = new cNtfsResultFolder((cNtfsResultFolder)Entry);
                        RetVal.Add(res);
                    }
                    return RetVal;
                }
            }
            catch (Exception E)
            {
                cLogManager.LogException(E);
            }

            return null;
        }
        private List<cNtfsResultBase> privRequestFoldersListAsync(string rootPath, int depth)
        {
            if (String.IsNullOrEmpty(rootPath))
                return new List<cNtfsResultBase>();
            scanningDepth = depth;
            return privRequestFoldersListAsync(new DirectoryInfo(rootPath), depth);
        }
        private List<cNtfsResultBase> privRequestFoldersListAsync(DirectoryInfo rootPath, int depth, cNtfsResultFolder parent = null)
        {
            ResetError();
            var folders = new List<cNtfsResultBase>();
            try
            {
                if (depth == 0)
                    return folders;

                DirectoryInfo[] directories;
                try
                {
                    directories = rootPath.GetDirectories();
                }
                catch (Exception E)
                {
                    cLogManager.LogEntry($"Could not enumerate directories under '{rootPath.FullName}': {E.Message}", LogLevels.Warning);
                    cLogManager.LogException(E, LogLevels.Debug);
                    return folders;
                }

                foreach (var directory in directories)
                {
                    cNtfsResultFolder folder;
                    try
                    {
                        folder = new cNtfsResultFolder()
                        {
                            ID = generateUniquId(directory.FullName),
                            Path = directory.FullName,
                            CreatedDate = directory.CreationTimeUtc.ToString("s"),
                            DisplayName = directory.Name,
                            Level = scanningDepth - depth + 1,
                            Parent = parent
                        };
                    }
                    catch (Exception E)
                    {
                        cLogManager.LogEntry($"Could not read directory metadata for '{directory.FullName}': {E.Message}", LogLevels.Warning);
                        cLogManager.LogException(E, LogLevels.Debug);
                        continue;
                    }

                    folders.Add(folder);
                    if (depth <= 0)
                        continue;

                    try
                    {
                        var result = privRequestFoldersListAsync(directory, depth - 1, folder);
                        if (result != null && result.Count > 0)
                            folders.AddRange(result);
                    }
                    catch (Exception E)
                    {
                        cLogManager.LogEntry($"Could not scan subtree '{directory.FullName}': {E.Message}", LogLevels.Warning);
                        cLogManager.LogException(E, LogLevels.Debug);
                    }
                }

                return folders;
            }
            catch (Exception E)
            {
                cLogManager.LogException(E);
            }

            return null;
        }



        public async Task<cNtfsCollectionBase> RequestFoldersListAsync(cNtfsResultBase folder, cNtfsResultBase parent)
        {
            ResetError();
            try
            {
                await Task.Delay(0);
                var Result = new List<cNtfsResultBase>();

                if (Result != null)
                {
                    var RetVal = new cNtfsCollectionBase(Result.Count);
                    foreach (var Entry in Result)
                    {
                        var res = new cNtfsResultFolder((cNtfsResultFolder)Entry);
                        res.Parent = parent;
                        RetVal.Add(res);
                    }
                    return RetVal;
                }
            }
            catch (Exception E)
            {
                cLogManager.LogException(E);
            }

            return null;
        }
        internal static int getDepth(DirectoryInfo root, DirectoryInfo folder)
        {
            var rootDepth = root.FullName.TrimEnd(Path.DirectorySeparatorChar).Split(System.IO.Path.DirectorySeparatorChar).Length;
            var folderDepth = folder.FullName.TrimEnd(Path.DirectorySeparatorChar).Split(System.IO.Path.DirectorySeparatorChar).Length;
            return folderDepth - rootDepth;

        }
        internal static int getDepth(string root, string folder)
        {
            return getDepth(new DirectoryInfo(root), new DirectoryInfo(folder));

        }
        internal static string generateUniquId(string name)
        {
            try
            {
                var md5 = new System.Security.Cryptography.MD5CryptoServiceProvider();
                var utf8 = new System.Text.UTF8Encoding();
                var hash = BitConverter.ToString(md5.ComputeHash(utf8.GetBytes(name)));
                hash = hash.ToLower().Replace("-", "");
                return hash;
            }
            catch (Exception E)
            {
                cLogManager.DefaultLogger.LogException(E);
                throw;
            }
        }
    }


    public class cNtfsLogonInfo
    {
        public string Domain;
        public string DomainControllers;
        public string User;
        public string UserSecret;
        public string TargetNetworkName;
        public string TargetGroupPath;
    }

    public class cNtfsResultBase
    {
        public string ID { get; set; } = null;
        public string DisplayName { get;  set; } = null;
        public string Path { get; set; } = null;
        public int Level { get; set; } = -1;
        public cNtfsResultBase()
        { }
        public cNtfsResultBase(cNtfsResultBase Result)
        {
            if (Result == null)
                return;

            ID = Result.ID;
            DisplayName = Result.DisplayName;
            Path = Result.Path;

        }
    }
    public class cNtfsResultFolder : cNtfsResultBase
    {
        public string Description { get; set; } = null;
        public string Visibility { get; set; } = null;
        public string OwnerADGroup { get; set; } = null;
        public string WriteADGroup { get; set; } = null;
        public string ReadADGroup { get; set; } = null;
        public cNtfsResultBase Parent { get;set;} = null;
        public string CreatedDate { get; set; } = DateTime.MinValue.ToString("s");
        public cNtfsResultFolder Folders { get; private set; } = null;
        public cNtfsResultFolder(cNtfsResultFolder Result) : base(Result)
        {
            Description = Result.Description;
            Visibility = Result.Visibility;
            CreatedDate = Result.CreatedDate;
            Folders = Result.Folders;
            Level = Result.Level;
            Parent = Result.Parent;
            OwnerADGroup = Result.OwnerADGroup;
            WriteADGroup = Result.WriteADGroup;
            ReadADGroup = Result.ReadADGroup;
        }

        public cNtfsResultFolder()
        {
        }
    }
    public class cNtfsResultShare : cNtfsResultBase
    {
        public cNtfsCollectionBase Folders { get; private set; } = null;
        public cNtfsResultShare(cNtfsResultBase Result) : base(Result)     {        }

        public cNtfsResultShare()
        {
        }

        public async Task<bool> ResolveFolders(cNtfsBase ntfs)
        {
            Folders = await ntfs.RequestFoldersListAsync(this.Path, 0);

            return Folders != null;
        }
    }

    public class cNtfsCollectionBase : SortedList<string, cNtfsResultBase>
    {
        public cNtfsCollectionBase() { }
        public cNtfsCollectionBase(int n) : base(n) { }

        public void Add(cNtfsResultBase Folder)
        {
            if (!this.ContainsKey(Folder.ID))
                this.Add(Folder.ID, Folder);
        }
    }
    public class cFileshareLogonInfo
    {
        public string Domain;
        public string User;
        public string UserSecret;
        public string TargetNetworkName;
    }

    [StructLayout(LayoutKind.Sequential)]
    public class NetResource
    {
        public ResourceScope Scope;
        public ResourceType ResourceType;
        public ResourceDisplaytype DisplayType;
        public int Usage;
        public string LocalName;
        public string RemoteName;
        public string Comment;
        public string Provider;
    }

    public enum ResourceScope : int
    {
        Connected = 1,
        GlobalNetwork,
        Remembered,
        Recent,
        Context
    };

    public enum ResourceType : int
    {
        Any = 0,
        Disk = 1,
        Print = 2,
        Reserved = 8,
    }

    public enum ResourceDisplaytype : int
    {
        Generic = 0x0,
        Domain = 0x01,
        Server = 0x02,
        Share = 0x03,
        File = 0x04,
        Group = 0x05,
        Network = 0x06,
        Root = 0x07,
        Shareadmin = 0x08,
        Directory = 0x09,
        Tree = 0x0a,
        Ndscontainer = 0x0b
    }

    public enum NetError : uint
    {
        NERR_Success = 0,
        NERR_BASE = 2100,
        NERR_UnknownDevDir = (NERR_BASE + 16),
        NERR_DuplicateShare = (NERR_BASE + 18),
        NERR_BufTooSmall = (NERR_BASE + 23),
    }
    public enum SHARE_TYPE : uint
    {
        STYPE_DISKTREE = 0,
        STYPE_PRINTQ = 1,
        STYPE_DEVICE = 2,
        STYPE_IPC = 3,
        STYPE_SPECIAL = 0x80000000,
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    public struct SHARE_INFO_1
    {
        public string shi1_netname;
        public uint shi1_type;
        public string shi1_remark;
        public SHARE_INFO_1(string sharename, uint sharetype, string remark)
        {
            this.shi1_netname = sharename;
            this.shi1_type = sharetype;
            this.shi1_remark = remark;
        }
        public override string ToString()
        {
            return shi1_netname;
        }
    }
}
