/*
 * Copyright
 *   2007 axYus - www.axyus.com
 *   2005 C.Marchand - christophe.marchand@axyus.com
 *
 * This file is part of XEMELIOS.
 *
 * XEMELIOS is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * XEMELIOS is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with XEMELIOS; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package fr.gouv.finances.cp.xemelios.auth.dgcp;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Hashtable;
import java.util.Properties;

import javax.naming.AuthenticationException;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;

import org.apache.log4j.Logger;

import fr.gouv.finances.cp.xemelios.Splash;
import fr.gouv.finances.cp.xemelios.auth.AbstractAuthenticationManager;
import fr.gouv.finances.cp.xemelios.auth.AuthChecker;
import fr.gouv.finances.cp.xemelios.auth.AuthProvider;
import fr.gouv.finances.cp.xemelios.auth.AuthenticationCancelationException;
import fr.gouv.finances.cp.xemelios.auth.AuthenticationConfigurationException;
import fr.gouv.finances.cp.xemelios.auth.AuthenticationFactory;
import fr.gouv.finances.cp.xemelios.auth.XemeliosUser;
import fr.gouv.finances.cp.xemelios.common.Crypter;
import fr.gouv.finances.cp.xemelios.data.DataAccessException;
import fr.gouv.finances.cp.xemelios.data.DataConfigurationException;
import fr.gouv.finances.cp.xemelios.data.DataImpl;
import fr.gouv.finances.cp.xemelios.data.DataLayerManager;
import fr.gouv.finances.cp.xemelios.data.impl.MySqlDataLayer;
import fr.gouv.finances.cp.xemelios.ui.MainWindow;

public class Manager extends AbstractAuthenticationManager implements AuthChecker {
	private static final transient String LDAP_SERVICE_PROVIDER = "auth.ldap.service.provider";
	private static final transient String LDAP_SERVER_URL = "auth.ldap.server.url";
	public static final transient String LDAP_BASE_DN = "auth.ldap.userDn";

	private static Logger logger = Logger.getLogger(Manager.class);
	private String serviceProvider,ldapUrl,baseDn;
	private AuthProvider provider = null;
	

	public Manager(MainWindow mw) throws AuthenticationConfigurationException, AuthenticationCancelationException {
		super();
		setMainWindow(mw);
		try {
			if(! (DataLayerManager.getImplementation() instanceof MySqlDataLayer))
				throw new AuthenticationConfigurationException(getAuthenticationManagerName()+" can run only with mysql data layer");
			Connection con = null;
			MySqlDataLayer dataLayer = null;
			PreparedStatement ps = null;
			try {
				dataLayer = (MySqlDataLayer)DataLayerManager.getImplementation();
				con = dataLayer.getConnection();
				String sql = "CREATE TABLE IF NOT EXISTS DGCP_USERS (LOGIN VARCHAR(24) NOT NULL, PASSWORD VARCHAR(128), ACCESS INT, CONSTRAINT UN_LOGIN UNIQUE (LOGIN))";
				con.createStatement().executeUpdate(sql);
				ResultSet rs = con.createStatement().executeQuery("SELECT COUNT(*) FROM DGCP_USERS");
				if(rs.next()) {
					int size = rs.getInt(1);
					if(size==0) {
						// on regarde si il existe une table BASIC_USERS, dans ce cas, on recopie le mot de passe de l'administrateur 
						sql = "SELECT PASSWORD FROM BASIC_USERS WHERE LOGIN='admin'";
						boolean exists = false;
						try {
							ps = con.prepareStatement(sql);
							rs = ps.executeQuery();
							if(rs.next()) {
								String password = rs.getString(1);
								sql = "INSERT INTO DGCP_USERS (LOGIN, PASSWORD, ACCESS) VALUES (?,?,?)";
								rs.close();
								ps.close();
								ps = con.prepareStatement(sql);
								ps.setString(1,"admin");
								ps.setString(2,password);
								ps.setInt(3,0xFFFF);
								ps.executeUpdate();
								ps.close();
								exists = true;
								if(getMainWindow().getStartListener() instanceof Splash) {
									Splash splash = (Splash)getMainWindow().getStartListener();
									splash.setAlwaysOnTop(false);
									splash.setVisible(false);
								}
								String msg = 
									"La configuration \"Réseau du Trésor\" a bien été activée.\n"+
									"L'utilisateur d'administration a été créé avec succès.\n"+
									"Son login est 'admin' et son mot de passe est celui\n"+
									"de l'ancien administrateur.";
								JOptionPane.showMessageDialog(getMainWindow(),msg);
								if(getMainWindow().getStartListener() instanceof Splash) {
									Splash splash = (Splash)getMainWindow().getStartListener();
									splash.setVisible(true);
									splash.setAlwaysOnTop(true);
								}
							}
						} catch(SQLException sqlEx) {
							// on ne fait rien, la table n'existe pas, c'est tout
						}
						if(!exists) {
							if(getMainWindow().getStartListener() instanceof Splash) {
								Splash splash = (Splash)getMainWindow().getStartListener();
								splash.setAlwaysOnTop(false);
								splash.setVisible(false);
							}
							NewAdmin na = new NewAdmin(getMainWindow());
							boolean bRet = na.run();
							if(getMainWindow().getStartListener() instanceof Splash) {
								Splash splash = (Splash)getMainWindow().getStartListener();
								splash.setVisible(true);
								splash.setAlwaysOnTop(true);
							}
							if(bRet) {
								String password = na.getNewPassword();
								String login = na.getNewLogin();
								sql = "INSERT INTO DGCP_USERS(LOGIN,PASSWORD,ACCESS) VALUES (?,?,?)";
								ps = con.prepareStatement(sql);
								ps.setString(1,login);
								ps.setString(2,Crypter.cryptPassword(password));
								ps.setInt(3,0xFFFF);	// all rights
								ps.executeUpdate();
							} else {
								throw new AuthenticationCancelationException("Action interrompue");
							}
						}
					}
				}
			} catch(SQLException sqlEx) {
				throw new AuthenticationConfigurationException(sqlEx);
			} finally {
				if(ps!=null) try { ps.close(); } catch(Throwable t) {}
				if(con!=null) dataLayer.releaseConnection(con);
			}
		} catch(DataConfigurationException dcEx) {
			// ignore it, it has already been thrown before
		}
	}

	public boolean authenticate(XemeliosUser user) throws DataAccessException, AuthenticationConfigurationException {
		boolean ret = false;
		try {
//			if(authenticateLDAP(user)) {
//				authenticateSQL(user);
//			} else {
//				if(isAdmin(user)) {
//					authenticateSQL(user);
//				}
//			}
			ret = authenticateLDAP(user);
			logger.debug("LDAP->"+(ret?"OK":"failed"));
			if(ret) {
				authenticateSQL(user);
			} else {
				if(isAdmin(user)) {
					logger.debug(user.getId()+" is admin");
					ret = authenticateSQLadmin(user);
				}
			}
		} catch(DataConfigurationException dcEx) {
			throw new AuthenticationConfigurationException(dcEx);
		}
		if(user.isAllowedTo(null,XemeliosUser.ROLE_CONNECT) && user.isAllowedTo(null,DgcpUser.ROLE_ADMIN)) insertAdminMenu();
		return ret && user.isAllowedTo(null,XemeliosUser.ROLE_CONNECT);
	}

	public AuthChecker getChecker() {
		return this;
	}

	public AuthProvider getProvider() {
		if(provider == null) provider = new DgcpProvider(getMainWindow());
		return provider;
	}

	public String getAuthenticationManagerName() {
		return "Authentification réseau DGCP";
	}

	protected boolean authenticateLDAP(XemeliosUser user) throws DataConfigurationException, AuthenticationConfigurationException {
		DgcpUser dgcpUser = null;
		if(! (user instanceof DgcpUser)) throw new DataConfigurationException("can only authenticate DcgpUser");
		else dgcpUser = (DgcpUser)user;
		DirContext dctx = null;
		Hashtable<String,String> env = new Hashtable<String,String>();

		if (serviceProvider == null) {
			throw new NullPointerException("you must configure service provider in "+System.getProperty(AuthenticationFactory.PROP_CONFIG_FILE_NAME)+" - "+LDAP_SERVICE_PROVIDER);
		}

		if (ldapUrl == null) {
			throw new NullPointerException("You must configure ldap server url in "+System.getProperty(AuthenticationFactory.PROP_CONFIG_FILE_NAME)+" - "+LDAP_SERVER_URL);
		}

		env.put(Context.INITIAL_CONTEXT_FACTORY, serviceProvider);
		env.put(Context.PROVIDER_URL, ldapUrl);

		env.put(Context.SECURITY_AUTHENTICATION, "simple");
		String userDn = baseDn.replaceAll("%LOGIN%",user.getId());
		env.put(Context.SECURITY_PRINCIPAL, userDn);
//		if (dgcpUser.getPassword()!=null)
		env.put(Context.SECURITY_CREDENTIALS, dgcpUser.getPassword());
		try {
			dctx = new InitialDirContext(env);
			dctx.close();
		} catch(AuthenticationException aEx) {
			logger.debug("authentication failed");
			return false;
		} catch (NamingException naEx) {
			logger.warn(naEx.getMessage());
// bug 284
			return false;
//			throw new AuthenticationConfigurationException(naEx);
		}
		logger.debug("authenticated");
		dgcpUser.setAuthenticated(true);
		return true;
	}
	protected boolean authenticateSQL(XemeliosUser user) throws DataConfigurationException, DataAccessException {
		DataImpl impl = DataLayerManager.getImplementation();
		if(! (impl instanceof MySqlDataLayer)) throw new DataConfigurationException(getAuthenticationManagerName()+" can only deal with MySql data layer");
		if(! (user instanceof DgcpUser)) throw new DataConfigurationException(getAuthenticationManagerName()+" can only authenticate DgcpUser");
		DgcpUser dgcpUser = (DgcpUser)user;
		MySqlDataLayer layer = (MySqlDataLayer)impl;
		Connection con = null;
		PreparedStatement ps = null;
		ResultSet rs = null;
		try {
			con = layer.getConnection();
			String sql = "SELECT ACCESS FROM DGCP_USERS WHERE LOGIN=?";
			ps = con.prepareStatement(sql);
			ps.setString(1,dgcpUser.getId());
//			ps.setString(2,Crypter.cryptPassword(dgcpUser.getPassword()));
			rs = ps.executeQuery();
			if(rs.next()) {
				// this user has already been registered
				dgcpUser.setAuth(rs.getInt(1));
				dgcpUser.setAuthenticated(true);
				rs.close();
			} else {
				ps.close();
				// check if it does not exists
//				sql = "SELECT ACCESS FROM DGCP_USERS WHERE LOGIN=? AND PASSWORD IS NULL";
//				ps = con.prepareStatement(sql);
//				ps.setString(1,user.getId());
//				rs = ps.executeQuery();
//				if(rs.next()) {
//					// this user exists but password typed is wrong
//					dgcpUser.setAuth(rs.getInt(1));
//				} else {
					if(dgcpUser.isAuthenticated()) {
//						rs.close();
//						ps.close();
						sql = "INSERT INTO DGCP_USERS (LOGIN,ACCESS) VALUES (?,?)";
						ps = con.prepareStatement(sql);
						ps.setString(1,dgcpUser.getId());
						ps.setInt(2,XemeliosUser.ROLE_CONNECT | XemeliosUser.ROLE_SEARCH | XemeliosUser.ROLE_BROWSE | XemeliosUser.ROLE_EXPORT);
						ps.executeUpdate();
					}
//				}
			}
		} catch(SQLException sqlEx) {
			throw new DataAccessException(sqlEx);
		} finally {
			if(ps!=null) try { ps.close(); } catch(Throwable t) {}
			if(con!=null) layer.releaseConnection(con);
			// we must clear password
			dgcpUser.setPassword(null);
		}
		return dgcpUser.isAuthenticated();
	}
	protected boolean authenticateSQLadmin(XemeliosUser user) throws DataConfigurationException, DataAccessException {
		DataImpl impl = DataLayerManager.getImplementation();
		if(! (impl instanceof MySqlDataLayer)) throw new DataConfigurationException(getAuthenticationManagerName()+" can only deal with MySql data layer");
		if(! (user instanceof DgcpUser)) throw new DataConfigurationException(getAuthenticationManagerName()+" can only authenticate DgcpUser");
		DgcpUser dgcpUser = (DgcpUser)user;
		MySqlDataLayer layer = (MySqlDataLayer)impl;
		Connection con = null;
		PreparedStatement ps = null;
		ResultSet rs = null;
		try {
			con = layer.getConnection();
			String sql = "SELECT ACCESS FROM DGCP_USERS WHERE LOGIN=? AND PASSWORD=?";
			ps = con.prepareStatement(sql);
			ps.setString(1,dgcpUser.getId());
			ps.setString(2,Crypter.cryptPassword(dgcpUser.getPassword()));
			rs = ps.executeQuery();
			if(rs.next()) {
				// this user has already been registered
				dgcpUser.setAuth(rs.getInt(1));
				dgcpUser.setAuthenticated(true);
				rs.close();
			} else {
				ps.close();
				// check if it does not exists
				sql = "SELECT ACCESS FROM DGCP_USERS WHERE LOGIN=? AND PASSWORD IS NULL";
				ps = con.prepareStatement(sql);
				ps.setString(1,user.getId());
				rs = ps.executeQuery();
				if(rs.next()) {
					// this user exists but password typed is wrong
					dgcpUser.setAuth(rs.getInt(1));
				} else {
					if(dgcpUser.isAuthenticated()) {
						rs.close();
						ps.close();
						sql = "INSERT INTO DGCP_USERS (LOGIN,ACCESS) VALUES (?,?)";
						ps = con.prepareStatement(sql);
						ps.setString(1,dgcpUser.getId());
						ps.setInt(2,XemeliosUser.ROLE_CONNECT | XemeliosUser.ROLE_SEARCH | XemeliosUser.ROLE_BROWSE | XemeliosUser.ROLE_EXPORT);
						ps.executeUpdate();
					}
				}
			}
		} catch(SQLException sqlEx) {
			throw new DataAccessException(sqlEx);
		} finally {
			if(ps!=null) try { ps.close(); } catch(Throwable t) {}
			if(con!=null) layer.releaseConnection(con);
			// we must clear password
			dgcpUser.setPassword(null);
		}
		return dgcpUser.isAuthenticated();
	}
	protected boolean isAdmin(XemeliosUser user) throws DataConfigurationException, DataAccessException {
		DataImpl impl = DataLayerManager.getImplementation();
		if(! (impl instanceof MySqlDataLayer)) throw new DataConfigurationException(getAuthenticationManagerName()+" can only deal with MySql data layer");
		MySqlDataLayer layer = (MySqlDataLayer)impl;
		Connection con = null;
		PreparedStatement ps = null;
		ResultSet rs = null;
		boolean bRet = false;
		try {
			con = layer.getConnection();
			String sql = "SELECT ACCESS FROM DGCP_USERS WHERE LOGIN=?";
			ps = con.prepareStatement(sql);
			ps.setString(1,user.getId());
			rs = ps.executeQuery();
			if(rs.next()) {
				bRet = (DgcpUser.ROLE_ADMIN & rs.getInt(1))>0;
				rs.close();
			}
		} catch(SQLException sqlEx) {
			throw new DataAccessException(sqlEx);
		} finally {
			if(ps!=null) try { ps.close(); } catch(Throwable t) {}
			if(con!=null) layer.releaseConnection(con);
		}
		return bRet;
	}
        public boolean modifyPassword(DgcpUser user) throws DataConfigurationException, DataAccessException {
		DataImpl impl = DataLayerManager.getImplementation();
		if(! (impl instanceof MySqlDataLayer)) throw new DataConfigurationException(getAuthenticationManagerName()+" can only deal with MySql data layer");
		MySqlDataLayer layer = (MySqlDataLayer)impl;
		Connection con = null;
		PreparedStatement ps = null;
		ResultSet rs = null;
                boolean bRet = false;
		try {
			con = layer.getConnection();
                        ps = con.prepareStatement("UPDATE DGCP_USERS SET PASSWORD=? WHERE LOGIN=?");
                        ps.setString(1,Crypter.cryptPassword(user.getPassword()));
                        ps.setString(2,user.getId());
                        bRet = (ps.executeUpdate()==1);
		} catch(SQLException sqlEx) {
			throw new DataAccessException(sqlEx);
		} finally {
			if(ps!=null) try { ps.close(); } catch(Throwable t) {}
			if(con!=null) layer.releaseConnection(con);
			// we must clear password
			user.setPassword(null);
		}
                return bRet;
        }
	@Override
	public void setProperties(Properties props) {
		super.setProperties(props);
		serviceProvider = props.getProperty(LDAP_SERVICE_PROVIDER);
		ldapUrl = props.getProperty(LDAP_SERVER_URL);
		baseDn = props.getProperty(LDAP_BASE_DN);
	}
	protected void insertAdminMenu() {
		logger.info("in insertAdminMenu");
		JMenu adminMenu = new JMenu("Administration");
		JMenuItem item = new JMenuItem("Gestion des utlisateurs...");
		item.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				new DlgAdminUsers(getMainWindow());
			}
		});
		adminMenu.add(item);
		JMenuBar mBar = getMainWindow().getJMenuBar();
		int mnuCount = mBar.getMenuCount();
		JMenu helpMenu = mBar.getMenu(mnuCount-1);
		mBar.add(adminMenu);
		mBar.add(helpMenu);
		getMainWindow().setJMenuBar(mBar);
	}
	protected void updateAuthUser(DgcpUser user) {
		try {
			DataImpl impl = DataLayerManager.getImplementation();
			if(! (impl instanceof MySqlDataLayer)) throw new DataConfigurationException(getAuthenticationManagerName()+" can only deal with MySql data layer");
			MySqlDataLayer layer = (MySqlDataLayer)impl;
			Connection con = null;
			PreparedStatement ps = null;
			try {
				con = layer.getConnection();
				String sql = "UPDATE DGCP_USERS SET ACCESS=? WHERE LOGIN=?";
				ps = con.prepareStatement(sql);
				ps.setInt(1,user.getAuth(null));
				ps.setString(2,user.getId());
				ps.executeUpdate();
			} catch(SQLException sqlEx) {
				sqlEx.printStackTrace();
			} finally {
				if(ps!=null) try { ps.close(); } catch(Throwable t) {}
				if(con!=null) layer.releaseConnection(con);
			}
		} catch(Throwable t) {
			t.printStackTrace();
		}
	}
	protected boolean create(DgcpUser user) {
		boolean bRet = false;
		try {
			DataImpl impl = DataLayerManager.getImplementation();
			if(! (impl instanceof MySqlDataLayer)) throw new DataConfigurationException(getAuthenticationManagerName()+" can only deal with MySql data layer");
			MySqlDataLayer layer = (MySqlDataLayer)impl;
			String sql = "INSERT INTO DGCP_USERS (LOGIN,ACCESS) VALUES (?,?)";
			Connection con = null;
			PreparedStatement ps = null;
			try {
				con = layer.getConnection();
				ps = con.prepareStatement(sql);
				ps.setString(1,user.getId());
				ps.setInt(2,user.getAuth(null));
				ps.executeUpdate();
				bRet = true;
			} catch(SQLException sqlEx) {
				sqlEx.printStackTrace();
			}
		} catch(Throwable t) {}
		return bRet;
	}
}
