/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License") +  you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.openmeetings.db.entity.user;

import static org.apache.openmeetings.db.bind.Constants.USER_NODE;
import static org.apache.openmeetings.db.dao.user.UserDao.FETCH_GROUP_BACKUP;
import static org.apache.openmeetings.db.dao.user.UserDao.FETCH_GROUP_GROUP;
import static org.apache.openmeetings.util.OpenmeetingsVariables.getSipContext;
import static org.apache.openmeetings.util.OpenmeetingsVariables.isSipEnabled;
import static org.apache.wicket.util.string.Strings.escapeMarkup;

import java.security.NoSuchAlgorithmException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;

import javax.persistence.Basic;
import javax.persistence.CascadeType;
import javax.persistence.CollectionTable;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.JoinColumn;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.PrimaryKeyJoinColumn;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

import org.apache.openjpa.persistence.ElementDependent;
import org.apache.openjpa.persistence.FetchAttribute;
import org.apache.openjpa.persistence.FetchGroup;
import org.apache.openjpa.persistence.FetchGroups;
import org.apache.openjpa.persistence.LoadFetchGroup;
import org.apache.openjpa.persistence.jdbc.ForeignKey;
import org.apache.openmeetings.db.bind.adapter.BooleanAdapter;
import org.apache.openmeetings.db.bind.adapter.DateAdapter;
import org.apache.openmeetings.db.bind.adapter.LocalDateAdapter;
import org.apache.openmeetings.db.bind.adapter.LongAdapter;
import org.apache.openmeetings.db.bind.adapter.SalutationAdapter;
import org.apache.openmeetings.db.bind.adapter.UserRightAdapter;
import org.apache.openmeetings.db.bind.adapter.UserTypeAdapter;
import org.apache.openmeetings.db.dao.label.LabelDao;
import org.apache.openmeetings.db.entity.HistoricalEntity;
import org.apache.openmeetings.db.entity.label.OmLanguage;
import org.apache.openmeetings.db.entity.server.Sessiondata;
import org.apache.openmeetings.db.util.MD5;
import org.apache.openmeetings.util.crypt.CryptProvider;
import org.apache.wicket.util.string.Strings;

/**
 * Entity to store user data, password field is {@link FetchType#LAZY}, so that
 * is why there is an extra udpate statement at this moment
 *
 * @author sebawagner, solomax
 *
 */
@Entity
@FetchGroups({
	@FetchGroup(name = FETCH_GROUP_BACKUP, attributes = { @FetchAttribute(name = "password") })
	, @FetchGroup(name = FETCH_GROUP_GROUP, attributes = { @FetchAttribute(name = "groupUsers")})
})
@NamedQuery(name = "getUserById", query = "SELECT u FROM User u WHERE u.id = :id")
@NamedQuery(name = "getUsersByIds", query = "select c from User c where c.id IN :ids")
@NamedQuery(name = "getUserByLogin", query = "SELECT u FROM User u WHERE u.deleted = false AND u.type = :type AND u.login = :login AND ((:domainId = 0 AND u.domainId IS NULL) OR (:domainId > 0 AND u.domainId = :domainId))")
@NamedQuery(name = "getUserByEmail", query = "SELECT u FROM User u WHERE u.deleted = false AND u.type = :type AND u.address.email = :email AND ((:domainId = 0 AND u.domainId IS NULL) OR (:domainId > 0 AND u.domainId = :domainId))")
@NamedQuery(name = "getUserByHash",  query = "SELECT u FROM User u WHERE u.deleted = false AND u.type = :type AND u.resethash = :resethash")
@NamedQuery(name = "getUserByExpiredHash",  query = "SELECT u FROM User u WHERE u.resetDate < :date")
@NamedQuery(name = "getContactByEmailAndUser", query = "SELECT u FROM User u WHERE u.deleted = false AND u.address.email = :email AND u.type = :type AND u.ownerId = :ownerId")
@NamedQuery(name = "selectMaxFromUsersWithSearch", query = "SELECT count(c.id) FROM User c "
		+ "WHERE c.deleted = false AND ("
		+ "lower(c.login) LIKE :search "
		+ "OR lower(c.firstname) LIKE :search "
		+ "OR lower(c.lastname) LIKE :search )")
