/*
 * eXist Open Source Native XML Database
 * Copyright (C) 2015 The eXist Project
 * http://exist-db.org
 *
 * This program 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 2
 * of the License, or (at your option) any later version.
 *  
 * This program 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 this program; if not, write to the Free Software Foundation
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
package org.exist.security.internal;

import org.exist.security.AbstractRealm;
import org.exist.security.AbstractAccount;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.exist.config.Configuration;
import org.exist.config.ConfigurationException;
import org.exist.config.Configurator;
import org.exist.config.annotation.ConfigurationClass;
import org.exist.config.annotation.ConfigurationFieldAsElement;
import org.exist.security.Group;
import org.exist.security.PermissionDeniedException;
import org.exist.security.SchemaType;
import org.exist.security.SecurityManager;
import org.exist.security.Account;
import org.exist.security.internal.aider.UserAider;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Optional;
import java.util.Properties;
import org.exist.security.Credential;

import org.exist.storage.DBBroker;

/**
 * Represents a user within the database.
 * 
 * @author Wolfgang Meier <wolfgang@exist-db.org>
 * @author {Marco.Tampucci, Massimo.Martinelli} @isti.cnr.it
 * @author Adam retter <adam@exist-db.org>
 */
@ConfigurationClass("account")
public class AccountImpl extends AbstractAccount {

    private final static Logger LOG = LogManager.getLogger(AccountImpl.class);
    public static boolean CHECK_PASSWORDS = true;

    private final static SecurityProperties securityProperties = new SecurityProperties();

    public static SecurityProperties getSecurityProperties() {
        return securityProperties;
    }

    /*
    static {
        Properties props = new Properties();
        try {
            props.load(AccountImpl.class.getClassLoader().getResourceAsStream(
                    "org/exist/security/security.properties"));
        } catch(IOException e) {
        }
        String option = props.getProperty("passwords.encoding", "md5");
        setPasswordEncoding(option);
        option = props.getProperty("passwords.check", "yes");
        CHECK_PASSWORDS = option.equalsIgnoreCase("yes")
                || option.equalsIgnoreCase("true");
    }

    static public void enablePasswordChecks(boolean check) {
        CHECK_PASSWORDS = check;
    }

    static public void setPasswordEncoding(String encoding) {
        if(encoding != null) {
            LOG.equals("Setting password encoding to " + encoding);
            if(encoding.equalsIgnoreCase("plain")) {
                PASSWORD_ENCODING = PLAIN_ENCODING;
            } else if(encoding.equalsIgnoreCase("md5")) {
                PASSWORD_ENCODING = MD5_ENCODING;
            } else {
                PASSWORD_ENCODING = SIMPLE_MD5_ENCODING;
            }
        }
    }*/

    @ConfigurationFieldAsElement("password")
    private String password = null;
    @ConfigurationFieldAsElement("digestPassword")
    private String digestPassword = null;

    /**
     * Create a new user with name and password
     *
     * @param realm
     * @param id
     * @param name
     * @param password
     * @throws ConfigurationException
     */
    public AccountImpl(final DBBroker broker, final AbstractRealm realm, final int id, final String name,final  String password) throws ConfigurationException {
        super(broker, realm, id, name);
        setPassword(password);
    }

    public AccountImpl(final DBBroker broker, final AbstractRealm realm, final int id, final String name, final String password, final Group group, final boolean hasDbaRole) throws ConfigurationException {
        super(broker, realm, id, name);
        setPassword(password);
        this.groups.add(group);
        this.hasDbaRole = hasDbaRole;
    }

    public AccountImpl(final DBBroker broker, final AbstractRealm realm, final int id, final String name, final String password, final Group group) throws ConfigurationException {
        super(broker, realm, id, name);
        setPassword(password);
        this.groups.add(group);
    }

    /**
     * Create a new user with name
     *
     * @param realm
     * @param name
     *            The account name
     * @throws ConfigurationException
     */
    public AccountImpl(final DBBroker broker, final AbstractRealm realm, final String name) throws ConfigurationException {
        super(broker, realm, Account.UNDEFINED_ID, name);
    }

//    /**
//     * Create a new user with name, password and primary group
//     *
//     * @param name
//     * @param password
//     * @param primaryGroup
//     * @throws ConfigurationException
//     * @throws PermissionDeniedException
//     */
//	public AccountImpl(AbstractRealm realm, int id, String name, String password, String primaryGroup) throws ConfigurationException {
//		this(realm, id, name, password);
//		addGroup(primaryGroup);
//	}
    
    public AccountImpl(final DBBroker broker, final AbstractRealm realm, final int id, final Account from_user) throws ConfigurationException, PermissionDeniedException {
        super(broker, realm, id, from_user.getName());
        instantiate(from_user);
    }
    
