/*
 * 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.patch;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.apache.ranger.biz.RangerBizUtil;
import org.apache.ranger.biz.ServiceDBStore;
import org.apache.ranger.common.AppConstants;
import org.apache.ranger.common.JSONUtil;
import org.apache.ranger.common.RangerCommonEnums;
import org.apache.ranger.common.SearchCriteria;
import org.apache.ranger.common.ServiceUtil;
import org.apache.ranger.common.StringUtil;
import org.apache.ranger.db.RangerDaoManager;
import org.apache.ranger.entity.XXAsset;
import org.apache.ranger.entity.XXAuditMap;
import org.apache.ranger.entity.XXGroup;
import org.apache.ranger.entity.XXPolicy;
import org.apache.ranger.entity.XXPolicyConditionDef;
import org.apache.ranger.entity.XXPortalUser;
import org.apache.ranger.entity.XXResource;
import org.apache.ranger.entity.XXServiceConfigDef;
import org.apache.ranger.entity.XXServiceDef;
import org.apache.ranger.entity.XXUser;
import org.apache.ranger.plugin.model.RangerPolicy;
import org.apache.ranger.plugin.model.RangerService;
import org.apache.ranger.plugin.model.RangerPolicy.RangerPolicyItem;
import org.apache.ranger.plugin.model.RangerPolicy.RangerPolicyItemAccess;
import org.apache.ranger.plugin.model.RangerPolicy.RangerPolicyResource;
import org.apache.ranger.plugin.store.EmbeddedServiceDefsUtil;
import org.apache.ranger.service.RangerPolicyService;
import org.apache.ranger.service.XPermMapService;
import org.apache.ranger.service.XPolicyService;
import org.apache.ranger.util.CLIUtil;
import org.apache.ranger.view.VXPermMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class PatchMigration_J10002 extends BaseLoader {
	private static final Logger logger = Logger.getLogger(PatchMigration_J10002.class);

	@Autowired
	RangerDaoManager daoMgr;

	@Autowired
	ServiceDBStore svcDBStore;

	@Autowired
	JSONUtil jsonUtil;

	@Autowired
	RangerPolicyService policyService;

	@Autowired
	StringUtil stringUtil;

	@Autowired
	XPolicyService xPolService;

	@Autowired
	XPermMapService xPermMapService;
	
	@Autowired
	RangerBizUtil bizUtil;

	private static int policyCounter = 0;
	private static int serviceCounter = 0;

	static Set<String> unsupportedLegacyPermTypes = new HashSet<String>();

	static {
		unsupportedLegacyPermTypes.add("Unknown");
		unsupportedLegacyPermTypes.add("Reset");
		unsupportedLegacyPermTypes.add("Obfuscate");
		unsupportedLegacyPermTypes.add("Mask");
	}

	public static void main(String[] args) {
		logger.info("main()");
		try {
			PatchMigration_J10002 loader = (PatchMigration_J10002) CLIUtil.getBean(PatchMigration_J10002.class);
			loader.init();
			while (loader.isMoreToProcess()) {
				loader.load();
			}
			logger.info("Load complete. Exiting!!!");
			System.exit(0);
		} catch (Exception e) {
			logger.error("Error loading", e);
			System.exit(1);
		}
	}
	
	@Override
	public void init() throws Exception {
		// Do Nothing
	}

	@Override
	public void execLoad() {
		logger.info("==> MigrationPatch.execLoad()");
		try {
			migrateServicesToNewSchema();
			migratePoliciesToNewSchema();
			updateSequences();
		} catch (Exception e) {
			logger.error("Error whille migrating data.", e);
		}
		logger.info("<== MigrationPatch.execLoad()");
	}

	@Override
	public void printStats() {
		logger.info("Total Number of migrated repositories/services: " + serviceCounter);
		logger.info("Total Number of migrated resources/policies: " + policyCounter);
	}

	public void migrateServicesToNewSchema() throws Exception {
		logger.info("==> MigrationPatch.migrateServicesToNewSchema()");

		try {
			List<XXAsset> repoList = daoMgr.getXXAsset().getAll();

			if (repoList.isEmpty()) {
				return;
			}
			if (!repoList.isEmpty()) {
				EmbeddedServiceDefsUtil.instance().init(svcDBStore);
			}

			svcDBStore.setPopulateExistingBaseFields(true);
			for (XXAsset xAsset : repoList) {

				if (xAsset.getActiveStatus() == AppConstants.STATUS_DELETED) {
					continue;
				}

				RangerService existing = svcDBStore.getServiceByName(xAsset.getName());
				if (existing != null) {
					logger.info("Repository/Service already exists. Ignoring migration of repo: " + xAsset.getName());
					continue;
				}

				RangerService service = new RangerService();
				service = mapXAssetToService(service, xAsset);

				service = svcDBStore.createService(service);

				serviceCounter++;
				logger.info("New Service created. ServiceName: " + service.getName());
			}
			svcDBStore.setPopulateExistingBaseFields(false);
		} catch (Exception e) {
			throw new Exception("Error while migrating data to new Plugin Schema.", e);
		}
		logger.info("<== MigrationPatch.migrateServicesToNewSchema()");
	}

	public void migratePoliciesToNewSchema() throws Exception {
		logger.info("==> MigrationPatch.migratePoliciesToNewSchema()");

		try {
			List<XXResource> resList = daoMgr.getXXResource().getAll();
			if (resList.isEmpty()) {
				return;
			}

			svcDBStore.setPopulateExistingBaseFields(true);
			for (XXResource xRes : resList) {

				if (xRes.getResourceStatus() == AppConstants.STATUS_DELETED) {
					continue;
				}

				XXAsset xAsset = daoMgr.getXXAsset().getById(xRes.getAssetId());
				if (xAsset == null) {
					logger.error("No Repository found for policyName: " + xRes.getPolicyName());
					continue;
				}

				RangerService service = svcDBStore.getServiceByName(xAsset.getName());
				
				if (service == null) {
					logger.error("No Service found for policy. Ignoring migration of such policy, policyName: "
							+ xRes.getPolicyName());
					continue;
				}

				XXPolicy existing = daoMgr.getXXPolicy().findByNameAndServiceId(xRes.getPolicyName(), service.getId());
				if (existing != null) {
					logger.info("Policy already exists. Ignoring migration of policy: " + existing.getName());
					continue;
				}

				RangerPolicy policy = new RangerPolicy();
				policy = mapXResourceToPolicy(policy, xRes, service);

				if(policy != null) {
					policy = svcDBStore.createPolicy(policy);

					policyCounter++;
					logger.info("New policy created. policyName: " + policy.getName());
				}
			}
			svcDBStore.setPopulateExistingBaseFields(false);
		} catch (Exception e) {
			throw new Exception("Error while migrating data to new Plugin Schema.", e);
		}
		logger.info("<== MigrationPatch.migratePoliciesToNewSchema()");
	}

	private RangerService mapXAssetToService(RangerService service, XXAsset xAsset) throws Exception {

		String type = "";
		String name = xAsset.getName();
		String description = xAsset.getDescription();
		Map<String, String> configs = null;

		int typeInt = xAsset.getAssetType();
		XXServiceDef serviceDef = daoMgr.getXXServiceDef().findByName(AppConstants.getLabelFor_AssetType(typeInt).toLowerCase());

		if (serviceDef == null) {
			throw new Exception("No ServiceDefinition found for repository: " + name);
		}
		type = serviceDef.getName();
		configs = jsonUtil.jsonToMap(xAsset.getConfig());

		List<XXServiceConfigDef> mandatoryConfigs = daoMgr.getXXServiceConfigDef().findByServiceDefName(type);
		for (XXServiceConfigDef serviceConf : mandatoryConfigs) {
			if (serviceConf.getIsMandatory()) {
				if (!stringUtil.isEmpty(configs.get(serviceConf.getName()))) {
					continue;
				}
				String dataType = serviceConf.getType();
				String defaultValue = serviceConf.getDefaultvalue();

				if (stringUtil.isEmpty(defaultValue)) {
					defaultValue = getDefaultValueForDataType(dataType);
				}
				configs.put(serviceConf.getName(), defaultValue);
			}
		}

		service.setType(type);
		service.setName(name);
		service.setDescription(description);
		service.setConfigs(configs);

		service.setCreateTime(xAsset.getCreateTime());
		service.setUpdateTime(xAsset.getUpdateTime());

		XXPortalUser createdByUser = daoMgr.getXXPortalUser().getById(xAsset.getAddedByUserId());
		XXPortalUser updByUser = daoMgr.getXXPortalUser().getById(xAsset.getUpdatedByUserId());

		if (createdByUser != null) {
			service.setCreatedBy(createdByUser.getLoginId());
		}
		if (updByUser != null) {
			service.setUpdatedBy(updByUser.getLoginId());
		}
		service.setId(xAsset.getId());

		return service;
	}

	private String getDefaultValueForDataType(String dataType) {

		String defaultValue = "";
		switch (dataType) {
		case "int":
			defaultValue = "0";
			break;
		case "string":
			defaultValue = "unknown";
			break;
		case "bool":
			defaultValue = "false";
			break;
		case "enum":
			defaultValue = "0";
			break;
		case "password":
			defaultValue = "password";
			break;
		default:
			break;
		}
		return defaultValue;
	}

	private RangerPolicy mapXResourceToPolicy(RangerPolicy policy, XXResource xRes, RangerService service) {
		String serviceName = service.getName();
		String serviceType = service.getType();
		String name = xRes.getPolicyName();
		String description = xRes.getDescription();
		Boolean isAuditEnabled = true;
		Boolean isEnabled = true;
		Map<String, RangerPolicyResource> resources = new HashMap<String, RangerPolicyResource>();
		List<RangerPolicyItem> policyItems = new ArrayList<RangerPolicyItem>();

		XXServiceDef svcDef = daoMgr.getXXServiceDef().findByName(serviceType);
		
		if(svcDef == null) {
			logger.error(serviceType + ": service-def not found. Skipping policy '" + name + "'");

			return null;
		}

		List<XXAuditMap> auditMapList = daoMgr.getXXAuditMap().findByResourceId(xRes.getId());
		if (stringUtil.isEmpty(auditMapList)) {
			isAuditEnabled = false;
		}
		if (xRes.getResourceStatus() == AppConstants.STATUS_DISABLED) {
			isEnabled = false;
		}

		Boolean isPathRecursive  = xRes.getIsRecursive() == RangerCommonEnums.BOOL_TRUE;
		Boolean isTableExcludes  = xRes.getTableType() == RangerCommonEnums.POLICY_EXCLUSION;
		Boolean isColumnExcludes = xRes.getColumnType() == RangerCommonEnums.POLICY_EXCLUSION;

		if (StringUtils.equalsIgnoreCase(serviceType, "hdfs")) {
			toRangerResourceList(xRes.getName(), "path", Boolean.FALSE, isPathRecursive, resources);
		} else if (StringUtils.equalsIgnoreCase(serviceType, "hbase")) {
			toRangerResourceList(xRes.getTables(), "table", isTableExcludes, Boolean.FALSE, resources);
			toRangerResourceList(xRes.getColumnFamilies(), "column-family", Boolean.FALSE, Boolean.FALSE, resources);
			toRangerResourceList(xRes.getColumns(), "column", isColumnExcludes, Boolean.FALSE, resources);
		} else if (StringUtils.equalsIgnoreCase(serviceType, "hive")) {
			toRangerResourceList(xRes.getDatabases(), "database", Boolean.FALSE, Boolean.FALSE, resources);
			toRangerResourceList(xRes.getTables(), "table", isTableExcludes, Boolean.FALSE, resources);
			toRangerResourceList(xRes.getColumns(), "column", isColumnExcludes, Boolean.FALSE, resources);
			toRangerResourceList(xRes.getUdfs(), "udf", Boolean.FALSE, Boolean.FALSE, resources);
		} else if (StringUtils.equalsIgnoreCase(serviceType, "knox")) {
			toRangerResourceList(xRes.getTopologies(), "topology", Boolean.FALSE, Boolean.FALSE, resources);
			toRangerResourceList(xRes.getServices(), "service", Boolean.FALSE, Boolean.FALSE, resources);
		} else if (StringUtils.equalsIgnoreCase(serviceType, "storm")) {
			toRangerResourceList(xRes.getTopologies(), "topology", Boolean.FALSE, Boolean.FALSE, resources);
		}

		policyItems = getPolicyItemListForRes(xRes, svcDef);

		policy.setService(serviceName);
		policy.setName(name);
		policy.setDescription(description);
		policy.setIsAuditEnabled(isAuditEnabled);
		policy.setIsEnabled(isEnabled);
		policy.setResources(resources);
		policy.setPolicyItems(policyItems);

		policy.setCreateTime(xRes.getCreateTime());
		policy.setUpdateTime(xRes.getUpdateTime());

		XXPortalUser createdByUser = daoMgr.getXXPortalUser().getById(xRes.getAddedByUserId());
		XXPortalUser updByUser = daoMgr.getXXPortalUser().getById(xRes.getUpdatedByUserId());

		if (createdByUser != null) {
			policy.setCreatedBy(createdByUser.getLoginId());
		}
		if (updByUser != null) {
			policy.setUpdatedBy(updByUser.getLoginId());
		}

		policy.setId(xRes.getId());

		return policy;
	}

	private Map<String, RangerPolicy.RangerPolicyResource> toRangerResourceList(String resourceString, String resourceType, Boolean isExcludes, Boolean isRecursive, Map<String, RangerPolicy.RangerPolicyResource> resources) {
		Map<String, RangerPolicy.RangerPolicyResource> ret = resources == null ? new HashMap<String, RangerPolicy.RangerPolicyResource>() : resources;

		if(StringUtils.isNotBlank(resourceString)) {
			RangerPolicy.RangerPolicyResource resource = ret.get(resourceType);

			if(resource == null) {
				resource = new RangerPolicy.RangerPolicyResource();
				resource.setIsExcludes(isExcludes);
				resource.setIsRecursive(isRecursive);

				ret.put(resourceType, resource);
			}

			Collections.addAll(resource.getValues(), resourceString.split(","));
		}

		return ret;
	}

	private List<RangerPolicyItem> getPolicyItemListForRes(XXResource xRes, XXServiceDef svcDef) {
		List<RangerPolicyItem> policyItems = new ArrayList<RangerPolicyItem>();

		SearchCriteria sc = new SearchCriteria();

		sc.addParam("resourceId", xRes.getId());
		List<VXPermMap> permMapList = xPermMapService.searchXPermMaps(sc).getVXPermMaps();

		HashMap<String, List<VXPermMap>> sortedPermMap = new HashMap<String, List<VXPermMap>>();

		// re-group the list with permGroup as the key
		if (permMapList != null) {
			for(VXPermMap permMap : permMapList) {
				String          permGrp    = permMap.getPermGroup();
				List<VXPermMap> sortedList = sortedPermMap.get(permGrp);

				if(sortedList == null) {
					sortedList = new ArrayList<VXPermMap>();
					sortedPermMap.put(permGrp, sortedList);
				}

				sortedList.add(permMap);
			}
		}

		for (Entry<String, List<VXPermMap>> entry : sortedPermMap.entrySet()) {
			List<String>                 userList   = new ArrayList<String>();
			List<String>                 groupList  = new ArrayList<String>();
			List<RangerPolicyItemAccess> accessList = new ArrayList<RangerPolicyItemAccess>();
			String                       ipAddress  = null;

			RangerPolicy.RangerPolicyItem policyItem = new RangerPolicy.RangerPolicyItem();

			for(VXPermMap permMap : entry.getValue()) {
				if(permMap.getPermFor() == AppConstants.XA_PERM_FOR_USER) {
					String userName = getUserName(permMap);

					if (! userList.contains(userName)) {
						userList.add(userName);
					}
				} else if(permMap.getPermFor() == AppConstants.XA_PERM_FOR_GROUP) {
					String groupName = getGroupName(permMap);

					if (! groupList.contains(groupName)) {
						groupList.add(groupName);
					}
				}

				String accessType = ServiceUtil.toAccessType(permMap.getPermType());
				if(StringUtils.isBlank(accessType) || unsupportedLegacyPermTypes.contains(accessType)) {
					logger.info(accessType + ": is not a valid access-type, ignoring accesstype for policy: " + xRes.getPolicyName());
					continue;
				}

				if(StringUtils.equalsIgnoreCase(accessType, "Admin")) {
					policyItem.setDelegateAdmin(Boolean.TRUE);
					if ( svcDef.getId() == EmbeddedServiceDefsUtil.instance().getHBaseServiceDefId()) {
						addAccessType(accessType, accessList);
					}
				} else {
					addAccessType(accessType, accessList);
				}

				ipAddress = permMap.getIpAddress();
			}

			if(CollectionUtils.isEmpty(accessList)) {
				logger.info("no access specified. ignoring policyItem for policy: " + xRes.getPolicyName());
				continue;
			}

			if(CollectionUtils.isEmpty(userList) && CollectionUtils.isEmpty(groupList)) {
				logger.info("no user or group specified. ignoring policyItem for policy: " + xRes.getPolicyName());
				continue;
			}

			policyItem.setUsers(userList);
			policyItem.setGroups(groupList);
			policyItem.setAccesses(accessList);

			if(ipAddress != null && !ipAddress.isEmpty()) {
				XXPolicyConditionDef policyCond = daoMgr.getXXPolicyConditionDef().findByServiceDefIdAndName(svcDef.getId(), "ip-range");

				if(policyCond != null) {
					RangerPolicy.RangerPolicyItemCondition ipCondition = new RangerPolicy.RangerPolicyItemCondition("ip-range", Collections.singletonList(ipAddress));

					policyItem.getConditions().add(ipCondition);
				}
			}

			policyItems.add(policyItem);
		}

		return policyItems;
	}

	private void addAccessType(String accessType, List<RangerPolicyItemAccess> accessList) {
		boolean alreadyExists = false;

		for(RangerPolicyItemAccess access : accessList) {
			if(StringUtils.equalsIgnoreCase(accessType, access.getType())) {
				alreadyExists = true;

				break;
			}
		}

		if(!alreadyExists) {
			accessList.add(new RangerPolicyItemAccess(accessType));
		}
	}

	private void updateSequences() {
		daoMgr.getXXServiceDef().updateSequence();
		daoMgr.getXXService().updateSequence();
		daoMgr.getXXPolicy().updateSequence();
	}

	private String getUserName(VXPermMap permMap) {
		String userName = permMap.getUserName();

		if(userName == null || userName.isEmpty()) {
			Long userId = permMap.getUserId();

			if(userId != null) {
				XXUser xxUser = daoMgr.getXXUser().getById(userId);

				if(xxUser != null) {
					userName = xxUser.getName();
				}
			}
		}

		return userName;
	}

	private String getGroupName(VXPermMap permMap) {
		String groupName = permMap.getGroupName();

		if(groupName == null || groupName.isEmpty()) {
			Long groupId = permMap.getGroupId();

			if(groupId != null) {
				XXGroup xxGroup = daoMgr.getXXGroup().getById(groupId);

				if(xxGroup != null) {
					groupName = xxGroup.getName();
				}
			}
		}

		return groupName;
	}
}
