require 'puppet/util/windows'

module Puppet::Util::Windows::SID
  class Principal
    extend FFI::Library
    attr_reader :account, :sid_bytes, :sid, :domain, :domain_account, :account_type

    def initialize(account, sid_bytes, sid, domain, account_type)
      # This is only ever called from lookup_account_sid which has already
      # removed the potential for passing in an account like host\user
      @account = account
      @sid_bytes = sid_bytes
      @sid = sid
      @domain = domain
      @account_type = account_type
      # When domain is available and it is a Domain principal, use domain only
      #   otherwise if domain is available then combine it with parsed account
      #   otherwise when the domain is not available, use the account value directly
      # WinNT naming standard https://msdn.microsoft.com/en-us/library/windows/desktop/aa746534(v=vs.85).aspx
      if (domain && !domain.empty? && @account_type == :SidTypeDomain)
        @domain_account = @domain
      elsif (domain && !domain.empty?)
        @domain_account =  "#{domain}\\#{@account}"
      else
        @domain_account = account
      end
    end

    # added for backward compatibility
    def ==(compare)
      compare.is_a?(Puppet::Util::Windows::SID::Principal) &&
        @sid_bytes == compare.sid_bytes
    end

    # added for backward compatibility
    def to_s
      @sid
    end

    # = 8 + max sub identifiers (15) * 4
    MAXIMUM_SID_BYTE_LENGTH = 68

    ERROR_INSUFFICIENT_BUFFER = 122

    def self.lookup_account_name(system_name = nil, account_name)
      system_name_ptr = FFI::Pointer::NULL
      begin
        if system_name
          system_name_wide = Puppet::Util::Windows::String.wide_string(system_name)
          # uchar here is synonymous with byte
          system_name_ptr = FFI::MemoryPointer.new(:byte, system_name_wide.bytesize)
          system_name_ptr.put_array_of_uchar(0, system_name_wide.bytes.to_a)
        end

        FFI::MemoryPointer.from_string_to_wide_string(account_name) do |account_name_ptr|
          FFI::MemoryPointer.new(:byte, MAXIMUM_SID_BYTE_LENGTH) do |sid_ptr|
            FFI::MemoryPointer.new(:dword, 1) do |sid_length_ptr|
              FFI::MemoryPointer.new(:dword, 1) do |domain_length_ptr|
                FFI::MemoryPointer.new(:uint32, 1) do |name_use_enum_ptr|

                sid_length_ptr.write_dword(MAXIMUM_SID_BYTE_LENGTH)
                success = LookupAccountNameW(system_name_ptr, account_name_ptr, sid_ptr, sid_length_ptr,
                  FFI::Pointer::NULL, domain_length_ptr, name_use_enum_ptr)
                last_error = FFI.errno

                if (success == FFI::WIN32_FALSE && last_error != ERROR_INSUFFICIENT_BUFFER)
                  raise Puppet::Util::Windows::Error.new(_('Failed to call LookupAccountNameW'), last_error)
                end

                FFI::MemoryPointer.new(:lpwstr, domain_length_ptr.read_dword) do |domain_ptr|
                  if LookupAccountNameW(system_name_ptr, account_name_ptr,
                      sid_ptr, sid_length_ptr,
                      domain_ptr, domain_length_ptr, name_use_enum_ptr) == FFI::WIN32_FALSE
                   raise Puppet::Util::Windows::Error.new(_('Failed to call LookupAccountNameW'))
                  end

                  # with a SID returned, loop back through lookup_account_sid to retrieve official name
                  # necessary when accounts like . or '' are passed in
                  return lookup_account_sid(
                    system_name,
                    sid_ptr.read_bytes(sid_length_ptr.read_dword).unpack('C*'))
                  end
                end
              end
            end
          end
        end
      ensure
        system_name_ptr.free if system_name_ptr != FFI::Pointer::NULL
      end
    end

    def self.lookup_account_sid(system_name = nil, sid_bytes)
      system_name_ptr = FFI::Pointer::NULL
      if (sid_bytes.nil? || (!sid_bytes.is_a? Array) || (sid_bytes.length == 0))
        #TRANSLATORS `lookup_account_sid` is a variable name and should not be translated
        raise Puppet::Util::Windows::Error.new(_('Byte array for lookup_account_sid must not be nil and must be at least 1 byte long'))
      end

      begin
        if system_name
          system_name_wide = Puppet::Util::Windows::String.wide_string(system_name)
          # uchar here is synonymous with byte
          system_name_ptr = FFI::MemoryPointer.new(:byte, system_name_wide.bytesize)
          system_name_ptr.put_array_of_uchar(0, system_name_wide.bytes.to_a)
        end

        FFI::MemoryPointer.new(:byte, sid_bytes.length) do |sid_ptr|
          FFI::MemoryPointer.new(:dword, 1) do |name_length_ptr|
            FFI::MemoryPointer.new(:dword, 1) do |domain_length_ptr|
              FFI::MemoryPointer.new(:uint32, 1) do |name_use_enum_ptr|

                sid_ptr.write_array_of_uchar(sid_bytes)
                success = LookupAccountSidW(system_name_ptr, sid_ptr, FFI::Pointer::NULL, name_length_ptr,
                  FFI::Pointer::NULL, domain_length_ptr, name_use_enum_ptr)
                last_error = FFI.errno

                if (success == FFI::WIN32_FALSE && last_error != ERROR_INSUFFICIENT_BUFFER)
                  raise Puppet::Util::Windows::Error.new(_('Failed to call LookupAccountSidW'), last_error)
                end

                FFI::MemoryPointer.new(:lpwstr, name_length_ptr.read_dword) do |name_ptr|
                  FFI::MemoryPointer.new(:lpwstr, domain_length_ptr.read_dword) do |domain_ptr|
                    if LookupAccountSidW(system_name_ptr, sid_ptr, name_ptr, name_length_ptr,
                        domain_ptr, domain_length_ptr, name_use_enum_ptr) == FFI::WIN32_FALSE
                     raise Puppet::Util::Windows::Error.new(_('Failed to call LookupAccountSidW'))
                    end

                    return new(
                      name_ptr.read_wide_string(name_length_ptr.read_dword),
                      sid_bytes,
                      Puppet::Util::Windows::SID.sid_ptr_to_string(sid_ptr),
                      domain_ptr.read_wide_string(domain_length_ptr.read_dword),
                      SID_NAME_USE[name_use_enum_ptr.read_uint32])
                  end
                end
              end
            end
          end
        end
      ensure
        system_name_ptr.free if system_name_ptr != FFI::Pointer::NULL
      end
    end

    ffi_convention :stdcall

    # https://msdn.microsoft.com/en-us/library/windows/desktop/aa379601(v=vs.85).aspx
    SID_NAME_USE = enum(
      :SidTypeUser, 1,
      :SidTypeGroup, 2,
      :SidTypeDomain, 3,
      :SidTypeAlias, 4,
      :SidTypeWellKnownGroup, 5,
      :SidTypeDeletedAccount, 6,
      :SidTypeInvalid, 7,
      :SidTypeUnknown, 8,
      :SidTypeComputer, 9,
      :SidTypeLabel, 10
    )

    # https://msdn.microsoft.com/en-us/library/windows/desktop/aa379159(v=vs.85).aspx
    # BOOL WINAPI LookupAccountName(
    #   _In_opt_  LPCTSTR       lpSystemName,
    #   _In_      LPCTSTR       lpAccountName,
    #   _Out_opt_ PSID          Sid,
    #   _Inout_   LPDWORD       cbSid,
    #   _Out_opt_ LPTSTR        ReferencedDomainName,
    #   _Inout_   LPDWORD       cchReferencedDomainName,
    #   _Out_     PSID_NAME_USE peUse
    # );
    ffi_lib :advapi32
    attach_function_private :LookupAccountNameW,
      [:lpcwstr, :lpcwstr, :pointer, :lpdword, :lpwstr, :lpdword, :pointer], :win32_bool

    # https://msdn.microsoft.com/en-us/library/windows/desktop/aa379166(v=vs.85).aspx
    # BOOL WINAPI LookupAccountSid(
    #   _In_opt_  LPCTSTR       lpSystemName,
    #   _In_      PSID          lpSid,
    #   _Out_opt_ LPTSTR        lpName,
    #   _Inout_   LPDWORD       cchName,
    #   _Out_opt_ LPTSTR        lpReferencedDomainName,
    #   _Inout_   LPDWORD       cchReferencedDomainName,
    #   _Out_     PSID_NAME_USE peUse
    # );
    ffi_lib :advapi32
    attach_function_private :LookupAccountSidW,
      [:lpcwstr, :pointer, :lpwstr, :lpdword, :lpwstr, :lpdword, :pointer], :win32_bool
  end
end

