/*
 * 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.ranger.plugin.policyengine;

import org.apache.commons.lang.StringUtils;
import org.apache.ranger.plugin.model.RangerPolicy;
import org.apache.ranger.plugin.model.RangerPolicy.RangerPolicyItemDataMaskInfo;
import org.apache.ranger.plugin.model.RangerPolicy.RangerPolicyItemRowFilterInfo;

import org.codehaus.jackson.annotate.JsonAutoDetect;
import org.codehaus.jackson.annotate.JsonIgnoreProperties;
import org.codehaus.jackson.map.annotate.JsonSerialize;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import static org.apache.ranger.plugin.policyevaluator.RangerPolicyEvaluator.ACCESS_ALLOWED;
import static org.apache.ranger.plugin.policyevaluator.RangerPolicyEvaluator.ACCESS_CONDITIONAL;
import static org.apache.ranger.plugin.policyevaluator.RangerPolicyEvaluator.ACCESS_DENIED;

public class RangerResourceACLs {
	final private Map<String, Map<String, AccessResult>> userACLs   = new HashMap<>();
	final private Map<String, Map<String, AccessResult>> groupACLs  = new HashMap<>();
	final private Map<String, Map<String, AccessResult>> roleACLs   = new HashMap<>();
	final private List<RowFilterResult>                  rowFilters = new ArrayList<>();
	final private List<DataMaskResult>                   dataMasks  = new ArrayList<>();

	public RangerResourceACLs() {
	}

	public Map<String, Map<String, AccessResult>> getUserACLs() {
		return userACLs;
	}

	public Map<String, Map<String, AccessResult>> getGroupACLs() {
		return groupACLs;
	}

	public Map<String, Map<String, AccessResult>> getRoleACLs() { return roleACLs; }

	public List<RowFilterResult> getRowFilters() { return rowFilters; }

	public List<DataMaskResult> getDataMasks() { return dataMasks; }

	public void finalizeAcls() {
		Map<String, AccessResult>  publicGroupAccessInfo = groupACLs.get(RangerPolicyEngine.GROUP_PUBLIC);
		if (publicGroupAccessInfo != null) {

			for (Map.Entry<String, AccessResult> entry : publicGroupAccessInfo.entrySet()) {
				String accessType = entry.getKey();
				AccessResult accessResult = entry.getValue();
				int access = accessResult.getResult();

				if (access == ACCESS_DENIED || access == ACCESS_ALLOWED) {
					for (Map.Entry<String, Map<String, AccessResult>> mapEntry : userACLs.entrySet()) {
						Map<String, AccessResult> mapValue = mapEntry.getValue();
						AccessResult savedAccessResult = mapValue.get(accessType);
						if (savedAccessResult != null && !savedAccessResult.getIsFinal()) {
							mapValue.remove(accessType);
						}
					}

					for (Map.Entry<String, Map<String, AccessResult>> mapEntry : groupACLs.entrySet()) {
						if (!StringUtils.equals(mapEntry.getKey(), RangerPolicyEngine.GROUP_PUBLIC)) {
							Map<String, AccessResult> mapValue = mapEntry.getValue();
							AccessResult savedAccessResult = mapValue.get(accessType);
							if (savedAccessResult != null && !savedAccessResult.getIsFinal()) {
								mapValue.remove(accessType);
							}
						}
					}
				}
			}
		}
		finalizeAcls(userACLs);
		finalizeAcls(groupACLs);
		finalizeAcls(roleACLs);
	}

	public void setUserAccessInfo(String userName, String accessType, Integer access, RangerPolicy policy) {
		Map<String, AccessResult> userAccessInfo = userACLs.get(userName);

		if (userAccessInfo == null) {
			userAccessInfo = new HashMap<>();

			userACLs.put(userName, userAccessInfo);
		}

		AccessResult accessResult = userAccessInfo.get(accessType);

		if (accessResult == null) {
			accessResult = new AccessResult(access, policy);

			userAccessInfo.put(accessType, accessResult);
		} else if (access != ACCESS_CONDITIONAL) {
			accessResult.setResult(access);
			accessResult.setPolicy(policy);
		}
	}

	public void setGroupAccessInfo(String groupName, String accessType, Integer access, RangerPolicy policy) {
		Map<String, AccessResult> groupAccessInfo = groupACLs.get(groupName);

		if (groupAccessInfo == null) {
			groupAccessInfo = new HashMap<>();

			groupACLs.put(groupName, groupAccessInfo);
		}

		AccessResult accessResult = groupAccessInfo.get(accessType);

		if (accessResult == null) {
			accessResult = new AccessResult(access, policy);

			groupAccessInfo.put(accessType, accessResult);
		} else if (access != ACCESS_CONDITIONAL) {
			accessResult.setResult(access);
			accessResult.setPolicy(policy);
		}
	}

	public void setRoleAccessInfo(String roleName, String accessType, Integer access, RangerPolicy policy) {
		Map<String, AccessResult> roleAccessInfo = roleACLs.get(roleName);

		if (roleAccessInfo == null) {
			roleAccessInfo = new HashMap<>();

			roleACLs.put(roleName, roleAccessInfo);
		}

		AccessResult accessResult = roleAccessInfo.get(accessType);

		if (accessResult == null) {
			accessResult = new AccessResult(access, policy);

			roleAccessInfo.put(accessType, accessResult);
		} else if (access != ACCESS_CONDITIONAL) {
			accessResult.setResult(access);
			accessResult.setPolicy(policy);
		}
	}

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();

		sb.append("{");

		sb.append("UserACLs={");
		for (Map.Entry<String, Map<String, AccessResult>> entry : userACLs.entrySet()) {
			sb.append("user=").append(entry.getKey()).append(":");
			sb.append("permissions={");
			for (Map.Entry<String, AccessResult> permission : entry.getValue().entrySet()) {
				sb.append("{Permission=").append(permission.getKey()).append(", value=").append(permission.getValue()).append("},");
				sb.append("{RangerPolicyID=").append(permission.getValue().getPolicy() == null ? null : permission.getValue().getPolicy().getId()).append("},");
			}
			sb.append("},");
		}
		sb.append("}");

		sb.append(", GroupACLs={");
		for (Map.Entry<String, Map<String, AccessResult>> entry : groupACLs.entrySet()) {
			sb.append("group=").append(entry.getKey()).append(":");
			sb.append("permissions={");
			for (Map.Entry<String, AccessResult> permission : entry.getValue().entrySet()) {
				sb.append("{Permission=").append(permission.getKey()).append(", value=").append(permission.getValue()).append("}, ");
				sb.append("{RangerPolicy ID=").append(permission.getValue().getPolicy() == null ? null : permission.getValue().getPolicy().getId()).append("},");
			}
			sb.append("},");
		}
		sb.append("}");

		sb.append(", RoleACLs={");
		for (Map.Entry<String, Map<String, AccessResult>> entry : roleACLs.entrySet()) {
			sb.append("role=").append(entry.getKey()).append(":");
			sb.append("permissions={");
			for (Map.Entry<String, AccessResult> permission : entry.getValue().entrySet()) {
				sb.append("{Permission=").append(permission.getKey()).append(", value=").append(permission.getValue()).append("}, ");
				sb.append("{RangerPolicy ID=").append(permission.getValue().getPolicy() == null ? null : permission.getValue().getPolicy().getId()).append("},");
			}
			sb.append("},");
		}
		sb.append("}");

		sb.append("}");

		sb.append(", rowFilters=[");
		for (RowFilterResult rowFilter : rowFilters) {
			rowFilter.toString(sb);
			sb.append(" ");
		}
		sb.append("]");

		sb.append(", dataMasks=[");
		for (DataMaskResult dataMask : dataMasks) {
			dataMask.toString(sb);
			sb.append(" ");
		}
		sb.append("]");

		return sb.toString();
	}

	private void finalizeAcls(Map<String, Map<String, AccessResult>> acls) {
		List<String> keysToRemove = new ArrayList<>();
		for (Map.Entry<String, Map<String, AccessResult>> entry : acls.entrySet()) {
			if (entry.getValue().isEmpty()) {
				keysToRemove.add(entry.getKey());
			} else {
				for (Map.Entry<String, AccessResult> permission : entry.getValue().entrySet()) {
					permission.getValue().setIsFinal(true);
				}
			}

		}
		for (String keyToRemove : keysToRemove) {
			acls.remove(keyToRemove);
		}
	}

	@JsonAutoDetect(fieldVisibility=JsonAutoDetect.Visibility.ANY)
	@JsonSerialize(include=JsonSerialize.Inclusion.NON_EMPTY)
	@JsonIgnoreProperties(ignoreUnknown=true)
	@XmlRootElement
	@XmlAccessorType(XmlAccessType.FIELD)
	public static class AccessResult {
		private int     result;
		private boolean isFinal;
		private RangerPolicy  policy;

		public AccessResult() {
			this(-1, null);
		}

		public AccessResult(int result, RangerPolicy policy) {
			this(result, false, policy);
		}

		public AccessResult(int result, boolean isFinal, RangerPolicy policy) {
			setIsFinal(isFinal);
			setResult(result);
			setPolicy(policy);
		}

		public int getResult() { return result; }

		public void setResult(int result) {
			if (!isFinal) {
				this.result = result;

				if (this.result == ACCESS_DENIED) {
					isFinal = true;
				}
			}
		}

		public boolean getIsFinal() { return isFinal; }

		public void setIsFinal(boolean isFinal) { this.isFinal = isFinal; }

		public RangerPolicy getPolicy() {
			return policy;
		}

		public void setPolicy(RangerPolicy policy){
			this.policy = policy;
		}

		@Override
		public boolean equals(Object other) {
			if (other == null)
				return false;
			if (other instanceof AccessResult) {
				AccessResult otherObject = (AccessResult)other;
				return result == otherObject.result && isFinal == otherObject.isFinal;
			} else
				return false;

		}
		@Override
		public String toString() {
			if (result == ACCESS_ALLOWED) {
				return "ALLOWED, final=" + isFinal;
			}
			if (result == ACCESS_DENIED) {
				return "NOT_ALLOWED, final=" + isFinal;
			}
			return "CONDITIONAL_ALLOWED, final=" + isFinal;
		}
	}

	@JsonAutoDetect(fieldVisibility=JsonAutoDetect.Visibility.ANY)
	@JsonSerialize(include=JsonSerialize.Inclusion.NON_EMPTY)
	@JsonIgnoreProperties(ignoreUnknown=true)
	@XmlRootElement
	@XmlAccessorType(XmlAccessType.FIELD)
	public static class DataMaskResult implements Serializable {
		private static final long serialVersionUID = 1L;

		private final Set<String>                  users;
		private final Set<String>                  groups;
		private final Set<String>                  roles;
		private final Set<String>                  accessTypes;
		private final RangerPolicyItemDataMaskInfo maskInfo;
		private       boolean                      isConditional = false;

		public DataMaskResult(Set<String> users, Set<String> groups, Set<String> roles, Set<String> accessTypes, RangerPolicyItemDataMaskInfo maskInfo) {
			this.users       = users;
			this.groups      = groups;
			this.roles       = roles;
			this.accessTypes = accessTypes;
			this.maskInfo    = maskInfo;
		}

		public DataMaskResult(DataMaskResult that) {
			this.users         = that.users;
			this.groups        = that.groups;
			this.roles         = that.roles;
			this.accessTypes   = that.accessTypes;
			this.maskInfo      = that.maskInfo;
			this.isConditional = that.isConditional;
		}

		public Set<String> getUsers() { return users; }

		public Set<String> getGroups() { return groups; }

		public Set<String> getRoles() { return roles; }

		public Set<String> getAccessTypes() { return accessTypes; }

		public RangerPolicyItemDataMaskInfo getMaskInfo() { return maskInfo; }

		public boolean getIsConditional() { return isConditional; }

		public void setIsConditional(boolean isConditional) { this.isConditional = isConditional; }

		@Override
		public int hashCode() { return Objects.hash(users, groups, roles, accessTypes, maskInfo, isConditional); }

		@Override
		public boolean equals(Object other) {
			if (this == other) {
				return true;
			} else if (other == null || getClass() != other.getClass()) {
				return false;
			} else {
				DataMaskResult that = (DataMaskResult) other;

				return Objects.equals(users, that.users) &&
					   Objects.equals(groups, that.groups) &&
					   Objects.equals(roles, that.roles) &&
					   Objects.equals(accessTypes, that.accessTypes) &&
					   Objects.equals(maskInfo, that.maskInfo) &&
					   isConditional == that.isConditional;
			}
		}

		@Override
		public String toString() {
			return toString(new StringBuilder()).toString();
		}

		public StringBuilder toString(StringBuilder sb) {
			sb.append("{");

			if (users != null && !users.isEmpty()) {
				sb.append("users:[");
				for (String user : users) {
					sb.append(user).append(' ');
				}
				sb.append("] ");
			}

			if (groups != null && !groups.isEmpty()) {
				sb.append("groups:[");
				for (String group : groups) {
					sb.append(group).append(' ');
				}
				sb.append("] ");
			}

			if (roles != null && !roles.isEmpty()) {
				sb.append("roles:[");
				for (String role : roles) {
					sb.append(role).append(' ');
				}
				sb.append("] ");
			}

			if (accessTypes != null && !accessTypes.isEmpty()) {
				sb.append("accessTypes:[");
				for (String accessType : accessTypes) {
					sb.append(accessType).append(' ');
				}
				sb.append("] ");
			}

			sb.append("maskInfo=");
			maskInfo.toString(sb);
			sb.append(" isConditional=").append(isConditional);

			sb.append("}");

			return sb;
		}
	}

	@JsonAutoDetect(fieldVisibility=JsonAutoDetect.Visibility.ANY)
	@JsonSerialize(include=JsonSerialize.Inclusion.NON_EMPTY)
	@JsonIgnoreProperties(ignoreUnknown=true)
	@XmlRootElement
	@XmlAccessorType(XmlAccessType.FIELD)
	public static class RowFilterResult implements Serializable {
		private static final long serialVersionUID = 1L;

		private final Set<String>                   users;
		private final Set<String>                   groups;
		private final Set<String>                   roles;
		private final Set<String>                   accessTypes;
		private final RangerPolicyItemRowFilterInfo filterInfo;
		private       boolean                       isConditional = false;

		public RowFilterResult(Set<String> users, Set<String> groups, Set<String> roles, Set<String> accessTypes, RangerPolicyItemRowFilterInfo filterInfo) {
			this.users       = users;
			this.groups      = groups;
			this.roles       = roles;
			this.accessTypes = accessTypes;
			this.filterInfo  = filterInfo;
		}

		public RowFilterResult(RowFilterResult that) {
			this.users         = that.users;
			this.groups        = that.groups;
			this.roles         = that.roles;
			this.accessTypes   = that.accessTypes;
			this.filterInfo    = that.filterInfo;
			this.isConditional = that.isConditional;
		}

		public Set<String> getUsers() { return users; }

		public Set<String> getGroups() { return groups; }

		public Set<String> getRoles() { return roles; }

		public Set<String> getAccessTypes() { return accessTypes; }

		public RangerPolicyItemRowFilterInfo getFilterInfo() { return filterInfo; }

		public boolean getIsConditional() { return isConditional; }

		public void setIsConditional(boolean isConditional) { this.isConditional = isConditional; }

		@Override
		public int hashCode() { return Objects.hash(users, groups, roles, accessTypes, filterInfo, isConditional); }

		@Override
		public boolean equals(Object other) {
			if (this == other) {
				return true;
			} else if (other == null || getClass() != other.getClass()) {
				return false;
			} else {
				RowFilterResult that = (RowFilterResult) other;

				return Objects.equals(users, that.users) &&
						Objects.equals(groups, that.groups) &&
						Objects.equals(roles, that.roles) &&
						Objects.equals(accessTypes, that.accessTypes) &&
						Objects.equals(filterInfo, that.filterInfo) &&
						isConditional == that.isConditional;
			}
		}

		@Override
		public String toString() {
			return toString(new StringBuilder()).toString();
		}

		public StringBuilder toString(StringBuilder sb) {
			sb.append("{");

			if (users != null && !users.isEmpty()) {
				sb.append("users:[");
				for (String user : users) {
					sb.append(user).append(' ');
				}
				sb.append("] ");
			}

			if (groups != null && !groups.isEmpty()) {
				sb.append("groups:[");
				for (String group : groups) {
					sb.append(group).append(' ');
				}
				sb.append("] ");
			}

			if (roles != null && !roles.isEmpty()) {
				sb.append("roles:[");
				for (String role : roles) {
					sb.append(role).append(' ');
				}
				sb.append("] ");
			}

			if (accessTypes != null && !accessTypes.isEmpty()) {
				sb.append("accessTypes:[");
				for (String accessType : accessTypes) {
					sb.append(accessType).append(' ');
				}
				sb.append("] ");
			}

			sb.append("filterInfo=");
			filterInfo.toString(sb);
			sb.append(" isConditional=").append(isConditional);

			sb.append("}");

			return sb;
		}
	}
}
