/*
 * Copyright 2017 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed 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.jbpm.services.task.identity;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

import javax.naming.InitialContext;
import javax.sql.DataSource;

import org.kie.internal.task.api.UserGroupCallback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Data base server user group callback implementation that utilizes SQL queries 
 * to get information about user, groups and relationship of these two.
 * <br/>
 * There are four configuration parameters required by this callback:
 * <ul>
 * 	<li>db.ds.jndi.name - JNDI name of the data source to be used for connections</li>
 * 	<li>db.user.query - query used to verify existence of the user (case sensitive, expects a single parameter on position 1)</li>
 * 	<li>db.roles.query - query user to check group existence (case sensitive, expects single parameter on position 1)</li>
 * 	<li>db.user.roles.query - query used to collect group for given user (case sensitive, expects single parameter on position 1, 
 * 	retrieves group name from position 1 of returned result set)</li>
 * </ul>
 *
 */
public class DBUserGroupCallbackImpl extends AbstractUserGroupInfo implements UserGroupCallback {

	private static final Logger logger = LoggerFactory.getLogger(DBUserGroupCallbackImpl.class);
    
    protected static final String DEFAULT_PROPERTIES_NAME = "classpath:/jbpm.usergroup.callback.properties";
    
    public static final String DS_JNDI_NAME = "db.ds.jndi.name";
    public static final String PRINCIPAL_QUERY = "db.user.query";
    public static final String USER_ROLES_QUERY = "db.user.roles.query";
    public static final String ROLES_QUERY = "db.roles.query";
	
    private Properties config;
    private DataSource ds; 
    
    //no no-arg constructor to prevent cdi from auto deploy
    public DBUserGroupCallbackImpl(boolean activate) {
        String propertiesLocation = System.getProperty("jbpm.usergroup.callback.properties");        
        config = readProperties(propertiesLocation, DEFAULT_PROPERTIES_NAME);
        init();

    }
    
    public DBUserGroupCallbackImpl(Properties config) {
        this.config = config;        
        init();

    }

	public boolean existsUser(String userId) {
		if (userId == null) {
			throw new IllegalArgumentException("UserId cannot be null");
		}
		return checkExistence(this.config.getProperty(PRINCIPAL_QUERY), userId);
	}

	public boolean existsGroup(String groupId) {
		if (groupId == null) {
			throw new IllegalArgumentException("GroupId cannot be null");
		}
		return checkExistence(this.config.getProperty(ROLES_QUERY), groupId);
	}

	public List<String> getGroupsForUser(String userId) {
		
		if (userId == null) {
			throw new IllegalArgumentException("UserId cannot be null");
		}
		
		List<String> roles = new ArrayList<String>();
		Connection conn = null;
		PreparedStatement ps = null;
		ResultSet rs = null;

		try {
			conn = ds.getConnection();

			ps = conn.prepareStatement(this.config.getProperty(USER_ROLES_QUERY));
			try {
				ps.setString(1, userId);
			} catch (ArrayIndexOutOfBoundsException ignore) {

			}
			rs = ps.executeQuery();
			while (rs.next()) {
				roles.add(rs.getString(1));
			}
		} catch (Exception e) {
			logger.error("Error when checking roles in db, parameter: " + userId, e);
		} finally {
			if (rs != null) {
				try {
					rs.close();
				} catch (SQLException e) {
				}
			}
			if (ps != null) {
				try {
					ps.close();
				} catch (SQLException e) {
				}
			}
			if (conn != null) {
				try {
					conn.close();
				} catch (Exception ex) {
				}
			}
		}
		
		return roles;
	}
	
	protected Connection getConnection() throws SQLException {

		return ds.getConnection();
	}
	
	private void init() {
		if (this.config == null || !this.config.containsKey(DS_JNDI_NAME) || 
				!this.config.containsKey(PRINCIPAL_QUERY) || !this.config.containsKey(ROLES_QUERY) 
				|| !this.config.containsKey(USER_ROLES_QUERY)) {
			throw new IllegalArgumentException("All properties must be given ("+ DS_JNDI_NAME + ","
					+ PRINCIPAL_QUERY +"," + ROLES_QUERY +"," +USER_ROLES_QUERY +")");
		}
		String jndiName = this.config.getProperty(DS_JNDI_NAME, "java:/DefaultDS");
		try {
			InitialContext ctx = new InitialContext();
			
			ds = (DataSource) ctx.lookup(jndiName);

		} catch (Exception e) {
			throw new IllegalStateException("Can get data source for DB usergroup callback, JNDI name: " + jndiName, e);
		}
	}
	
	protected boolean checkExistence(String querySql, String parameter) {
		Connection conn = null;
		PreparedStatement ps = null;
		ResultSet rs = null;
		boolean result = false;
		try {
			conn = ds.getConnection();

			ps = conn.prepareStatement(querySql);
			
			ps.setString(1, parameter);

			rs = ps.executeQuery();
			if (rs.next()) {
				result = true;
			}
		} catch (Exception e) {
			logger.error("Error when checking user/group in db, parameter: " + parameter, e);
		} finally {
			if (rs != null) {
				try {
					rs.close();
				} catch (SQLException e) {
				}
			}
			if (ps != null) {
				try {
					ps.close();
				} catch (SQLException e) {
				}
			}
			if (conn != null) {
				try {
					conn.close();
				} catch (Exception ex) {
				}
			}
		}
		
		
		return result;
	}

}
