Member = mondelefant.new_class()
Member.table = 'member'

local function secret_token()
  local parts = {}
  for i = 1, 5 do
    parts[#parts+1] = multirand.string(5, "23456789bcdfghjkmnpqrstvwxyz")
  end
  return (table.concat(parts, "-"))
end

Member:add_reference{
  mode          = "1m",
  to            = "MemberHistory",
  this_key      = 'id',
  that_key      = 'member_id',
  ref           = 'history_entries',
  back_ref      = 'member'
}

Member:add_reference{
  mode          = '1m',
  to            = "MemberImage",
  this_key      = 'id',
  that_key      = 'member_id',
  ref           = 'images',
  back_ref      = 'member'
}

Member:add_reference{
  mode          = '1m',
  to            = "Contact",
  this_key      = 'id',
  that_key      = 'member_id',
  ref           = 'contacts',
  back_ref      = 'member',
  default_order = '"other_member_id"'
}

Member:add_reference{
  mode          = '1m',
  to            = "Contact",
  this_key      = 'id',
  that_key      = 'member_id',
  ref           = 'foreign_contacts',
  back_ref      = 'other_member',
  default_order = '"member_id"'
}

Member:add_reference{
  mode          = '1m',
  to            = "Session",
  this_key      = 'id',
  that_key      = 'member_id',
  ref           = 'sessions',
  back_ref      = 'member',
  default_order = '"ident"'
}

Member:add_reference{
  mode          = '1m',
  to            = "Draft",
  this_key      = 'id',
  that_key      = 'author_id',
  ref           = 'drafts',
  back_ref      = 'author',
  default_order = '"id"'
}

Member:add_reference{
  mode          = '1m',
  to            = "Suggestion",
  this_key      = 'id',
  that_key      = 'author_id',
  ref           = 'suggestions',
  back_ref      = 'author',
  default_order = '"id"'
}

Member:add_reference{
  mode          = '1m',
  to            = "Membership",
  this_key      = 'id',
  that_key      = 'member_id',
  ref           = 'memberships',
  back_ref      = 'member',
  default_order = '"area_id"'
}

Member:add_reference{
  mode          = '1m',
  to            = "Interest",
  this_key      = 'id',
  that_key      = 'member_id',
  ref           = 'interests',
  back_ref      = 'member',
  default_order = '"id"'
}

Member:add_reference{
  mode          = '1m',
  to            = "Initiator",
  this_key      = 'id',
  that_key      = 'member_id',
  ref           = 'initiators',
  back_ref      = 'member'
}

Member:add_reference{
  mode          = '1m',
  to            = "Supporter",
  this_key      = 'id',
  that_key      = 'member_id',
  ref           = 'supporters',
  back_ref      = 'member'
}

Member:add_reference{
  mode          = '1m',
  to            = "Opinion",
  this_key      = 'id',
  that_key      = 'member_id',
  ref           = 'opinions',
  back_ref      = 'member',
  default_order = '"id"'
}

Member:add_reference{
  mode          = '1m',
  to            = "Delegation",
  this_key      = 'id',
  that_key      = 'truster_id',
  ref           = 'outgoing_delegations',
  back_ref      = 'truster',
--  default_order = '"id"'
}

Member:add_reference{
  mode          = '1m',
  to            = "Delegation",
  this_key      = 'id',
  that_key      = 'trustee_id',
  ref           = 'incoming_delegations',
  back_ref      = 'trustee',
--  default_order = '"id"'
}

Member:add_reference{
  mode          = '1m',
  to            = "DirectVoter",
  this_key      = 'id',
  that_key      = 'member_id',
  ref           = 'direct_voter',
  back_ref      = 'member',
  default_order = '"issue_id"'
}

Member:add_reference{
  mode          = '1m',
  to            = "Vote",
  this_key      = 'id',
  that_key      = 'member_id',
  ref           = 'vote',
  back_ref      = 'member',
  default_order = '"issue_id", "initiative_id"'
}

Member:add_reference{
  mode          = '11',
  to            = "MemberProfile",
  this_key      = 'id',
  that_key      = 'member_id',
  ref           = 'profile',
  back_ref      = 'member'
}

Member:add_reference{
  mode          = '11',
  to            = "MemberSettings",
  this_key      = 'id',
  that_key      = 'member_id',
  ref           = 'settings',
  back_ref      = 'member'
}

Member:add_reference{
  mode                  = 'mm',
  to                    = "Member",
  this_key              = 'id',
  that_key              = 'id',
  connected_by_table    = 'contact',
  connected_by_this_key = 'member_id',
  connected_by_that_key = 'other_member_id',
  ref                   = 'saved_members',
}

Member:add_reference{
  mode                  = 'mm',
  to                    = "Member",
  this_key              = 'id',
  that_key              = 'id',
  connected_by_table    = 'contact',
  connected_by_this_key = 'other_member_id',
  connected_by_that_key = 'member_id',
  ref                   = 'saved_by_members',
}

Member:add_reference{
  mode                  = 'mm',
  to                    = "Unit",
  this_key              = 'id',
  that_key              = 'id',
  connected_by_table    = 'privilege',
  connected_by_this_key = 'member_id',
  connected_by_that_key = 'unit_id',
  ref                   = 'units'
}

Member:add_reference{
  mode                  = 'mm',
  to                    = "Area",
  this_key              = 'id',
  that_key              = 'id',
  connected_by_table    = 'membership',
  connected_by_this_key = 'member_id',
  connected_by_that_key = 'area_id',
  ref                   = 'areas'
}

Member:add_reference{
  mode                  = 'mm',
  to                    = "Issue",
  this_key              = 'id',
  that_key              = 'id',
  connected_by_table    = 'interest',
  connected_by_this_key = 'member_id',
  connected_by_that_key = 'issue_id',
  ref                   = 'issues'
}

Member:add_reference{
  mode                  = 'mm',
  to                    = "Initiative",
  this_key              = 'id',
  that_key              = 'id',
  connected_by_table    = 'initiator',
  connected_by_this_key = 'member_id',
  connected_by_that_key = 'initiative_id',
  ref                   = 'initiated_initiatives'
}

Member:add_reference{
  mode                  = 'mm',
  to                    = "Initiative",
  this_key              = 'id',
  that_key              = 'id',
  connected_by_table    = 'supporter',
  connected_by_this_key = 'member_id',
  connected_by_that_key = 'initiative_id',
  ref                   = 'supported_initiatives'
}

function Member:build_selector(args)
  local selector = self:new_selector()
  if args.active ~= nil then
    selector:add_where{ "member.active = ?", args.active }
  end
  if args.locked ~= nil then
    selector:add_where{ "member.locked = ?", args.locked }
  end
  if args.is_contact_of_member_id then
    selector:join("contact", "__model_member__contact", "member.id = __model_member__contact.other_member_id")
    selector:add_where{ "__model_member__contact.member_id = ?", args.is_contact_of_member_id }
  end
  if args.voting_right_for_unit_id then
    selector:join("privilege", "__model_member__privilege", { "member.id = __model_member__privilege.member_id AND __model_member__privilege.voting_right AND __model_member__privilege.unit_id = ?", args.voting_right_for_unit_id })
  end
  if args.admin_search then
    local search_string = "%" .. args.admin_search .. "%"
    selector:add_where{ "member.identification ILIKE ? OR member.name ILIKE ?", search_string, search_string }
  end
  if args.order then
    if args.order == "id" then
      selector:add_order_by("id")
    elseif args.order == "identification" then
      selector:add_order_by("identification")
    elseif args.order == "name" then
      selector:add_order_by("name")
    else
      error("invalid order")
    end
  end
  return selector
end

function Member:lockForReference()
  self.get_db_conn().query("LOCK TABLE " .. self:get_qualified_table() .. " IN ROW SHARE MODE")
end


function Member:get_all_by_authority(authority)
  
  local members = Member:new_selector()
    :add_where{ "authority = ?", authority }
    :add_field("authority_uid")
    :exec()
    
  return members
end

function Member.object:set_password(password)
  trace.disable()
  
  local hash_prefix
  local salt_length

  local function rounds()
    return multirand.integer(
      config.password_hash_min_rounds,
      config.password_hash_max_rounds
    )
  end
      
  if config.password_hash_algorithm == "crypt_md5" then
    hash_prefix = "$1$" 
    salt_length = 8
    
  elseif config.password_hash_algorithm == "crypt_sha256" then
    hash_prefix = "$5$rounds=" .. rounds() .. "$"
    salt_length = 16
    
  elseif config.password_hash_algorithm == "crypt_sha512" then
    hash_prefix = "$6$rounds=" .. rounds() .. "$"
    salt_length = 16
    
  else
    error("Unknown hash algorithm selected in configuration")

  end
  
  hash_prefix = hash_prefix .. multirand.string(
    salt_length,
    "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz./"
  )

  local hash = extos.crypt(password, hash_prefix)

  if not hash or hash:sub(1, #hash_prefix) ~= hash_prefix then
    error("Password hashing algorithm failed")
  end
  
  self.password = hash
  self.password_reset_secret = nil
  self.password_reset_secret_expiry = nil
end

function Member.object:check_password(password)
  if type(password) == "string" and type(self.password) == "string" then
    return extos.crypt(password, self.password) == self.password
  else
    return false
  end
end

function Member.object_get:password_hash_needs_update()
  
  if self.password == nil then
    return nil
  end

  local function check_rounds(rounds)
    if rounds then
      rounds = tonumber(rounds)
      if 
        rounds >= config.password_hash_min_rounds and 
        rounds <= config.password_hash_max_rounds
      then
        return false
      end
    end
    return true
  end
  
  if config.password_hash_algorithm == "crypt_md5" then

    return self.password:sub(1,3) ~= "$1$"
    
  elseif config.password_hash_algorithm == "crypt_sha256" then
    
    return check_rounds(self.password:match("^%$5%$rounds=([1-9][0-9]*)%$"))
    
  elseif config.password_hash_algorithm == "crypt_sha512" then

    return check_rounds(self.password:match("^%$6%$rounds=([1-9][0-9]*)%$"))

  else
    error("Unknown hash algorithm selected in configuration")

  end
  
end

function Member.object_get:published_contacts()
  return Member:new_selector()
    :join('"contact"', nil, '"contact"."other_member_id" = "member"."id"')
    :add_where{ '"contact"."member_id" = ?', self.id }
    :add_where("public")
    :exec()
end

function Member:by_login_and_password(login, password)
 
  local function prepare_login_selector()
    local selector = self:new_selector()
    selector:add_field({ "now() > COALESCE(last_delegation_check, activated) + ?::interval", config.check_delegations_interval_hard }, "needs_delegation_check_hard")
    selector:optional_object_mode()
    return selector
  end
  
  local function do_local_login()
    local selector = prepare_login_selector()
    selector:add_where{'"login" = ?', login }
    local member = selector:exec()
    if member and member:check_password(password) then
      return member
    else
      return nil
    end
  end
  
  if config.ldap.member then

    -- Let's check the users credentials against the LDAP      
    local ldap_entry, ldap_err = ldap.check_credentials(login, password)

    -- Is the user already registered as member?
    local uid
    local selector = prepare_login_selector()

    -- Get login name from LDAP entry
    if ldap_entry then
      uid = config.ldap.member.uid_map(ldap_entry)
      selector:add_where{'"authority" = ? AND "authority_uid" = ?', "ldap", uid }

    -- or build it from the login
    else
      login = config.ldap.member.login_normalizer(login)
      selector:add_where{'"authority" = ? AND "authority_uid" = ?', "ldap", login }
    end
    
    local member = selector:exec()
    -- The member is already registered
    if member then

      -- The credentials entered by the user are invalid
      if ldap_err == "invalid_credentials" then
        
        -- Check if the user tried a cached password (which is invalid now)
        if config.ldap.member.cache_passwords and member:check_password(password) then
          member.password = nil
          member:save()
        end
        
        -- Try a regular login
        return do_local_login()

      end
      
      -- The credentials were accepted by the LDAP server and no error occured
      if ldap_entry and not ldap_err then
        
        -- Cache the password (if feature enabled)
        if config.ldap.member.cache_passwords and not member:check_password(password) then
          member:set_password(password)
        end

        -- update the member attributes and privileges from LDAP
        local ldap_conn, ldap_entry, err, err2 = ldap.update_member_attr(member, nil, uid)
        if not err then
          ldap.update_member_allowed(member, ldap_entry)
          local err = member:try_save()
          if err then
            return nil, "member_save_error", err
          end
          if member.locked then
            return nil, "member_locked"
          end
          local succes, err, err2 = ldap.update_member_privileges(member, ldap_entry)
          if err then
            return nil, "update_member_privileges_error", err, err2
          end
          return member
        end

      end

      -- Some kind of LDAP error happened, if cached password are enabled,
      -- check user credentials against the cache
      if config.ldap.member.cache_passwords and member:check_password(password) then

        -- return the successfully logged in member
        return member

      end
      
    -- The member is not registered
    elseif config.ldap.member.registration and ldap_entry and not ldap_err then
      -- Automatic registration ("auto")
      if config.ldap.member.registration == "auto" then
        member = Member:new()
        member.authority = "ldap"
        local ldap_login
        if config.ldap.member.cache_passwords then 
          if config.ldap.member.login_normalizer then
            ldap_login = config.ldap.member.login_normalizer(login)
          else
            ldap_login = login
          end
        end
        -- TODO change this when SQL layers supports hstore
        member.authority_uid = uid
        member.authority_login = ldap_login
        member.activated = "now"
        member.last_activity = "now"
        if config.ldap.member.cache_passwords then
          member:set_password(password)
        end
        local ldap_conn, ldap_entry, err, err2 = ldap.update_member_attr(member, nil, uid)
        if not err then
          ldap.update_member_allowed(member, ldap_entry)
          if member.locked then
            return nil, "member_not_allowed"
          end
          local err = member:try_save()
          if err then
            return nil, "member_save_error", err
          end
          local success, err, err2 = ldap.update_member_privileges(member, ldap_entry)
          if err then
            return nil, "update_member_privileges_error", err, err2
          end
          return member
        end

      -- No automatic registration
      else
        return nil, "ldap_credentials_valid_but_no_member", uid
      end
    end
    
  end

  return do_local_login()
  
end

function Member:by_ids(ids)
  local selector = self:new_selector()
  selector:add_where{'"id" IN ($)', { ids } }
  return selector:exec()
end

function Member:by_login(login)
  local selector = self:new_selector()
  selector:add_where{'"login" = ?', login }
  selector:optional_object_mode()
  return selector:exec()
end

function Member:by_name(name)
  local selector = self:new_selector()
  selector:add_where{'"name" = ?', name }
  selector:optional_object_mode()
  return selector:exec()
end

function Member:get_search_selector(search_string)
  return self:new_selector()
    :add_field( {'"highlight"("member"."name", ?)', search_string }, "name_highlighted")
    :add_where{ 'to_tsvector("member") @@ "plainto_tsquery"(?)', search_string }
    :add_where("activated NOTNULL AND active")
end

function Member.object:send_password_reset_mail()
  trace.disable()
  if not self.notify_email then
    return false
  end
  self.password_reset_secret = secret_token()
  local expiry = db:query("SELECT now() + '1 days'::interval as expiry", "object").expiry
  self.password_reset_secret_expiry = expiry
  self:save()
  local content = slot.use_temporary(function()
    slot.put(_"Hello " .. self.name .. ",\n\n")
    slot.put(_"to reset your password please click on the following link:\n\n")
    slot.put(request.get_absolute_baseurl() .. "index/reset_password.html?secret=" .. self.password_reset_secret .. "\n\n")
    slot.put(_"If this link is not working, please open following url in your web browser:\n\n")
    slot.put(request.get_absolute_baseurl() .. "index/reset_password.html\n\n")
    slot.put(_"On that page please enter the reset code:\n\n")
    slot.put(self.password_reset_secret .. "\n\n")
  end)
  local success = net.send_mail{
    envelope_from = config.mail_envelope_from,
    from          = config.mail_from,
    reply_to      = config.mail_reply_to,
    to            = self.notify_email,
    subject       = config.mail_subject_prefix .. _"Password reset request",
    content_type  = "text/plain; charset=UTF-8",
    content       = content
  }
  return success
end

function Member.object:send_invitation(template_file, subject)
  trace.disable()
  self.invite_code = secret_token()
  self:save()
  
  local subject = subject
  local content
  local baseurl = request.get_absolute_baseurl() .. "index/register.html"
  local url = baseurl .. "?skip=1&code=" .. self.invite_code
  if template_file then
    local fh = io.open(template_file, "r")
    content = fh:read("*a")
    content = content:gsub("#{url}", url):gsub("#{baseurl}", baseurl):gsub("#{code}", self.invite_code)
  elseif config.invitation_mail then
    subject = config.invitation_mail.subject
    content = config.invitation_mail.content:gsub("#{url}", url):gsub("#{baseurl}", baseurl):gsub("#{code}", self.invite_code)
  else
    subject = config.mail_subject_prefix .. _"Invitation to LiquidFeedback"
    content = slot.use_temporary(function()
      slot.put(_"Hello\n\n")
      slot.put(_"You are invited to LiquidFeedback. To register please click the following link:\n\n")
      slot.put(url .. "\n\n")
      slot.put(_"If this link is not working, please open following url in your web browser:\n\n")
      slot.put(baseurl .. "\n\n")
      slot.put(_"On that page please enter the invite key:\n\n")
      slot.put(self.invite_code .. "\n\n")
    end)
  end

  local success = net.send_mail{
    envelope_from = config.mail_envelope_from,
    from          = config.mail_from,
    reply_to      = config.mail_reply_to,
    to            = self.notify_email_unconfirmed or self.notify_email,
    subject       = subject,
    content_type  = "text/plain; charset=UTF-8",
    content       = content
  }
  return success
end

function Member.object:set_notify_email(notify_email)
  trace.disable()
  local expiry = db:query("SELECT now() + '7 days'::interval as expiry", "object").expiry
  self.notify_email_unconfirmed = notify_email
  self.notify_email_secret = secret_token()
  self.notify_email_secret_expiry = expiry
  local content = slot.use_temporary(function()
    slot.put(_"Hello " .. self.name .. ",\n\n")
    slot.put(_"Please confirm your email address by clicking the following link:\n\n")
    slot.put(request.get_absolute_baseurl() .. "index/confirm_notify_email.html?secret=" .. self.notify_email_secret .. "\n\n")
    slot.put(_"If this link is not working, please open following url in your web browser:\n\n")
    slot.put(request.get_absolute_baseurl() .. "index/confirm_notify_email.html\n\n")
    slot.put(_"On that page please enter the confirmation code:\n\n")
    slot.put(self.notify_email_secret .. "\n\n")
  end)
  local success = net.send_mail{
    envelope_from = config.mail_envelope_from,
    from          = config.mail_from,
    reply_to      = config.mail_reply_to,
    to            = self.notify_email_unconfirmed,
    subject       = config.mail_subject_prefix .. _"Email confirmation request",
    content_type  = "text/plain; charset=UTF-8",
    content       = content
  }
  if success then
    local lock_expiry = db:query("SELECT now() + '1 hour'::interval AS lock_expiry", "object").lock_expiry
    self.notify_email_lock_expiry = lock_expiry
  end
  self:save()
  return success
end

function Member.object:get_setting(key)
  return Setting:by_pk(self.id, key)
end

function Member.object:get_setting_value(key)
  local setting = Setting:by_pk(self.id, key)
  if setting then
    return setting.value
  end
end

function Member.object:set_setting(key, value)
  local setting = self:get_setting(key)
  if not setting then
    setting = Setting:new()
    setting.member_id = self.id
    setting.key = key
  end
  setting.value = value
  setting:save()
end

function Member.object:get_setting_maps_by_key(key)
  return SettingMap:new_selector()
    :add_where{ "member_id = ?", self.id }
    :add_where{ "key = ?", key }
    :add_order_by("subkey")
    :exec()
end

function Member.object:get_setting_map_by_key_and_subkey(key, subkey)
  return SettingMap:new_selector()
    :add_where{ "member_id = ?", self.id }
    :add_where{ "key = ?", key }
    :add_where{ "subkey = ?", subkey }
    :add_order_by("subkey")
    :optional_object_mode()
    :exec()
end

function Member.object:set_setting_map(key, subkey, value)
  setting_map = self:get_setting_map_by_key_and_subkey(key, subkey)
  if not setting_map then
    setting_map = SettingMap:new()
    setting_map.member_id = self.id
    setting_map.key = key
    setting_map.subkey = subkey
  end
  setting_map.value = value
  setting_map:save()
end

function Member.object_get:notify_email_locked()
  return(
    Member:new_selector()
      :add_where{ "id = ?", app.session.member.id }
      :add_where("notify_email_lock_expiry > now()")
      :count() == 1
  )
end

function Member.object_get:units_with_voting_right()
  return(Unit:new_selector()
    :join("privilege", nil, { "privilege.unit_id = unit.id AND privilege.member_id = ? AND privilege.voting_right", self.id })
    :exec()
  )
end

function Member.object:ui_field_text(args)
  args = args or {}
  if app.session:has_access("authors_pseudonymous") then
    -- ugly workaround for getting html into a replaced string and to the user
    ui.container{label = args.label, label_attr={class="ui_field_label"}, content = function()
        slot.put(string.format('<span><a href="%s">%s</a></span>',
                                                encode.url{
                                                  module    = "member",
                                                  view      = "show",
                                                  id        = self.id,
                                                },
                                                encode.html(self.name)))
      end
    }
  else
    ui.field.text{ label = args.label,      value = _"[not displayed public]" }
  end
end

local function populate_units_with_initiative_right_hash(self)
  if not self.__units_with_initiative_right_hash then
    local privileges = Privilege:new_selector()
      :add_where{ "member_id = ?", self.id }
      :add_where("initiative_right")
      :exec()
    self.__units_with_initiative_right_hash = {}
    for i, privilege in ipairs(privileges) do
      self.__units_with_initiative_right_hash[privilege.unit_id] = true
    end
  end
end

function Member.object:has_initiative_right_for_unit_id(unit_id)
  populate_units_with_initiative_right_hash(self)
  return self.__units_with_initiative_right_hash[unit_id] and true or false
end

function Member.object_get:has_initiative_right()
  populate_units_with_initiative_right_hash(self)
  for k, v in pairs(self.__units_with_initiative_right_hash) do
    return true
  end
  return false
end

local function populate_units_with_voting_right_hash(self)
  if not self.__units_with_voting_right_hash then
    local privileges = Privilege:new_selector()
      :add_where{ "member_id = ?", self.id }
      :add_where("voting_right")
      :exec()
    self.__units_with_voting_right_hash = {}
    for i, privilege in ipairs(privileges) do
      self.__units_with_voting_right_hash[privilege.unit_id] = true
    end
  end
end

function Member.object_get:has_voting_right()
  populate_units_with_voting_right_hash(self)
  for k, v in pairs(self.__units_with_voting_right_hash) do
    return true
  end
  return false
end

function Member.object:has_voting_right_for_unit_id(unit_id)
  populate_units_with_voting_right_hash(self)
  return self.__units_with_voting_right_hash[unit_id] and true or false
end

function Member.object:has_polling_right_for_unit_id(unit_id)
  if not self.__units_with_polling_right_hash then
    local privileges = Privilege:new_selector()
      :add_where{ "member_id = ?", self.id }
      :add_where("polling_right")
      :exec()
    self.__units_with_polling_right_hash = {}
    for i, privilege in ipairs(privileges) do
      self.__units_with_polling_right_hash[privilege.unit_id] = true
    end
  end
  return self.__units_with_polling_right_hash[unit_id] and true or false
end

function Member.object:get_delegatee_member(unit_id, area_id, issue_id)
  local selector = Member:new_selector()
  if unit_id then
    selector:join("delegation", nil, { "delegation.trustee_id = member.id AND delegation.scope = 'unit' AND delegation.unit_id = ? AND delegation.truster_id = ?", unit_id, self.id })
  end
  selector:optional_object_mode()
  return selector:exec()
end

function Member.object:has_role(role)
  self:load("units")
  for i, unit in ipairs(self.units) do
    if unit.attr.role == role then
      return true
    end
  end
  return false
end

function Member.object:delete()
  db:query{ "SELECT delete_member(?)", self.id }
end

function Member.object_get:display_name()
  if self.identification then
    return self.identification
  elseif self.name then
    return self.name
  else
    return "Member #" .. self.id
  end
end
