/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2012, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This 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.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.gatein.portal.installer;

import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import javax.jcr.Session;

import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.exoplatform.commons.utils.PropertyManager;
import org.exoplatform.container.ExoContainerContext;
import org.exoplatform.container.PortalContainer;
import org.exoplatform.container.component.RequestLifeCycle;
import org.exoplatform.container.xml.InitParams;
import org.exoplatform.container.xml.ValueParam;
import org.exoplatform.services.jcr.RepositoryService;
import org.exoplatform.services.jcr.core.ManageableRepository;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.exoplatform.services.organization.Group;
import org.exoplatform.services.organization.MembershipType;
import org.exoplatform.services.organization.OrganizationService;
import org.exoplatform.services.organization.User;
import org.exoplatform.services.organization.UserStatus;
import org.picocontainer.Startable;

/**
 * This class is responsible to check a flag in JCR and memory for proper GateIn root password setup.
 *
 * @author <a href="mailto:lponce@redhat.com">Lucas Ponce</a>
 *
 */
public class PortalSetupService implements Startable {
    private static Log log = ExoLogger.getLogger(PortalSetupService.class);

    public static final String GATEIN_SETUP_ENABLE = "gatein.portal.setup.enable";
    public static final String ROOT_PASSWORD_PROPERTY = "gatein.portal.setup.initialpassword.root";

    private static String SALT = "unodostrescuatro";
    private static int COUNT = 9;
    private static String KEY = "somearbitrarycrazystringthatdoesnotmatter";

    private static final String SETUP_FLAG = "gatein-setup-flag";

    private String jcrWS;
    private String jcrRepo;
    private ManageableRepository repository;

    // We check root password per portal container
    private Map<String, Boolean> setup = new ConcurrentHashMap<String, Boolean>();

    private OrganizationService orgService;
    private RepositoryService jcrService;
    private boolean enable;

    public PortalSetupService(InitParams params, OrganizationService orgService, RepositoryService jcrService) throws Exception {

        ValueParam repoConfig = params.getValueParam("repository");
        if (repoConfig != null) {
            jcrRepo = repoConfig.getValue().trim();
        }
        ValueParam ws = params.getValueParam("workspace");
        if (ws != null) {
            jcrWS = ws.getValue().trim();
        }

        //
        this.orgService = orgService;
        this.jcrService = jcrService;
        String config = PropertyManager.getProperty(PortalSetupService.GATEIN_SETUP_ENABLE);
        this.enable = Boolean.parseBoolean(config);
    }