    private void instantiate(final Account from_user) throws PermissionDeniedException {

        //copy metadata
        for(final SchemaType metadataKey : from_user.getMetadataKeys()) {
            final String metadataValue = from_user.getMetadataValue(metadataKey);
            setMetadataValue(metadataKey, metadataValue);
        }
        
        //copy umask
        setUserMask(from_user.getUserMask());

        if(from_user instanceof AccountImpl) {
            final AccountImpl user = (AccountImpl) from_user;

            groups = new ArrayList<>(user.groups);

            password = user.password;
            digestPassword = user.digestPassword;

            hasDbaRole = user.hasDbaRole;

            _cred = user._cred;
        } else if(from_user instanceof UserAider) {
            final UserAider user = (UserAider) from_user;

            final String[] groups = user.getGroups();
            for (final String group : groups) {
                addGroup(group);
            }

            setPassword(user.getPassword());
            digestPassword = user.getDigestPassword();
        } else {
            addGroup(from_user.getDefaultGroup());
            //TODO: groups
        }
    }

    public AccountImpl(final DBBroker broker, final AbstractRealm realm, final AccountImpl from_user) throws ConfigurationException {
        super(broker, realm, from_user.id, from_user.name);

        //copy metadata
        for(final SchemaType metadataKey : from_user.getMetadataKeys()) {
            final String metadataValue = from_user.getMetadataValue(metadataKey);
            setMetadataValue(metadataKey, metadataValue);
        }

        groups = from_user.groups;

        password = from_user.password;
        digestPassword = from_user.digestPassword;

        hasDbaRole = from_user.hasDbaRole;

        _cred = from_user._cred;

        //this.realm = realm;   //set via super()
    }

    public AccountImpl(final AbstractRealm realm, final Configuration configuration) throws ConfigurationException {
        super(realm, configuration);

        //this is required because the classes fields are initialized after the super constructor
        if(this.configuration != null) {
            this.configuration = Configurator.configure(this, this.configuration);
        }
        this.hasDbaRole = this.hasGroup(SecurityManager.DBA_GROUP);
    }

    public AccountImpl(final AbstractRealm realm, final Configuration configuration, final boolean removed) throws ConfigurationException {
        this(realm, configuration);
        this.removed = removed;
    }

    /**
     * Get the user's password
     *
     * @return Description of the Return Value
     * @deprecated
     */
    @Override
    public final String getPassword() {
        return password;
    }

    @Override
    public final String getDigestPassword() {
        return digestPassword;
    }

    @Override
    public final void setPassword(final String passwd) {
        _cred = new Password(this, passwd);

        if(passwd == null) {
            this.password = null;
            this.digestPassword = null;
        } else {
            this.password = _cred.toString();
            this.digestPassword = _cred.getDigest();
        }
    }

    @Override
    public void setCredential(final Credential credential) {
        this._cred = credential;
        this.password = _cred.toString();
        this.digestPassword = _cred.getDigest();
    }

    public final static class SecurityProperties {

        private final static boolean DEFAULT_CHECK_PASSWORDS = true;

        private final static String PROP_CHECK_PASSWORDS = "passwords.check";

        private Properties loadedSecurityProperties = null;
        private Boolean checkPasswords = null;

        public synchronized boolean isCheckPasswords() {
            if(checkPasswords == null) {
                final String property = getProperty(PROP_CHECK_PASSWORDS);
                if(property == null || property.length() == 0) {
                    checkPasswords = DEFAULT_CHECK_PASSWORDS;
                } else {
                    checkPasswords = property.equalsIgnoreCase("yes") || property.equalsIgnoreCase("true");
                }
            }
            return checkPasswords;
        }

        public synchronized void enableCheckPasswords(final boolean enable) {
            this.checkPasswords = enable;
        }

        private synchronized String getProperty(final String propertyName) {
            if(loadedSecurityProperties == null) {
                loadedSecurityProperties = new Properties();

                try(final InputStream is = AccountImpl.class.getResourceAsStream("security.properties")) {
                    
                    if(is != null) {
                        loadedSecurityProperties.load(is);
                    }
                } catch(final IOException ioe) {
                    LOG.error("Unable to load security.properties, using defaults. " + ioe.getMessage(), ioe);
                }
            }
            return loadedSecurityProperties.getProperty(propertyName);
        }
    }

    //this method is used by Configurator
    public final Group insertGroup(final int index, final String name) throws PermissionDeniedException {
        //if we cant find the group in our own realm, try other realms
        final Group group = Optional.ofNullable(getRealm().getGroup(name))
                .orElse(getRealm().getSecurityManager().getGroup(name));

        return insertGroup(index, group);
    }

    private Group insertGroup(final int index, final Group group) throws PermissionDeniedException {

        if(group == null){
            return null;
        }

        final Account user = getDatabase().getActiveBroker().getCurrentSubject();
        group.assertCanModifyGroup(user);

        if(!groups.contains(group)) {
            groups.add(index, group);

            if(SecurityManager.DBA_GROUP.equals(group.getName())) {
                hasDbaRole = true;
            }
        }

        return group;
    }
}