@NamedQuery(name = "getAllUsers", query = "SELECT u FROM User u ORDER BY u.id")
@NamedQuery(name = "getPassword", query = "SELECT u.password FROM User u WHERE u.deleted = false AND u.id = :userId ")
@NamedQuery(name = "updatePassword", query = "UPDATE User u SET u.password = :password WHERE u.id = :userId")
@NamedQuery(name = "getNondeletedUsers", query = "SELECT u FROM User u WHERE u.deleted = false")
@NamedQuery(name = "countNondeletedUsers", query = "SELECT COUNT(u) FROM User u WHERE u.deleted = false")
@NamedQuery(name = "getUsersByGroupId", query = "SELECT u FROM User u WHERE u.deleted = false AND u.groupUsers.group.id = :groupId")
@NamedQuery(name = "getExternalUser", query = "SELECT gu.user FROM GroupUser gu WHERE "
		+ "gu.group.deleted = false AND gu.group.external = true AND gu.group.name = :externalType "
		+ "AND gu.user.deleted = false AND gu.user.type = :type AND gu.user.externalId = :externalId")
@NamedQuery(name = "getUserByLoginOrEmail", query = "SELECT u from User u WHERE u.deleted = false AND u.type = :type AND (u.login = :userOrEmail OR u.address.email = :userOrEmail)")
@Table(name = "om_user", indexes = {
		@Index(name = "login_idx", columnList = "login")
		, @Index(name = "lastname_idx", columnList = "lastname")
		, @Index(name = "firstname_idx", columnList = "firstname")
})
@XmlRootElement(name = USER_NODE)
public class User extends HistoricalEntity {
	private static final long serialVersionUID = 1L;
	public static final String DISPLAY_NAME_NA = "N/A";
	public static final int SALUTATION_MR_ID = 1;
	public static final int SALUTATION_MS_ID = 2;
	public static final int SALUTATION_MRS_ID = 3;
	public static final int SALUTATION_DR_ID = 4;
	public static final int SALUTATION_PROF_ID = 5;

	@XmlType(namespace="org.apache.openmeetings.user.right")
	public enum Right {
		ADMIN			// access to Admin module
		, GROUP_ADMIN	// partial access to Admin module (should not be directly assigned)
		, ROOM			// enter the room
		, DASHBOARD		// access the dashboard
		, LOGIN			// login to Om internal DB
		, SOAP			// use rest/soap calls
	}

	@XmlType(namespace="org.apache.openmeetings.user.type")
	public enum Type {
		USER
		, LDAP
		, OAUTH
		, EXTERNAL
		, CONTACT
	}
	@XmlType(namespace="org.apache.openmeetings.user.salutation")
	public enum Salutation {
		MR(SALUTATION_MR_ID)
		, MS(SALUTATION_MS_ID)
		, MRS(SALUTATION_MRS_ID)
		, DR(SALUTATION_DR_ID)
		, PROF(SALUTATION_PROF_ID);
		private int id;

		Salutation(int id) {
			this.id = id;
		}

		public int getId() {
			return id;
		}

		public static Salutation get(Long type) {
			return get(type == null ? 1 : type.intValue());
		}

		public static Salutation get(Integer type) {
			return get(type == null ? 1 : type.intValue());
		}

