"""
Windows specific utility functions, this module should be imported in a try,
except block because it is only applicable on Windows platforms.


Much of what is here was adapted from the following:

    https://stackoverflow.com/a/43233332
    http://stackoverflow.com/questions/29566330
"""

import base64
import collections
import ctypes
import logging
import os
import re
import subprocess
from ctypes import wintypes

# pylint: disable=3rd-party-module-not-gated
import ntsecuritycon
import psutil
import win32api
import win32con
import win32process
import win32security
import win32service

# pylint: enable=3rd-party-module-not-gated

# Set up logging
log = logging.getLogger(__name__)

ntdll = ctypes.WinDLL("ntdll")
secur32 = ctypes.WinDLL("secur32")
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
advapi32 = ctypes.WinDLL("advapi32", use_last_error=True)
userenv = ctypes.WinDLL("userenv", use_last_error=True)

SYSTEM_SID = "S-1-5-18"
LOCAL_SRV_SID = "S-1-5-19"
NETWORK_SRV_SID = "S-1-5-19"

# STARTUPINFO
STARTF_USESHOWWINDOW = 0x00000001
STARTF_USESTDHANDLES = 0x00000100

# dwLogonFlags
LOGON_WITH_PROFILE = 0x00000001

# Process Creation Flags
CREATE_NEW_CONSOLE = 0x00000010
CREATE_NO_WINDOW = 0x08000000
CREATE_SUSPENDED = 0x00000004
CREATE_UNICODE_ENVIRONMENT = 0x00000400

WINSTA_ALL = (
    win32con.WINSTA_ACCESSCLIPBOARD
    | win32con.WINSTA_ACCESSGLOBALATOMS
    | win32con.WINSTA_CREATEDESKTOP
    | win32con.WINSTA_ENUMDESKTOPS
    | win32con.WINSTA_ENUMERATE
    | win32con.WINSTA_EXITWINDOWS
    | win32con.WINSTA_READATTRIBUTES
    | win32con.WINSTA_READSCREEN
    | win32con.WINSTA_WRITEATTRIBUTES
    | win32con.DELETE
    | win32con.READ_CONTROL
    | win32con.WRITE_DAC
    | win32con.WRITE_OWNER
)

DESKTOP_ALL = (
    win32con.DESKTOP_CREATEMENU
    | win32con.DESKTOP_CREATEWINDOW
    | win32con.DESKTOP_ENUMERATE
    | win32con.DESKTOP_HOOKCONTROL
    | win32con.DESKTOP_JOURNALPLAYBACK
    | win32con.DESKTOP_JOURNALRECORD
    | win32con.DESKTOP_READOBJECTS
    | win32con.DESKTOP_SWITCHDESKTOP
    | win32con.DESKTOP_WRITEOBJECTS
    | win32con.DELETE
    | win32con.READ_CONTROL
    | win32con.WRITE_DAC
    | win32con.WRITE_OWNER
)

MAX_COMPUTER_NAME_LENGTH = 15

SECURITY_LOGON_TYPE = wintypes.ULONG
Interactive = 2
Network = 3
Batch = 4
Service = 5

LOGON_SUBMIT_TYPE = wintypes.ULONG
PROFILE_BUFFER_TYPE = wintypes.ULONG

MsV1_0InteractiveLogon = 2
MsV1_0Lm20Logon = 3
MsV1_0NetworkLogon = 4
MsV1_0WorkstationUnlockLogon = 7
MsV1_0S4ULogon = 12
MsV1_0NoElevationLogon = 82

KerbInteractiveLogon = 2
KerbWorkstationUnlockLogon = 7
KerbS4ULogon = 12

MSV1_0_S4U_LOGON_FLAG_CHECK_LOGONHOURS = 0x2

KERB_S4U_LOGON_FLAG_CHECK_LOGONHOURS = 0x2
KERB_S4U_LOGON_FLAG_IDENTITY = 0x8

TOKEN_SOURCE_LENGTH = 8

NEGOTIATE_PACKAGE_NAME = b"Negotiate"
MICROSOFT_KERBEROS_NAME = b"Kerberos"
MSV1_0_PACKAGE_NAME = b"MICROSOFT_AUTHENTICATION_PACKAGE_V1_0"

DELETE = 0x00010000
READ_CONTROL = 0x00020000
WRITE_DAC = 0x00040000
WRITE_OWNER = 0x00080000

STANDARD_RIGHTS_REQUIRED = DELETE | READ_CONTROL | WRITE_DAC | WRITE_OWNER

TOKEN_ASSIGN_PRIMARY = 0x0001
TOKEN_DUPLICATE = 0x0002
TOKEN_IMPERSONATE = 0x0004
TOKEN_QUERY = 0x0008
TOKEN_QUERY_SOURCE = 0x0010
TOKEN_ADJUST_PRIVILEGES = 0x0020
TOKEN_ADJUST_GROUPS = 0x0040
TOKEN_ADJUST_DEFAULT = 0x0080
TOKEN_ADJUST_SESSIONID = 0x0100

TOKEN_ALL_ACCESS = (
    STANDARD_RIGHTS_REQUIRED
    | TOKEN_ASSIGN_PRIMARY
    | TOKEN_DUPLICATE
    | TOKEN_IMPERSONATE
    | TOKEN_QUERY
    | TOKEN_QUERY_SOURCE
    | TOKEN_ADJUST_PRIVILEGES
    | TOKEN_ADJUST_GROUPS
    | TOKEN_ADJUST_DEFAULT
    | TOKEN_ADJUST_SESSIONID
)

DUPLICATE_CLOSE_SOURCE = 0x00000001
DUPLICATE_SAME_ACCESS = 0x00000002

TOKEN_TYPE = wintypes.ULONG
TokenPrimary = 1
TokenImpersonation = 2

SECURITY_IMPERSONATION_LEVEL = wintypes.ULONG
SecurityAnonymous = 0
SecurityIdentification = 1
SecurityImpersonation = 2
SecurityDelegation = 3


class NTSTATUS(wintypes.LONG):
    def to_error(self):
        return ntdll.RtlNtStatusToDosError(self)

    def __repr__(self):
        name = self.__class__.__name__
        status = wintypes.ULONG.from_buffer(self)
        return f"{name}({status.value})"


PNTSTATUS = ctypes.POINTER(NTSTATUS)