    @Override
    public void start() {
        try {
            if (jcrRepo != null) {
                repository = jcrService.getRepository(jcrRepo);
            } else {
                repository = jcrService.getCurrentRepository();
            }

            if (jcrWS == null) {
                jcrWS = repository.getConfiguration().getDefaultWorkspaceName();
            }
        } catch (Exception e) {
            log.error("Can't get JCR repository", e);
        }

        if (isEnable()) {
            RequestLifeCycle.begin(PortalContainer.getInstance());
            checkJcrFlag();
            try {
                if (!isSetup()) {
                    User root = getRootUser();
                    root.setPassword(rootPassword());
                    orgService.getUserHandler().saveUser(root, true);
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                RequestLifeCycle.end();
            }
        }
    }

    public User getRootUser() throws Exception {
        User root = orgService.getUserHandler().findUserByName("root", UserStatus.ANY);
        // In the case the root user is not present
        // This case can happens if organization-configuration.xml is not well configured
        if (root == null) {
            root = orgService.getUserHandler().createUserInstance("root");
            root.setFirstName("Root");
            root.setLastName("Root");
            root.setEmail("root@localhost");
            root.setDisplayName("root");
            orgService.getUserHandler().createUser(root, true);
            // Get memberships
            MembershipType manager = orgService.getMembershipTypeHandler().findMembershipType("manager");
            MembershipType member = orgService.getMembershipTypeHandler().findMembershipType("member");
            // Get groups
            Group administrators = orgService.getGroupHandler().findGroupById("/platform/administrators");
            Group users = orgService.getGroupHandler().findGroupById("/platform/users");
            Group executive_board = orgService.getGroupHandler().findGroupById("/organization/management/executive-board");
            // Assign users
            orgService.getMembershipHandler().linkMembership(root, administrators, manager, true);
            orgService.getMembershipHandler().linkMembership(root, users, member, true);
            orgService.getMembershipHandler().linkMembership(root, executive_board, member, true);
        }
        return root;
    }

    private String getPCName() {
        return ExoContainerContext.getCurrentContainer().getContext().getPortalContainerName();
    }

    private void checkJcrFlag() {
        setup.put(getPCName(), false);
        try {
            Session session = getJcrSession();
            if (session.itemExists("/" + SETUP_FLAG))
                setup.put(getPCName(), true);
            session.logout();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void setJcrFlag() {
        RequestLifeCycle.begin(PortalContainer.getInstance());
        try {
            Session session = getJcrSession();
            session.getRootNode().addNode(SETUP_FLAG);
            session.save();
            session.logout();
            setup.put(getPCName(), true);
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            RequestLifeCycle.end();
        }
    }

    private String rootPassword() {

        String password = null;
        try {
            password = decodePassword(System.getProperty(ROOT_PASSWORD_PROPERTY));
            if (password == null) {
                password = randomPassword();
                setup.put(getPCName(), false);
            } else {
                setup.put(getPCName(), true);
                setJcrFlag();
            }
            return password;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public String decodePassword(String encodedPassword) throws Exception {

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

        byte[] salt = SALT.substring(0, 8).getBytes();
        int count = COUNT;
        char[] password = KEY.toCharArray();
        PBEParameterSpec cipherSpec = new PBEParameterSpec(salt, count);
        PBEKeySpec keySpec = new PBEKeySpec(password);
        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBEwithMD5andDES");
        SecretKey cipherKey = factory.generateSecret(keySpec);
        return PBEUtils.decode64(encodedPassword, "PBEwithMD5andDES", cipherKey, cipherSpec);
    }

    /**
     * Encodes a plain text password using PBEwithMD5andDES and Base64.
     *
     * @param plainTextPassword
     * @return
     * @throws Exception
     * @throws UnsupportedEncodingException
     * @throws PortalSetupCommand.SetupCommandException
     */
    public static String encodePassword(String plainTextPassword) throws Exception {

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

        String encodedPassword = null;
        byte[] salt = SALT.substring(0, 8).getBytes();
        int count = COUNT;
        char[] password = KEY.toCharArray();
        PBEParameterSpec cipherSpec = new PBEParameterSpec(salt, count);
        PBEKeySpec keySpec = new PBEKeySpec(password);
        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBEwithMD5andDES");
        SecretKey cipherKey = factory.generateSecret(keySpec);
        encodedPassword = PBEUtils.encode64(plainTextPassword.getBytes("UTF-8"), "PBEwithMD5andDES", cipherKey, cipherSpec);
        return encodedPassword;
    }

    /**
     * PortalContainer's name matches with ServletContextName.
     * @see org.exoplatform.container.RootContainer
     */
    public boolean isSetup(String context) {
        return (setup.get(context) != null ? setup.get(context) : false);
    }

    public boolean isSetup() {
        return isSetup(getPCName());
    }

    public boolean isEnable() {
        return enable;
    }

    public void setFlag() {
        setup.put(getPCName(), true);
    }

    private String randomPassword() {
        return new BigInteger(130, new SecureRandom()).toString(8);
    }

    @Override
    public void stop() {
    }

    private Session getJcrSession() throws Exception {
        if (repository == null) {
            throw new IllegalStateException("repository is null, jcrSession can only be retrieved after PortalSetupService has started");
        }

        return repository.getSystemSession(jcrWS);
    }
}