		public static Salutation get(int type) {
			Salutation rt = Salutation.MR;
			switch (type) {
				case SALUTATION_MS_ID:
					rt = Salutation.MS;
					break;
				case SALUTATION_MRS_ID:
					rt = Salutation.MRS;
					break;
				case SALUTATION_DR_ID:
					rt = Salutation.DR;
					break;
				case SALUTATION_PROF_ID:
					rt = Salutation.PROF;
					break;
				default:
					//no-op
			}
			return rt;
		}
	}

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "id")
	@XmlElement(name = "user_id", required = false)
	@XmlJavaTypeAdapter(LongAdapter.class)
	private Long id;

	@Column(name = "age")
	@XmlElement(name = "age", required = false)
	@XmlJavaTypeAdapter(LocalDateAdapter.class)
	private LocalDate age;

	@Column(name = "firstname")
	@XmlElement(name = "firstname", required = false)
	private String firstname;

	@Column(name = "lastlogin")
	@XmlTransient
	private Date lastlogin;

	@Column(name = "lastname")
	@XmlElement(name = "lastname", required = false)
	private String lastname;

	@Column(name = "displayName")
	@XmlElement(name = "displayName", required = false)
	private String displayName;

	@Column(name = "login")
	@XmlElement(name = "login", required = false)
	private String login;

	@Basic(fetch = FetchType.LAZY)
	@Column(name = "password", length = 1024)
	@LoadFetchGroup(FETCH_GROUP_BACKUP)
	@XmlElement(name = "pass", required = false)
	private String password;

	@Column(name = "regdate")
	@XmlElement(name = "regdate", required = false)
	@XmlJavaTypeAdapter(DateAdapter.class)
	private Date regdate;

	@Column(name = "salutation")
	@Enumerated(EnumType.STRING)
	@XmlElement(name = "title_id", required = false)
	@XmlJavaTypeAdapter(SalutationAdapter.class)
	private Salutation salutation;

	@Column(name = "pictureuri")
	@XmlElement(name = "pictureUri", required = false)
	private String pictureUri;

	@Column(name = "language_id")
	@XmlElement(name = "language_id", required = false)
	@XmlJavaTypeAdapter(value = LongAdapter.class, type = long.class)
	private long languageId;

	@Column(name = "resethash")
	@XmlElement(name = "resethash", required = false)
	private String resethash;

	@Column(name = "resetdate")
	@XmlElement(name = "resetDate", required = false)
	@XmlJavaTypeAdapter(DateAdapter.class)
	private Date resetDate;

	@Column(name = "activatehash")
	@XmlElement(name = "activatehash", required = false)
	private String activatehash;

	@OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true)
	@JoinColumn(name = "address_id", insertable = true, updatable = true)
	@ForeignKey(enabled = true)
	@XmlElement(name = "address", required = false)
	private Address address;

	@OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
	@JoinColumn(name = "user_id", insertable = true, updatable = true, nullable = false)
	@ElementDependent
	@XmlElementWrapper(name = "organisations", required = false)
	@XmlElement(name = "user_organisation", required = false)
	private List<GroupUser> groupUsers = new ArrayList<>();

	@OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true)
	@PrimaryKeyJoinColumn(name="sip_user_id", referencedColumnName="id")
	@XmlElement(name = "sipUser", required = false)
	private AsteriskSipUser sipUser;

	// Vars to simulate external Users
	@Column(name = "external_id")
	@XmlElement(name = "externalUserId", required = false)
	private String externalId;

	@XmlElement(name = "externalUserType", required = false)
	@Deprecated(since = "5.0")
	@Transient
	/**
	 * @deprecated External group should be used instead
	 */
	private String externalType;

	/**
	 * java.util.TimeZone Id
	 */
	@Column(name = "time_zone_id")
	@XmlElement(name = "timeZoneId", required = false)
	private String timeZoneId;

	@Transient
	@XmlTransient
	private Sessiondata sessionData;

	@Column(name = "user_offers")
	@XmlElement(name = "userOffers", required = false)
	private String userOffers;

	@Column(name = "user_searchs")
	@XmlElement(name = "userSearchs", required = false)
	private String userSearchs;

	@Column(name = "show_contact_data", nullable = false)
	@XmlElement(name = "showContactData", required = false)
	@XmlJavaTypeAdapter(value = BooleanAdapter.class, type = boolean.class)
	private boolean showContactData;

	@Column(name = "show_contact_data_to_contacts", nullable = false)
	@XmlElement(name = "showContactDataToContacts", required = false)
	@XmlJavaTypeAdapter(value = BooleanAdapter.class, type = boolean.class)
	private boolean showContactDataToContacts;

	@Column(name = "type")
	@Enumerated(EnumType.STRING)
	@XmlElement(name = "type", required = false)
	@XmlJavaTypeAdapter(UserTypeAdapter.class)
	private Type type = Type.USER;

	@Column(name = "owner_id")
	@XmlElement(name = "ownerId", required = false)
	@XmlJavaTypeAdapter(LongAdapter.class)
	private Long ownerId;

	@ElementCollection(fetch = FetchType.EAGER)
	@Column(name = "om_right")
	@CollectionTable(name = "om_user_right", joinColumns = @JoinColumn(name = "user_id"))
	@Enumerated(EnumType.STRING)
	@XmlElementWrapper(name = "rights", required = false)
	@XmlElement(name = "right", required = false)
	@XmlJavaTypeAdapter(UserRightAdapter.class)
	private Set<Right> rights = new HashSet<>();

	@Column(name = "domain_id")
	@XmlElement(name = "domainId", required = false)
	@XmlJavaTypeAdapter(LongAdapter.class)
	private Long domainId; // LDAP config id for LDAP, OAuth server id for OAuth

	@Override
	public Long getId() {
		return id;
	}

	@Override
	public void setId(Long id) {
		this.id = id;
	}

	public Address getAddress() {
		return address;
	}

	public void setAddress(Address address) {
		this.address = address;
	}

	public LocalDate getAge() {
		return age;
	}

	public void setAge(LocalDate age) {
		this.age = age == null ? LocalDate.now() : age;
	}

	public String getFirstname() {
		return firstname;
	}

	public User setFirstname(String firstname) {
		this.firstname = firstname;
		return this;
	}

	public Date getLastlogin() {
		return lastlogin;
	}

	public void setLastlogin(Date lastlogin) {
		this.lastlogin = lastlogin;
	}

	public String getLastname() {
		return lastname;
	}

	public User setLastname(String lastname) {
		this.lastname = lastname;
		return this;
	}

	public String getDisplayName() {
		return Strings.isEmpty(displayName) ? generateDisplayName() : displayName;
	}

	public User setDisplayName(String displayName) {
		if (!Strings.isEmpty(displayName)) {
			this.displayName = escapeMarkup(displayName).toString();
		}
		return this;
	}

	public void resetDisplayName() {
		displayName = generateDisplayName();
	}

	public String getLogin() {
		return login;
	}

	public void setLogin(String login) {
		this.login = login;
	}

	public void updatePassword(String pass) throws NoSuchAlgorithmException {
		if (isSipEnabled()) {
			AsteriskSipUser u = getSipUser();
			if (u == null) {
				setSipUser(u = new AsteriskSipUser());
			}
			String defaultRoomContext = getSipContext();
			u.setName(login);
			u.setDefaultuser(login);
			u.setMd5secret(MD5.checksum(login + ":asterisk:" + pass));
			u.setContext(defaultRoomContext);
			u.setHost("dynamic");
		} else {
			setSipUser(null);
		}
		password = CryptProvider.get().hash(pass);
	}

	public String getPassword() {
		return password;
	}

	/**
	 * For internal use only
	 * should not be used directly (for bean usage only)
	 *
	 * @param password - password to set
	 */
	public void setPassword(String password) {
		this.password = password;
	}

	public Date getRegdate() {
		return regdate;
	}

	public void setRegdate(Date regdate) {
		this.regdate = regdate;
	}

	public Salutation getSalutation() {
		return salutation;
	}

	public void setSalutation(Salutation salutation) {
		this.salutation = salutation;
	}

	public String getPictureUri() {
		return pictureUri;
	}

	public void setPictureUri(String pictureUri) {
		this.pictureUri = pictureUri;
	}

	public long getLanguageId() {
		return languageId;
	}

	public void setLanguageId(long languageId) {
		this.languageId = languageId;
	}

	public List<GroupUser> getGroupUsers() {
		return groupUsers;
	}

	public void addGroup(Group g) {
		if (groupUsers == null) {
			groupUsers = new ArrayList<>();
		}
		groupUsers.add(new GroupUser(g, this));
	}

	public void setGroupUsers(List<GroupUser> groupUsers) {
		if (groupUsers != null) {
			this.groupUsers = groupUsers;
		}
	}

	public String getResethash() {
		return resethash;
	}

	public void setResethash(String resethash) {
		this.resethash = resethash;
	}

	public Date getResetDate() {
		return resetDate;
	}

	public void setResetDate(Date resetDate) {
		this.resetDate = resetDate;
	}

	public String getActivatehash() {
		return activatehash;
	}

	public void setActivatehash(String activatehash) {
		this.activatehash = activatehash;
	}

	public String getExternalId() {
		return externalId;
	}

	public void setExternalId(String externalId) {
		this.externalId = externalId;
	}

	public String externalType() {
		Optional<String> extType = groupUsers == null
				? Optional.empty()
				: groupUsers.stream().filter(gu -> gu.getGroup().isExternal()).findFirst()
				.map(gu -> gu.getGroup().getName());
		return extType.isPresent() ? extType.get() : null;
	}

	@Deprecated(since = "5.0")
	/**
	 * @deprecated External group should be used instead
	 */
	public String getExternalType() {
		return externalType;
	}

	@Deprecated(since = "5.0")
	/**
	 * @deprecated External group should be used instead
	 */
	public void setExternalType(String externalType) {
		this.externalType = externalType;
	}

	public Sessiondata getSessionData() {
		return sessionData;
	}

	public void setSessionData(Sessiondata sessionData) {
		this.sessionData = sessionData;
	}

	public AsteriskSipUser getSipUser() {
		return sipUser;
	}

	public void setSipUser(AsteriskSipUser sipUser) {
		this.sipUser = sipUser;
	}

	public String getTimeZoneId() {
		return timeZoneId;
	}
	public void setTimeZoneId(String timeZoneId) {
		this.timeZoneId = timeZoneId;
	}

	public String getUserOffers() {
		return userOffers;
	}

	public void setUserOffers(String userOffers) {
		this.userOffers = userOffers;
	}

	public String getUserSearchs() {
		return userSearchs;
	}

	public void setUserSearchs(String userSearchs) {
		this.userSearchs = userSearchs;
	}

	public boolean isShowContactData() {
		return showContactData;
	}

	public void setShowContactData(boolean showContactData) {
		this.showContactData = showContactData;
	}

	public boolean isShowContactDataToContacts() {
		return showContactDataToContacts;
	}

	public void setShowContactDataToContacts(boolean showContactDataToContacts) {
		this.showContactDataToContacts = showContactDataToContacts;
	}

	public Type getType() {
		return type;
	}

	public void setType(Type type) {
		this.type = type;
	}

	public Long getOwnerId(){
		return ownerId;
	}

	public void setOwnerId(Long ownerId){
		this.ownerId = ownerId;
	}

	public Set<Right> getRights() {
		return rights;
	}

	public void setRights(Set<Right> rights) {
		this.rights = rights;
	}

	public Long getDomainId() {
		return domainId;
	}

	public void setDomainId(Long domainId) {
		this.domainId = domainId;
	}

	@Override
	public String toString() {
		return "User [id=" + id + ", firstname=" + firstname
				+ ", lastname=" + lastname + ", login=" + login
				+ ", pictureuri=" + pictureUri + ", deleted=" + isDeleted()
				+ ", languageId=" + languageId + ", address=" + address
				+ ", externalId=" + externalId + ", type=" + type + "]";
	}

	private String generateDisplayName() {
		StringBuilder sb = new StringBuilder();
		String delim = "";
		OmLanguage l = LabelDao.getLanguage(languageId);
		String first = l.isRtl() ? getLastname() : getFirstname();
		String last = l.isRtl() ? getFirstname() : getLastname();
		if (!Strings.isEmpty(first)) {
			sb.append(first);
			delim = " ";
		}
		if (!Strings.isEmpty(last)) {
			sb.append(delim).append(last);
		}
		if (Strings.isEmpty(sb) && address != null && !Strings.isEmpty(address.getEmail())) {
			sb.append(delim).append(address.getEmail());
		}
		if (Strings.isEmpty(sb)) {
			sb.append(DISPLAY_NAME_NA);
		}
		return escapeMarkup(sb).toString();
	}
}