class BOOL(wintypes.BOOL):
    def __repr__(self):
        name = self.__class__.__name__
        return f"{name}({bool(self)})"


class HANDLE(wintypes.HANDLE):
    __slots__ = ("closed",)

    def __int__(self):
        return self.value or 0

    def Detach(self):
        if not getattr(self, "closed", False):
            self.closed = True
            value = int(self)
            self.value = None
            return value
        raise ValueError("already closed")

    def Close(self, CloseHandle=kernel32.CloseHandle):
        if self and not getattr(self, "closed", False):
            try:
                CloseHandle(self.Detach())
            except OSError:
                # Suppress the error when there is no handle (WinError 6)
                if ctypes.get_last_error() == 6:
                    pass

    __del__ = Close

    def __repr__(self):
        return f"{self.__class__.__name__}({int(self)})"


class LARGE_INTEGER(wintypes.LARGE_INTEGER):
    # https://msdn.microsoft.com/en-us/library/ff553204
    ntdll.RtlSecondsSince1970ToTime.restype = None
    _unix_epoch = wintypes.LARGE_INTEGER()
    ntdll.RtlSecondsSince1970ToTime(0, ctypes.byref(_unix_epoch))
    _unix_epoch = _unix_epoch.value

    def __int__(self):
        return self.value

    def __repr__(self):
        name = self.__class__.__name__
        return f"{name}({self.value})"

    def as_time(self):
        time100ns = self.value - self._unix_epoch
        if time100ns >= 0:
            return time100ns / 1e7
        raise ValueError("value predates the Unix epoch")

    @classmethod
    def from_time(cls, t):
        time100ns = int(t * 10**7)
        return cls(time100ns + cls._unix_epoch)


CHAR = ctypes.c_char
WCHAR = ctypes.c_wchar
PCHAR = ctypes.POINTER(CHAR)
PWCHAR = ctypes.POINTER(WCHAR)


class STRING(ctypes.Structure):
    _fields_ = (
        ("Length", wintypes.USHORT),
        ("MaximumLength", wintypes.USHORT),
        ("Buffer", PCHAR),
    )


LPSTRING = ctypes.POINTER(STRING)


class UNICODE_STRING(ctypes.Structure):
    _fields_ = (
        ("Length", wintypes.USHORT),
        ("MaximumLength", wintypes.USHORT),
        ("Buffer", PWCHAR),
    )


LPUNICODE_STRING = ctypes.POINTER(UNICODE_STRING)


class LUID(ctypes.Structure):
    _fields_ = (
        ("LowPart", wintypes.DWORD),
        ("HighPart", wintypes.LONG),
    )

    def __new__(cls, value=0):
        return cls.from_buffer_copy(ctypes.c_ulonglong(value))

    def __int__(self):
        return ctypes.c_ulonglong.from_buffer(self).value

    def __repr__(self):
        name = self.__class__.__name__
        return f"{name}({int(self)})"


LPLUID = ctypes.POINTER(LUID)
PSID = wintypes.LPVOID


class SID_AND_ATTRIBUTES(ctypes.Structure):
    _fields_ = (
        ("Sid", PSID),
        ("Attributes", wintypes.DWORD),
    )


LPSID_AND_ATTRIBUTES = ctypes.POINTER(SID_AND_ATTRIBUTES)


class TOKEN_GROUPS(ctypes.Structure):
    _fields_ = (
        ("GroupCount", wintypes.DWORD),
        ("Groups", SID_AND_ATTRIBUTES * 1),
    )


LPTOKEN_GROUPS = ctypes.POINTER(TOKEN_GROUPS)


class TOKEN_SOURCE(ctypes.Structure):
    _fields_ = (
        ("SourceName", CHAR * TOKEN_SOURCE_LENGTH),
        ("SourceIdentifier", LUID),
    )

    def __init__(self, SourceName=None, SourceIdentifier=None):
        super().__init__()
        if SourceName is not None:
            if not isinstance(SourceName, bytes):
                SourceName = SourceName.encode("mbcs")
            self.SourceName = SourceName
        if SourceIdentifier is None:
            # pylint: disable=access-member-before-definition
            luid = self.SourceIdentifier
            # pylint: enable=access-member-before-definition
            ntdll.NtAllocateLocallyUniqueId(ctypes.byref(luid))
        else:
            self.SourceIdentifier = SourceIdentifier


LPTOKEN_SOURCE = ctypes.POINTER(TOKEN_SOURCE)
py_source_context = TOKEN_SOURCE(b"PYTHON  ")
py_origin_name = __name__.encode()
py_logon_process_name = f"{py_origin_name}-{os.getpid()}"
SIZE_T = ctypes.c_size_t


class QUOTA_LIMITS(ctypes.Structure):
    _fields_ = (
        ("PagedPoolLimit", SIZE_T),
        ("NonPagedPoolLimit", SIZE_T),
        ("MinimumWorkingSetSize", SIZE_T),
        ("MaximumWorkingSetSize", SIZE_T),
        ("PagefileLimit", SIZE_T),
        ("TimeLimit", wintypes.LARGE_INTEGER),
    )


LPQUOTA_LIMITS = ctypes.POINTER(QUOTA_LIMITS)
LPULONG = ctypes.POINTER(wintypes.ULONG)
LSA_OPERATIONAL_MODE = wintypes.ULONG
LPLSA_OPERATIONAL_MODE = LPULONG
LPHANDLE = ctypes.POINTER(wintypes.HANDLE)
LPLPVOID = ctypes.POINTER(wintypes.LPVOID)
LPDWORD = ctypes.POINTER(wintypes.DWORD)


class ContiguousUnicode(ctypes.Structure):
    # _string_names_: sequence matched to underscore-prefixed fields
    def __init__(self, *args, **kwargs):  # pylint: disable=useless-super-delegation
        super().__init__(*args, **kwargs)

    def _get_unicode_string(self, name):
        wchar_size = ctypes.sizeof(WCHAR)
        s = getattr(self, f"_{name}")
        length = s.Length // wchar_size
        buf = s.Buffer
        if buf:
            return buf[:length]
        return None

    def _set_unicode_buffer(self, values):
        cls = type(self)
        wchar_size = ctypes.sizeof(WCHAR)
        bufsize = (len("\x00".join(values)) + 1) * wchar_size
        ctypes.resize(self, ctypes.sizeof(cls) + bufsize)
        addr = ctypes.addressof(self) + ctypes.sizeof(cls)
        for value in values:
            bufsize = (len(value) + 1) * wchar_size
            ctypes.memmove(addr, value, bufsize)
            addr += bufsize

    def _set_unicode_string(self, name, value):
        values = []
        for n in self._string_names_:
            if n == name:
                values.append(value or "")
            else:
                values.append(getattr(self, n) or "")
        self._set_unicode_buffer(values)

        cls = type(self)
        wchar_size = ctypes.sizeof(WCHAR)
        addr = ctypes.addressof(self) + ctypes.sizeof(cls)
        for n, v in zip(self._string_names_, values):
            ptr = ctypes.cast(addr, PWCHAR)
            ustr = getattr(self, f"_{n}")
            length = ustr.Length = len(v) * wchar_size
            full_length = length + wchar_size
            if (n == name and value is None) or (
                n != name and not (length or ustr.Buffer)
            ):
                ustr.Buffer = None
                ustr.MaximumLength = 0
            else:
                ustr.Buffer = ptr
                ustr.MaximumLength = full_length
            addr += full_length

    def __getattr__(self, name):
        if name not in self._string_names_:
            raise AttributeError
        return self._get_unicode_string(name)

    def __setattr__(self, name, value):
        if name in self._string_names_:
            self._set_unicode_string(name, value)
        else:
            super().__setattr__(name, value)

    @classmethod
    def from_address_copy(cls, address, size=None):
        x = ctypes.Structure.__new__(cls)  # pylint: disable=no-value-for-parameter
        if size is not None:
            ctypes.resize(x, size)
        ctypes.memmove(ctypes.byref(x), address, ctypes.sizeof(x))
        delta = ctypes.addressof(x) - address
        for n in cls._string_names_:
            ustr = getattr(x, f"_{n}")
            addr = ctypes.c_void_p.from_buffer(ustr.Buffer)
            if addr:
                addr.value += delta
        return x


class AuthInfo(ContiguousUnicode):
    # _message_type_: from a logon-submit-type enumeration
    def __init__(self):
        super().__init__()
        self.MessageType = self._message_type_


class MSV1_0_INTERACTIVE_LOGON(AuthInfo):
    _message_type_ = MsV1_0InteractiveLogon
    _string_names_ = "LogonDomainName", "UserName", "Password"

    _fields_ = (
        ("MessageType", LOGON_SUBMIT_TYPE),
        ("_LogonDomainName", UNICODE_STRING),
        ("_UserName", UNICODE_STRING),
        ("_Password", UNICODE_STRING),
    )

    def __init__(self, UserName=None, Password=None, LogonDomainName=None):
        super().__init__()
        if LogonDomainName is not None:
            self.LogonDomainName = LogonDomainName
        if UserName is not None:
            self.UserName = UserName
        if Password is not None:
            self.Password = Password


class S4ULogon(AuthInfo):
    _string_names_ = "UserPrincipalName", "DomainName"

    _fields_ = (
        ("MessageType", LOGON_SUBMIT_TYPE),
        ("Flags", wintypes.ULONG),
        ("_UserPrincipalName", UNICODE_STRING),
        ("_DomainName", UNICODE_STRING),
    )

    def __init__(self, UserPrincipalName=None, DomainName=None, Flags=0):
        super().__init__()
        self.Flags = Flags
        if UserPrincipalName is not None:
            self.UserPrincipalName = UserPrincipalName
        if DomainName is not None:
            self.DomainName = DomainName


class MSV1_0_S4U_LOGON(S4ULogon):
    _message_type_ = MsV1_0S4ULogon


class KERB_S4U_LOGON(S4ULogon):
    _message_type_ = KerbS4ULogon


PMSV1_0_S4U_LOGON = ctypes.POINTER(MSV1_0_S4U_LOGON)
PKERB_S4U_LOGON = ctypes.POINTER(KERB_S4U_LOGON)


class ProfileBuffer(ContiguousUnicode):
    # _message_type_
    def __init__(self):
        super().__init__()
        self.MessageType = self._message_type_


class MSV1_0_INTERACTIVE_PROFILE(ProfileBuffer):
    _message_type_ = MsV1_0InteractiveLogon
    _string_names_ = (
        "LogonScript",
        "HomeDirectory",
        "FullName",
        "ProfilePath",
        "HomeDirectoryDrive",
        "LogonServer",
    )
    _fields_ = (
        ("MessageType", PROFILE_BUFFER_TYPE),
        ("LogonCount", wintypes.USHORT),
        ("BadPasswordCount", wintypes.USHORT),
        ("LogonTime", LARGE_INTEGER),
        ("LogoffTime", LARGE_INTEGER),
        ("KickOffTime", LARGE_INTEGER),
        ("PasswordLastSet", LARGE_INTEGER),
        ("PasswordCanChange", LARGE_INTEGER),
        ("PasswordMustChange", LARGE_INTEGER),
        ("_LogonScript", UNICODE_STRING),
        ("_HomeDirectory", UNICODE_STRING),
        ("_FullName", UNICODE_STRING),
        ("_ProfilePath", UNICODE_STRING),
        ("_HomeDirectoryDrive", UNICODE_STRING),
        ("_LogonServer", UNICODE_STRING),
        ("UserFlags", wintypes.ULONG),
    )


def _check_status(result, func, args):
    if result.value < 0:
        raise ctypes.WinError(result.to_error())
    return args


def _check_bool(result, func, args):
    if not result:
        raise ctypes.WinError(ctypes.get_last_error())
    return args


INVALID_HANDLE_VALUE = wintypes.HANDLE(-1).value
INVALID_DWORD_VALUE = wintypes.DWORD(-1).value  # ~WinAPI
INFINITE = INVALID_DWORD_VALUE
STD_INPUT_HANDLE = wintypes.DWORD(-10).value
STD_OUTPUT_HANDLE = wintypes.DWORD(-11).value
STD_ERROR_HANDLE = wintypes.DWORD(-12).value


class SECURITY_ATTRIBUTES(ctypes.Structure):
    _fields_ = (
        ("nLength", wintypes.DWORD),
        ("lpSecurityDescriptor", wintypes.LPVOID),
        ("bInheritHandle", wintypes.BOOL),
    )

    def __init__(self, **kwds):
        self.nLength = ctypes.sizeof(self)
        super().__init__(**kwds)


LPSECURITY_ATTRIBUTES = ctypes.POINTER(SECURITY_ATTRIBUTES)
LPBYTE = ctypes.POINTER(wintypes.BYTE)
LPHANDLE = PHANDLE = ctypes.POINTER(ctypes.c_void_p)
LPDWORD = ctypes.POINTER(ctypes.c_ulong)


class STARTUPINFO(ctypes.Structure):
    """https://msdn.microsoft.com/en-us/library/ms686331"""

    _fields_ = (
        ("cb", wintypes.DWORD),
        ("lpReserved", wintypes.LPWSTR),
        ("lpDesktop", wintypes.LPWSTR),
        ("lpTitle", wintypes.LPWSTR),
        ("dwX", wintypes.DWORD),
        ("dwY", wintypes.DWORD),
        ("dwXSize", wintypes.DWORD),
        ("dwYSize", wintypes.DWORD),
        ("dwXCountChars", wintypes.DWORD),
        ("dwYCountChars", wintypes.DWORD),
        ("dwFillAttribute", wintypes.DWORD),
        ("dwFlags", wintypes.DWORD),
        ("wShowWindow", wintypes.WORD),
        ("cbReserved2", wintypes.WORD),
        ("lpReserved2", LPBYTE),
        ("hStdInput", wintypes.HANDLE),
        ("hStdOutput", wintypes.HANDLE),
        ("hStdError", wintypes.HANDLE),
    )

    def __init__(self, **kwds):
        self.cb = ctypes.sizeof(self)
        super().__init__(**kwds)


LPSTARTUPINFO = ctypes.POINTER(STARTUPINFO)


class PROC_THREAD_ATTRIBUTE_LIST(ctypes.Structure):
    pass


PPROC_THREAD_ATTRIBUTE_LIST = ctypes.POINTER(PROC_THREAD_ATTRIBUTE_LIST)


class STARTUPINFOEX(STARTUPINFO):
    _fields_ = (("lpAttributeList", PPROC_THREAD_ATTRIBUTE_LIST),)


LPSTARTUPINFOEX = ctypes.POINTER(STARTUPINFOEX)


class PROCESS_INFORMATION(ctypes.Structure):
    """https://msdn.microsoft.com/en-us/library/ms684873"""

    _fields_ = (
        ("hProcess", wintypes.HANDLE),
        ("hThread", wintypes.HANDLE),
        ("dwProcessId", wintypes.DWORD),
        ("dwThreadId", wintypes.DWORD),
    )


LPPROCESS_INFORMATION = ctypes.POINTER(PROCESS_INFORMATION)


class HANDLE_IHV(wintypes.HANDLE):
    pass


def errcheck_ihv(result, func, args):
    if result.value == INVALID_HANDLE_VALUE:
        raise ctypes.WinError(ctypes.get_last_error())
    return result.value


class DWORD_IDV(wintypes.DWORD):
    pass


def errcheck_idv(result, func, args):
    if result.value == INVALID_DWORD_VALUE:
        raise ctypes.WinError(ctypes.get_last_error())
    return result.value


def errcheck_bool(result, func, args):
    if not result:
        raise ctypes.WinError(ctypes.get_last_error())
    return args


def _win(func, restype, *argtypes):
    func.restype = restype
    func.argtypes = argtypes
    if issubclass(restype, NTSTATUS):
        func.errcheck = _check_status
    elif issubclass(restype, BOOL):
        func.errcheck = _check_bool
    elif issubclass(restype, HANDLE_IHV):
        func.errcheck = errcheck_ihv
    elif issubclass(restype, DWORD_IDV):
        func.errcheck = errcheck_idv
    else:
        func.errcheck = errcheck_bool


# https://msdn.microsoft.com/en-us/library/ms683231
_win(kernel32.GetStdHandle, HANDLE_IHV, wintypes.DWORD)  # _In_ nStdHandle


# https://msdn.microsoft.com/en-us/library/ms724211
_win(kernel32.CloseHandle, wintypes.BOOL, wintypes.HANDLE)  # _In_ hObject


# https://msdn.microsoft.com/en-us/library/ms724935
_win(
    kernel32.SetHandleInformation,
    wintypes.BOOL,
    wintypes.HANDLE,  # _In_ hObject
    wintypes.DWORD,  # _In_ dwMask
    wintypes.DWORD,
)  # _In_ dwFlags


# https://msdn.microsoft.com/en-us/library/ms724251
_win(
    kernel32.DuplicateHandle,
    wintypes.BOOL,
    wintypes.HANDLE,  # _In_  hSourceProcessHandle,
    wintypes.HANDLE,  # _In_  hSourceHandle,
    wintypes.HANDLE,  # _In_  hTargetProcessHandle,
    LPHANDLE,  # _Out_ lpTargetHandle,
    wintypes.DWORD,  # _In_  dwDesiredAccess,
    wintypes.BOOL,  # _In_  bInheritHandle,
    wintypes.DWORD,
)  # _In_  dwOptions


# https://msdn.microsoft.com/en-us/library/ms683179
_win(kernel32.GetCurrentProcess, wintypes.HANDLE)


# https://msdn.microsoft.com/en-us/library/ms683189
_win(
    kernel32.GetExitCodeProcess,
    wintypes.BOOL,
    wintypes.HANDLE,  # _In_  hProcess,
    LPDWORD,
)  # _Out_ lpExitCode


# https://msdn.microsoft.com/en-us/library/aa365152
_win(
    kernel32.CreatePipe,
    wintypes.BOOL,
    PHANDLE,  # _Out_    hReadPipe,
    PHANDLE,  # _Out_    hWritePipe,
    LPSECURITY_ATTRIBUTES,  # _In_opt_ lpPipeAttributes,
    wintypes.DWORD,
)  # _In_     nSize


# https://msdn.microsoft.com/en-us/library/ms682431
# _win(advapi32.CreateProcessWithTokenW, wintypes.BOOL,
#    PHANDLE,       # _In_        lpUsername
#    wintypes.DWORD,         # _In_        dwLogonFlags
#    wintypes.LPCWSTR,       # _In_opt_    lpApplicationName
#    wintypes.LPWSTR,        # _Inout_opt_ lpCommandLine
#    wintypes.DWORD,         # _In_        dwCreationFlags
#    wintypes.LPVOID,        # _In_opt_     lpEnvironment
#    wintypes.LPCWSTR,       # _In_opt_    lpCurrentDirectory
#    LPSTARTUPINFO,          # _In_        lpStartupInfo
#    LPPROCESS_INFORMATION)  # _Out_       lpProcessInformation


# https://msdn.microsoft.com/en-us/library/ms682431
_win(
    advapi32.CreateProcessWithLogonW,
    wintypes.BOOL,
    wintypes.LPCWSTR,  # _In_        lpUsername
    wintypes.LPCWSTR,  # _In_opt_    lpDomain
    wintypes.LPCWSTR,  # _In_        lpPassword
    wintypes.DWORD,  # _In_        dwLogonFlags
    wintypes.LPCWSTR,  # _In_opt_    lpApplicationName
    wintypes.LPWSTR,  # _Inout_opt_ lpCommandLine
    wintypes.DWORD,  # _In_        dwCreationFlags
    wintypes.LPCWSTR,  # _In_opt_    lpEnvironment
    wintypes.LPCWSTR,  # _In_opt_    lpCurrentDirectory
    LPSTARTUPINFO,  # _In_        lpStartupInfo
    LPPROCESS_INFORMATION,
)  # _Out_       lpProcessInformation


# https://msdn.microsoft.com/en-us/library/ms683179
_win(kernel32.GetCurrentProcess, wintypes.HANDLE)


# https://msdn.microsoft.com/en-us/library/ms724251
_win(
    kernel32.DuplicateHandle,
    BOOL,
    wintypes.HANDLE,  # _In_  hSourceProcessHandle
    wintypes.HANDLE,  # _In_  hSourceHandle
    wintypes.HANDLE,  # _In_  hTargetProcessHandle
    LPHANDLE,  # _Out_ lpTargetHandle
    wintypes.DWORD,  # _In_  dwDesiredAccess
    wintypes.BOOL,  # _In_  bInheritHandle
    wintypes.DWORD,
)  # _In_  dwOptions


# https://msdn.microsoft.com/en-us/library/ms724295
_win(
    kernel32.GetComputerNameW, BOOL, wintypes.LPWSTR, LPDWORD  # _Out_   lpBuffer
)  # _Inout_ lpnSize


# https://msdn.microsoft.com/en-us/library/aa379295
_win(
    advapi32.OpenProcessToken,
    BOOL,
    wintypes.HANDLE,  # _In_  ProcessHandle
    wintypes.DWORD,  # _In_  DesiredAccess
    LPHANDLE,
)  # _Out_ TokenHandle


# https://msdn.microsoft.com/en-us/library/aa446617
_win(
    advapi32.DuplicateTokenEx,
    BOOL,
    wintypes.HANDLE,  # _In_     hExistingToken
    wintypes.DWORD,  # _In_     dwDesiredAccess
    LPSECURITY_ATTRIBUTES,  # _In_opt_ lpTokenAttributes
    SECURITY_IMPERSONATION_LEVEL,  # _In_     ImpersonationLevel
    TOKEN_TYPE,  # _In_     TokenType
    LPHANDLE,
)  # _Out_    phNewToken


# https://msdn.microsoft.com/en-us/library/ff566415
_win(ntdll.NtAllocateLocallyUniqueId, NTSTATUS, LPLUID)  # _Out_ LUID


# https://msdn.microsoft.com/en-us/library/aa378279
_win(
    secur32.LsaFreeReturnBuffer,
    NTSTATUS,
    wintypes.LPVOID,
)  # _In_ Buffer


# https://msdn.microsoft.com/en-us/library/aa378265
_win(
    secur32.LsaConnectUntrusted,
    NTSTATUS,
    LPHANDLE,
)  # _Out_ LsaHandle


# https://msdn.microsoft.com/en-us/library/aa378318
_win(
    secur32.LsaRegisterLogonProcess,
    NTSTATUS,
    LPSTRING,  # _In_  LogonProcessName
    LPHANDLE,  # _Out_ LsaHandle
    LPLSA_OPERATIONAL_MODE,
)  # _Out_ SecurityMode


# https://msdn.microsoft.com/en-us/library/aa378269
_win(secur32.LsaDeregisterLogonProcess, NTSTATUS, wintypes.HANDLE)  # _In_ LsaHandle


# https://msdn.microsoft.com/en-us/library/aa378297
_win(
    secur32.LsaLookupAuthenticationPackage,
    NTSTATUS,
    wintypes.HANDLE,  # _In_  LsaHandle
    LPSTRING,  # _In_  PackageName
    LPULONG,
)  # _Out_ AuthenticationPackage


# https://msdn.microsoft.com/en-us/library/aa378292
_win(
    secur32.LsaLogonUser,
    NTSTATUS,
    wintypes.HANDLE,  # _In_     LsaHandle
    LPSTRING,  # _In_     OriginName
    SECURITY_LOGON_TYPE,  # _In_     LogonType
    wintypes.ULONG,  # _In_     AuthenticationPackage
    wintypes.LPVOID,  # _In_     AuthenticationInformation
    wintypes.ULONG,  # _In_     AuthenticationInformationLength
    LPTOKEN_GROUPS,  # _In_opt_ LocalGroups
    LPTOKEN_SOURCE,  # _In_     SourceContext
    LPLPVOID,  # _Out_    ProfileBuffer
    LPULONG,  # _Out_    ProfileBufferLength
    LPLUID,  # _Out_    LogonId
    LPHANDLE,  # _Out_    Token
    LPQUOTA_LIMITS,  # _Out_    Quotas
    PNTSTATUS,
)  # _Out_    SubStatus


def duplicate_token(
    source_token=None,
    access=TOKEN_ALL_ACCESS,
    impersonation_level=SecurityImpersonation,
    token_type=TokenPrimary,
    attributes=None,
):
    close_source = False
    if source_token is None:
        close_source = True
        source_token = HANDLE()
        advapi32.OpenProcessToken(
            kernel32.GetCurrentProcess(), TOKEN_ALL_ACCESS, ctypes.byref(source_token)
        )
    token = HANDLE()
    try:
        advapi32.DuplicateTokenEx(
            source_token,
            access,
            attributes,
            impersonation_level,
            token_type,
            ctypes.byref(token),
        )
    finally:
        if close_source:
            source_token.Close()
    return token


def lsa_connect_untrusted():
    handle = wintypes.HANDLE()
    secur32.LsaConnectUntrusted(ctypes.byref(handle))
    return handle.value


def lsa_register_logon_process(logon_process_name):
    if not isinstance(logon_process_name, bytes):
        logon_process_name = logon_process_name.encode("mbcs")
    logon_process_name = logon_process_name[:127]
    buf = ctypes.create_string_buffer(logon_process_name, 128)
    name = STRING(len(logon_process_name), len(buf), buf)
    handle = wintypes.HANDLE()
    mode = LSA_OPERATIONAL_MODE()
    secur32.LsaRegisterLogonProcess(
        ctypes.byref(name), ctypes.byref(handle), ctypes.byref(mode)
    )
    return handle.value


def lsa_lookup_authentication_package(lsa_handle, package_name):
    if not isinstance(package_name, bytes):
        package_name = package_name.encode("mbcs")
    package_name = package_name[:127]
    buf = ctypes.create_string_buffer(package_name)
    name = STRING(len(package_name), len(buf), buf)
    package = wintypes.ULONG()
    secur32.LsaLookupAuthenticationPackage(
        lsa_handle, ctypes.byref(name), ctypes.byref(package)
    )
    return package.value


LOGONINFO = collections.namedtuple(
    "LOGONINFO", ("Token", "LogonId", "Profile", "Quotas")
)


def lsa_logon_user(
    auth_info,
    local_groups=None,
    origin_name=py_origin_name,
    source_context=None,
    auth_package=None,
    logon_type=None,
    lsa_handle=None,
):
    if local_groups is None:
        plocal_groups = LPTOKEN_GROUPS()
    else:
        plocal_groups = ctypes.byref(local_groups)
    if source_context is None:
        source_context = py_source_context
    if not isinstance(origin_name, bytes):
        origin_name = origin_name.encode("mbcs")
    buf = ctypes.create_string_buffer(origin_name)
    origin_name = STRING(len(origin_name), len(buf), buf)
    if auth_package is None:
        if isinstance(auth_info, MSV1_0_S4U_LOGON):
            auth_package = NEGOTIATE_PACKAGE_NAME
        elif isinstance(auth_info, KERB_S4U_LOGON):
            auth_package = MICROSOFT_KERBEROS_NAME
        else:
            auth_package = MSV1_0_PACKAGE_NAME
    if logon_type is None:
        if isinstance(auth_info, S4ULogon):
            logon_type = win32con.LOGON32_LOGON_NETWORK
        else:
            logon_type = Interactive
    profile_buffer = wintypes.LPVOID()
    profile_buffer_length = wintypes.ULONG()
    profile = None
    logonid = LUID()
    htoken = HANDLE()
    quotas = QUOTA_LIMITS()
    substatus = NTSTATUS()
    deregister = False
    if lsa_handle is None:
        lsa_handle = lsa_connect_untrusted()
        deregister = True
    try:
        if isinstance(auth_package, (str, bytes)):
            auth_package = lsa_lookup_authentication_package(lsa_handle, auth_package)
        try:
            secur32.LsaLogonUser(
                lsa_handle,
                ctypes.byref(origin_name),
                logon_type,
                auth_package,
                ctypes.byref(auth_info),
                ctypes.sizeof(auth_info),
                plocal_groups,
                ctypes.byref(source_context),
                ctypes.byref(profile_buffer),
                ctypes.byref(profile_buffer_length),
                ctypes.byref(logonid),
                ctypes.byref(htoken),
                ctypes.byref(quotas),
                ctypes.byref(substatus),
            )
        except OSError:
            if substatus.value:
                raise ctypes.WinError(substatus.to_error())
            raise
        finally:
            if profile_buffer:
                address = profile_buffer.value
                buftype = PROFILE_BUFFER_TYPE.from_address(address).value
                if buftype == MsV1_0InteractiveLogon:
                    profile = MSV1_0_INTERACTIVE_PROFILE.from_address_copy(
                        address, profile_buffer_length.value
                    )
                secur32.LsaFreeReturnBuffer(address)
    finally:
        if deregister:
            secur32.LsaDeregisterLogonProcess(lsa_handle)
    return LOGONINFO(htoken, logonid, profile, quotas)


def logon_msv1(
    name,
    password,
    domain=None,
    local_groups=None,
    origin_name=py_origin_name,
    source_context=None,
):
    return lsa_logon_user(
        MSV1_0_INTERACTIVE_LOGON(name, password, domain),
        local_groups,
        origin_name,
        source_context,
    )


def logon_msv1_s4u(
    name, local_groups=None, origin_name=py_origin_name, source_context=None
):
    domain = ctypes.create_unicode_buffer(MAX_COMPUTER_NAME_LENGTH + 1)
    length = wintypes.DWORD(len(domain))
    kernel32.GetComputerNameW(domain, ctypes.byref(length))
    return lsa_logon_user(
        MSV1_0_S4U_LOGON(name, domain.value), local_groups, origin_name, source_context
    )


def logon_kerb_s4u(
    name,
    realm=None,
    local_groups=None,
    origin_name=py_origin_name,
    source_context=None,
    logon_process_name=py_logon_process_name,
):
    lsa_handle = lsa_register_logon_process(logon_process_name)
    try:
        return lsa_logon_user(
            KERB_S4U_LOGON(name, realm),
            local_groups,
            origin_name,
            source_context,
            lsa_handle=lsa_handle,
        )
    finally:
        secur32.LsaDeregisterLogonProcess(lsa_handle)


def DuplicateHandle(
    hsrc=kernel32.GetCurrentProcess(),
    srchandle=kernel32.GetCurrentProcess(),
    htgt=kernel32.GetCurrentProcess(),
    access=0,
    inherit=False,
    options=win32con.DUPLICATE_SAME_ACCESS,
):
    tgthandle = wintypes.HANDLE()
    kernel32.DuplicateHandle(
        hsrc, srchandle, htgt, ctypes.byref(tgthandle), access, inherit, options
    )
    return tgthandle.value


def CreatePipe(inherit_read=False, inherit_write=False):
    read, write = wintypes.HANDLE(), wintypes.HANDLE()
    kernel32.CreatePipe(ctypes.byref(read), ctypes.byref(write), None, 0)
    if inherit_read:
        kernel32.SetHandleInformation(
            read, win32con.HANDLE_FLAG_INHERIT, win32con.HANDLE_FLAG_INHERIT
        )
    if inherit_write:
        kernel32.SetHandleInformation(
            write, win32con.HANDLE_FLAG_INHERIT, win32con.HANDLE_FLAG_INHERIT
        )
    return read.value, write.value


def set_user_perm(obj, perm, sid):
    """
    Set an object permission for the given user sid
    """
    info = (
        win32security.OWNER_SECURITY_INFORMATION
        | win32security.GROUP_SECURITY_INFORMATION
        | win32security.DACL_SECURITY_INFORMATION
    )
    sd = win32security.GetUserObjectSecurity(obj, info)
    dacl = sd.GetSecurityDescriptorDacl()
    ace_cnt = dacl.GetAceCount()
    for idx in range(0, ace_cnt):
        (aceType, aceFlags), ace_mask, ace_sid = dacl.GetAce(idx)
        ace_exists = (
            aceType == ntsecuritycon.ACCESS_ALLOWED_ACE_TYPE
            and ace_mask == perm
            and ace_sid == sid
        )
        if ace_exists:
            # If the ace already exists, do nothing
            break
    else:
        dacl.AddAccessAllowedAce(dacl.GetAclRevision(), perm, sid)
        sd.SetSecurityDescriptorDacl(1, dacl, 0)
        win32security.SetUserObjectSecurity(obj, info, sd)


def grant_winsta_and_desktop(th):
    """
    Grant the token's user access to the current process's window station and
    desktop.
    """
    current_sid = win32security.GetTokenInformation(th, win32security.TokenUser)[0]
    # Add permissions for the sid to the current windows station and thread id.
    # This prevents Windows error 0xC0000142.
    winsta = win32process.GetProcessWindowStation()
    set_user_perm(winsta, WINSTA_ALL, current_sid)
    desktop = win32service.GetThreadDesktop(win32api.GetCurrentThreadId())
    set_user_perm(desktop, DESKTOP_ALL, current_sid)


def environment_string(env):
    senv = ""
    for k, v in env.items():
        senv += k + "=" + v + "\0"
    senv += "\0"
    return ctypes.create_unicode_buffer(senv)


def CreateProcessWithTokenW(
    token,
    logonflags=0,
    applicationname=None,
    commandline=None,
    creationflags=0,
    environment=None,
    currentdirectory=None,
    startupinfo=None,
):
    creationflags |= win32con.CREATE_UNICODE_ENVIRONMENT
    if commandline is not None:
        commandline = ctypes.create_unicode_buffer(commandline)
    if startupinfo is None:
        startupinfo = STARTUPINFO()
    if currentdirectory is not None:
        currentdirectory = ctypes.create_unicode_buffer(currentdirectory)
    if environment is not None and isinstance(environment, dict):
        environment = ctypes.pointer(environment_string(environment))
    process_info = PROCESS_INFORMATION()
    ret = advapi32.CreateProcessWithTokenW(
        token,
        logonflags,
        applicationname,
        commandline,
        creationflags,
        environment,
        currentdirectory,
        ctypes.byref(startupinfo),
        ctypes.byref(process_info),
    )
    if ret == 0:
        raise ctypes.WinError(ctypes.get_last_error())
    return process_info


def enumerate_tokens(sid=None, session_id=None, privs=None):
    """
    Enumerate tokens from any existing processes that can be accessed.
    Optionally filter by sid.
    """
    for p in psutil.process_iter():
        if p.pid == 0:
            continue
        try:
            ph = win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS, 0, p.pid)
        except win32api.error as exc:
            if exc.winerror == 5:
                log.debug("Unable to OpenProcess pid=%d name=%s", p.pid, p.name())
                continue
            raise exc
        try:
            access = (
                win32security.TOKEN_DUPLICATE
                | win32security.TOKEN_QUERY
                | win32security.TOKEN_IMPERSONATE
                | win32security.TOKEN_ASSIGN_PRIMARY
            )
            th = win32security.OpenProcessToken(ph, access)
        except Exception as exc:  # pylint: disable=broad-except
            log.debug(
                "OpenProcessToken failed pid=%d name=%s user%s",
                p.pid,
                p.name(),
                p.username(),
            )
            continue
        try:
            process_sid = win32security.GetTokenInformation(
                th, win32security.TokenUser
            )[0]
        except Exception as exc:  # pylint: disable=broad-except
            log.exception(
                "GetTokenInformation pid=%d name=%s user%s",
                p.pid,
                p.name(),
                p.username(),
            )
            continue

        proc_sid = win32security.ConvertSidToStringSid(process_sid)
        if sid and sid != proc_sid:
            log.debug("Token for pid does not match user sid: %s", sid)
            continue

        if (
            session_id
            and win32security.GetTokenInformation(th, win32security.TokenSessionId)
            != session_id
        ):
            continue

        def has_priv(tok, priv):
            luid = win32security.LookupPrivilegeValue(None, priv)
            for priv_luid, flags in win32security.GetTokenInformation(
                tok, win32security.TokenPrivileges
            ):
                if priv_luid == luid:
                    return True
            return False

        if privs:
            has_all = True
            for name in privs:
                if not has_priv(th, name):
                    has_all = False
            if not has_all:
                continue
        yield dup_token(th)


def impersonate_sid(sid, session_id=None, privs=None):
    """
    Find an existing process token for the given sid and impersonate the token.
    """
    for tok in enumerate_tokens(sid, session_id, privs):
        tok = dup_token(tok)
        elevate_token(tok)
        if win32security.ImpersonateLoggedOnUser(tok) == 0:
            raise OSError("Impersonation failure")
        return tok
    raise OSError("Impersonation failure")


def dup_token(th):
    """
    duplicate the access token
    """
    # TODO: is `duplicate_token` the same?
    sec_attr = win32security.SECURITY_ATTRIBUTES()
    sec_attr.bInheritHandle = True
    return win32security.DuplicateTokenEx(
        th,
        win32security.SecurityImpersonation,
        win32con.MAXIMUM_ALLOWED,
        win32security.TokenPrimary,
        sec_attr,
    )


def elevate_token(th):
    """
    Set all token privileges to enabled
    """
    # Get list of privileges this token contains
    privileges = win32security.GetTokenInformation(th, win32security.TokenPrivileges)

    # Create a set of all privileges to be enabled
    enable_privs = set()
    for luid, flags in privileges:
        enable_privs.add((luid, win32con.SE_PRIVILEGE_ENABLED))

    # Enable the privileges
    if win32security.AdjustTokenPrivileges(th, 0, enable_privs) == 0:
        raise OSError(win32api.FormatMessage(win32api.GetLastError()))


def make_inheritable(token):
    """Create an inheritable handle"""
    return win32api.DuplicateHandle(
        win32api.GetCurrentProcess(),
        token,
        win32api.GetCurrentProcess(),
        0,
        1,
        win32con.DUPLICATE_SAME_ACCESS,
    )


def CreateProcessWithLogonW(
    username=None,
    domain=None,
    password=None,
    logonflags=0,
    applicationname=None,
    commandline=None,
    creationflags=0,
    environment=None,
    currentdirectory=None,
    startupinfo=None,
):
    creationflags |= win32con.CREATE_UNICODE_ENVIRONMENT
    if commandline is not None:
        commandline = ctypes.create_unicode_buffer(commandline)
    if startupinfo is None:
        startupinfo = STARTUPINFO()
    if environment is not None and isinstance(environment, dict):
        environment = ctypes.pointer(environment_string(environment))
    process_info = PROCESS_INFORMATION()
    advapi32.CreateProcessWithLogonW(
        username,
        domain,
        password,
        logonflags,
        applicationname,
        commandline,
        creationflags,
        environment,
        currentdirectory,
        ctypes.byref(startupinfo),
        ctypes.byref(process_info),
    )
    return process_info


def _cmd_exe_cswitch_quoted_argument(payload: str) -> str:
    """
    Wrap ``payload`` for use as the argument to ``cmd.exe``'s ``/c`` switch.

    Doubles embedded double quotes per ``cmd.exe`` parsing rules so the entire
    payload is one argument when passed through ``CreateProcess`` /
    ``CreateProcessWithTokenW`` command lines (so ``&``, ``|``, etc. are not
    parsed at the outer process level).
    """
    return '"' + payload.replace('"', '""') + '"'


def prepend_cmd(
    win_shell,
    cmd,
    quote_c_payload=True,
    msvc_quote_bare_path_string=False,
):
    """
    Prep cmd when shell is cmd.exe. Always use a command string instead of a
    list to satisfy both :class:`subprocess.Popen` and CreateProcess.

    Args:
        win_shell: Path to ``cmd.exe``.
        cmd: String or argv sequence. Lists/tuples are passed through
            ``list2cmdline`` first.
        quote_c_payload: If ``True`` (default), wrap the entire user payload
            in double quotes after ``/c`` and double internal double quotes so
            the whole command is a single argument (required for
            ``CreateProcessWithTokenW`` and :mod:`win_runas <salt.utils.win_runas>`).
            If ``False``, use ``cmd /c`` without the big ``_cmd_exe_cswitch`` wrap
            (so batch ``%1``/``%2`` match the non-runas path). A *string* without
            ``&``/``|`` is then either passed raw (so ``echo "a b"`` works with
            ``python_shell``) or as one :func:`subprocess.list2cmdline` token
            (see *msvc_quote_bare_path_string*). Compound *strings* use the
            raw tail so ``cmd`` still parses ``&`` and ``|``. List/tuple payloads
            use ``list2cmdline`` as above.
        msvc_quote_bare_path_string: When ``quote_c_payload`` is false and
            *cmd* is a string, if true and the payload has no ``&``/``|``,
            wrap the entire string with ``list2cmdline`` so a *single* path
            with spaces is one token (``runas`` and no ``python_shell``). If
            false (default), use the string as the raw ``/c`` tail (e.g.
            ``python_shell`` and ``echo`` with quotes).

    Returns:
        A full command line string starting with ``win_shell``.
    """
    if isinstance(cmd, (list, tuple)):
        args = subprocess.list2cmdline(cmd)
    else:
        # cmd.exe treats newlines as command separators even within a command-line
        # string, so a multiline command passed via the command line only executes
        # its first line. Collapse CRLF/LF to a single space so the entire command
        # reaches the target process (e.g. a PowerShell -Command script block).
        args = cmd.replace("\r\n", " ").replace("\n", " ")
        # When PowerShell's stdout is piped (non-interactive), the -Command { block }
        # form evaluates the script block as an expression and returns the ScriptBlock
        # object instead of executing it. Converting to -EncodedCommand avoids this
        # and also sidesteps cmd.exe quoting issues with double quotes inside the block.
        args = _maybe_encode_powershell_block(args)
    if not quote_c_payload:
        if isinstance(cmd, (list, tuple)):
            c_payload = args
        else:
            if ("&" in args) or ("|" in args):
                c_payload = args
            elif msvc_quote_bare_path_string:
                c_payload = subprocess.list2cmdline([args])
            else:
                c_payload = args
        return f"{win_shell} /c {c_payload}"
    return f"{win_shell} /c {_cmd_exe_cswitch_quoted_argument(args)}"


# Matches: powershell[-Command { block }] when the block is the last argument.
# The executable name check prevents false positives on non-PowerShell commands.
_PS_BLOCK_RE = re.compile(r"-Command\s+\{(.*)\}\s*$", re.IGNORECASE | re.DOTALL)
_PS_EXE_RE = re.compile(r"(?:^|[\\/])(?:powershell|pwsh)(?:\.exe)?\b", re.IGNORECASE)


def _maybe_encode_powershell_block(cmd):
    """
    If *cmd* is a PowerShell invocation that uses ``-Command { block }`` syntax,
    replace it with ``-EncodedCommand <base64>`` so the script block is executed
    rather than returned as a ScriptBlock object when stdout is not a console.
    """
    if not _PS_EXE_RE.search(cmd):
        return cmd
    m = _PS_BLOCK_RE.search(cmd)
    if not m:
        return cmd
    content = m.group(1)
    encoded = base64.b64encode(content.encode("utf-16-le")).decode("ascii")
    return cmd[: m.start()] + f"-EncodedCommand {encoded}"
