/*
 * Copyright 2006-2007 Queplix Corp.
 *
 * Licensed under the Queplix Public License, Version 1.1.1 (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.queplix.com/solutions/commercial-open-source/queplix-public-license/
 *
 * 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 com.queplix.core.integrator.security;

import com.queplix.core.error.GenericSystemException;
import com.queplix.core.modules.config.utils.EntityHelper;
import com.queplix.core.modules.config.utils.SysPropertyManager;
import com.queplix.core.modules.eql.error.EQLException;
import com.queplix.core.modules.jeo.JEObjectHandler;
import com.queplix.core.modules.jeo.ejb.JEOManagerLocal;
import com.queplix.core.modules.jeo.ejb.JEOManagerLocalHome;
import com.queplix.core.modules.jeo.gen.UserObject;
import com.queplix.core.modules.jeo.gen.UserObjectHandler;
import com.queplix.core.modules.jeo.gen.UserPermissionsObject;
import com.queplix.core.modules.jeo.gen.UserPermissionsObjectHandler;
import com.queplix.core.modules.jeo.gen.UserRolesObject;
import com.queplix.core.modules.jeo.gen.UserRolesObjectHandler;
import com.queplix.core.modules.jeo.gen.UserSettingsObject;
import com.queplix.core.modules.jeo.gen.UserSettingsObjectHandler;
import com.queplix.core.modules.jeo.gen.ViewObjectsObject;
import com.queplix.core.modules.jeo.gen.ViewObjectsObjectHandler;
import com.queplix.core.modules.jeo.gen.WorkgroupObject;
import com.queplix.core.modules.jeo.gen.WorkgroupObjectHandler;
import com.queplix.core.modules.security.LoginModule;
import com.queplix.core.modules.security.LoginModuleFactory;
import com.queplix.core.utils.JNDINames;
import com.queplix.core.utils.NumberHelper;
import com.queplix.core.utils.SystemHelper;
import com.queplix.core.utils.cache.CacheObjectManager;
import com.queplix.core.utils.log.AbstractLogger;
import com.queplix.core.utils.log.Log;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Helper class for security-related operations.
 *
 * @author Kozmin Sergey
 * @author [MVT] Michael Trofimov
 * @since 16.01.2007
 */
public final class AccessRightsManager {

    /**
     * Admin user ID (system property).
     */
    private static final String ADMIN_ID_SYS_PROPERTY = "AdminId";

    /**
     * System user login (system property).
     */
    private static final String SYSTEM_LOGIN_PROP = "ReporterId";

    /**
     * System user's authentication type.
     * TODO do we need to move this value to the qx_sys_prop table?
     */
    private static final int SYSTEM_AUTH_TYPE = 0;

    /**
     * System session id attribute. All system properties has this session id.
     */
    public static final String SYSTEM_SESSION_ID = "SYSTEM_SESSION_ID";

    /**
     * Logger.
     */
    private static AbstractLogger logger = Log.getLog(
            AccessRightsManager.class);

    /**
     * Cache object manager
     */
    private static final CacheObjectManager com = new CacheObjectManager();

    /**
     * User getter.
     * Couldn't return null object.
     *
     * @param loginName user login
     * @param password  not crypted password
     * @return User
     * @throws BadNameOrPasswordException thrown if no such user in DB.
     */
    public static User getUser(String loginName, String password)
            throws BadNameOrPasswordException {
        
        UserObject user = null;
        
        JEOManagerLocal jeoManager = getJEOManager();
        LogonSession ls = getSystemLogonSession();
        
        try {
            JEObjectHandler userObjectHandler = UserObjectHandler.selectByLogin (jeoManager, ls, loginName);
            if (userObjectHandler != null) {
                user = (UserObject) userObjectHandler.getJEObject();
            }
         } catch (EQLException e) {
             throw new BadNameOrPasswordException();
         }
         if(user == null) {
              throw new BadNameOrPasswordException();
         }
         
         LoginModule lm; 
         try {
             lm = LoginModuleFactory.getInstance().getLoginModule(user.getUser_type().toString());
         } catch (java.lang.ExceptionInInitializerError e) {
             logger.ERROR("User cannot login: " + e.getMessage());
             throw new BadNameOrPasswordException();
         }

         logger.DEBUG("Trying to get user: " + loginName);
         user = null;
         user = lm.doLogin(loginName, password);

         logger.DEBUG("User found: " + user.getFullname());

         if(user == null) {
             logger.ERROR("User " + loginName + "can not be found");
             throw new GenericSystemException(
                    "AccessRightsManager: user login failed");
         }

         return createUser(ls, user);
    }

    /**
     * User getter.
     * Couldn't return null object.
     *
     * @param userID user id
     * @return User
     * @throws NoSuchUserException thrown if no such user in DB.
     */
    public static User getUser(Long userID) throws NoSuchUserException {
        LogonSession ls = getSystemLogonSession();
        return getUser(ls, userID);
    }

    /**
     * Retrieve the given user using the given logon session object.
     *
     * @param ls     logon session
     * @param userID user id
     * @return user object
     * @throws NoSuchUserException if there is no such user in DB
     */
    private static User getUser(LogonSession ls, Long userID)
            throws NoSuchUserException {

        JEOManagerLocal jeoManager = getJEOManager();
        UserObject user;
        try {
            user = UserObjectHandler.selectObjectByID(jeoManager, ls, userID);
        } catch (EQLException e) {
            logger.ERROR("EQLException: " + e.getMessage(), e);
            throw new GenericSystemException("EQLException: " + e.getMessage());
        }
        if(user == null) {
            throw new NoSuchUserException(userID);
        }

        return createUser(ls, user);
    }

    /**
     * Retrieve administrator user.
     *
     * @return User object
     * @throws NoSuchUserException if admin property doesn't set in system.
     */
    public static User getAdminUser() throws NoSuchUserException {
        return getUser(getAdminUserId());
    }

    /**
     * @return admin user id
     * @throws NoSuchUserException if admin property doesn't set in system.
     */
    public static Long getAdminUserId() throws NoSuchUserException {
        try {
            String prop = SysPropertyManager.getProperty(ADMIN_ID_SYS_PROPERTY);
            return Long.parseLong(prop);
        } catch (NumberFormatException ex) {
            throw new NoSuchUserException(-1L);
        }
    }

    /**
     * This method is for internal use only. It is needed to retrieve initial info about "system" user. This user should not be used for any
     * other purposes and should not be returned to external classes.
     *
     * @return fake system user object.
     */
    private static User getSystemUserPrototype() {
        User user = new User();
        user.setUserID(getSystemUserId());
        user.setFullName("System. ");
        user.setLoginName("system");
        user.setPasswordDigest("");
        user.setAuthenticationType(SYSTEM_AUTH_TYPE);
        user.setLangID(SystemHelper.DEFAULT_LANGUAGE);
        user.setCountryID(SystemHelper.DEFAULT_COUNTRY);
        user.setTimeZoneID(SystemHelper.DEFAULT_TIMEZONE.getID());
        user.setDatePattern(SystemHelper.DEFAULT_DATE_PATTERN);
        user.setTimePattern(SystemHelper.DEFAULT_TIME_PATTERN);
        return user;
    }

    /**
     * Gets an user logon session.
     *
     * @return system LogonSession object
     */
    public static LogonSession getSystemLogonSession() {
        LogonSession protoLs = buildLogonSession(getSystemUserPrototype(),
                SYSTEM_SESSION_ID);
        return protoLs;
        /*//todo as soon as JEO manager have remove ejb interface uncomment it and load JEOManager by local or remove interface.
        User loadedSystemUser;
        try {
            loadedSystemUser = getUser(protoLs, getSystemUserId());
        } catch (NoSuchUserException e) {
            throw new GenericSystemException("There is no system ID account in database. ", e);
        }
        return buildLogonSession(loadedSystemUser, SYSTEM_SESSION_ID);*/
    }

    /**
     * Trys to get {@link #SYSTEM_LOGIN_PROP} as a long, if it can't then throws an exception.
     *
     * @return system user id.
     */
    private static Long getSystemUserId() {
        String prop = SysPropertyManager.getProperty(SYSTEM_LOGIN_PROP);
        try {
            return Long.parseLong(prop);
        } catch (NumberFormatException ex) {
            throw new GenericSystemException("Incorrect System ID value");
        }
    }

    /**
     * Build logon session from the given params.
     *
     * @param user      user
     * @param sessionId session id
     * @return logon session object
     */
    private static LogonSession buildLogonSession(User user, String sessionId) {
        return new LogonSession(user, sessionId);
    }

    /**
     * Creates logon session object for the given user and sessionID.
     *
     * @param user      required user
     * @param sessionID session id. Use {@link #SYSTEM_SESSION_ID} if there is no real session.
     * @return required LogonSession object
     */
    public static LogonSession getLogonSessionForUser(User user,
                                                      String sessionID) {
        return new LogonSession(user, sessionID);
    }

    /**
     * Returns full system permission set for a user.
     *
     * @param user
     * @return
     */
    public static PermissionSet getPermissionSetForUser(User user) {
        // Get system user's logon session
        LogonSession ls = getSystemLogonSession();

        // Create set of real user's permissions, retain only unique permissions with highest access level
        PermissionSet realPermissions;
        try {
            realPermissions = createRealPermissionSet(ls, user);
        } catch (EQLException e) {
            logger.ERROR("EQLException: " + e.getMessage(), e);
            throw new GenericSystemException("EQLException: " + e.getMessage(),
                    e);
        }
        // If empty - no any additional actions needed
        if(realPermissions.isEmpty()) {
            return realPermissions;
        }

        List<ViewObjectsObject> viewObjects;
        try {
            viewObjects = getRelatedViewObjects(ls, realPermissions);
        } catch (EQLException e) {
            logger.ERROR("EQLException: " + e.getMessage(), e);
            throw new GenericSystemException("EQLException: " + e.getMessage(),
                    e);
        }

        // Create set of permissions for each permission object that corresponds with real permissions
        PermissionSet permissions = createPermissionSetFromViewObjects(
                viewObjects, realPermissions);

        // Replace permissions with default access level with real permissions
        permissions.addPermissionSet(realPermissions);

        adjustAccessLevel(permissions);

        return permissions;
    }

    /**
     * Creates set of permissions that includes only permissions that corresponds with real permissions
     * Access level by default - {@link AccessLevel#READ READ}
     * <p/>
     * focus - will be created
     * subfocus - real permission
     * tab1 - will be created
     * form1 - real permission
     * form2 - will be created
     * tab2 - will be created
     * form3 - real permission
     * form4 - real permission
     */
    private static PermissionSet createPermissionSetFromViewObjects(
            List<ViewObjectsObject> viewObjects,
            PermissionSet realPermissions) {

        Set<String> objectsNames = new HashSet<String>();
        PermissionSet permissions = new PermissionSet();

        for(ViewObjectsObject obj : viewObjects) {
            String objectName = obj.getName();
            if(objectsNames.contains(objectName)) {
                continue;
            }

            for(Permission realPermission : realPermissions) {
                String realObjectName = realPermission.getObjectID();
                if(EntityHelper.isParentObject(objectName, realObjectName)
                        || EntityHelper.isParentObject(realObjectName,
                        objectName)) {
                    objectsNames.add(objectName);
                    // access level - null
                    Permission permission = new Permission(
                            PermissionObjectType.getByTypeConstant(
                                    obj.getType()), null, objectName);
                    permissions.addPermission(permission);
                    break;
                }
            }
        }

        return permissions;
    }

    /**
     * Returns set of permissions based on real user's permissions.
     *
     * @return
     * @throws EQLException
     */
    private static PermissionSet createRealPermissionSet(LogonSession ls,
                                                         User user)
            throws EQLException {

        // retrieve real permissions records
        List<UserPermissionsObject> permissionObjs
                = UserPermissionsObjectHandler.selectByUserID(
                getJEOManager(), ls, user.getUserID());
        if(permissionObjs == null) {
            return PermissionSet.EMPTY;
        }

        PermissionSet permissions = new PermissionSet();

        for(UserPermissionsObject permissionObj : permissionObjs) {
            PermissionObjectType objectType = PermissionObjectType
                    .getByTypeConstant(
                            permissionObj.getPermission_object_type());
            // Value of access_level
            AccessLevel accessLevel = AccessLevel.getByLevelConstant(
                    permissionObj.getAccess_level());
            // Value of object_name
            String objectName = permissionObj.getObject_name();

            Permission permission = permissions.getPermissionObject(objectType,
                    objectName);
            if(permission != null) {
                // retain only unique permissions with highest access level
                if(permission.getLevel().level < accessLevel.level) {
                    permission.setLevel(accessLevel);
                }
            } else {
                permission = new Permission(objectType, accessLevel,
                        objectName);
                permissions.addPermission(permission);
            }
        }

        return permissions;
    }

    /**
     * Adjusts given set of permissions by access level in proper order.
     * Note that given set of permissions should contains sequence of all permissions for
     * the tree-like structure as shown below.
     * <p/>
     * focus - by default (READ)
     * subfocus - real permission (WRITE)
     * tab1 - inherited from parent subfocus (WRITE)
     * form1 - real permission (OWNER)
     * form2 - inherited from parent tab (WRITE)
     * tab2 - inherited from parent subfocus (WRITE)
     * form3 - real permission (READ overloads inherited WRITE)
     * form4 - real permission (OWNER)
     */
    private static void adjustAccessLevel(PermissionSet permissions) {
        for(Permission permission : permissions) {
            if(permission.getObjectType().equals(PermissionObjectType.FOCUS)) {
                ajustAccessLevelRecursively(permissions, permission);
            }
        }
    }

    private static void ajustAccessLevelRecursively(PermissionSet permissions,
                                                    Permission parent) {
        // Default permission level is READ
        if(parent.getLevel() == null) {
            parent.setLevel(AccessLevel.READ);
        }

        for(Permission permission : permissions) {
            if(isDirectChildPermission(parent, permission)) {
                if(permission.getLevel() == null) {
                    permission.setLevel(parent.getLevel());
                }

                ajustAccessLevelRecursively(permissions, permission);
            }
        }
    }

    private static boolean isDirectChildPermission(Permission parent,
                                                   Permission child) {
        if(EntityHelper.isParentObject(parent.getObjectID(),
                child.getObjectID())) {
            return child.getObjectType().isDirectChildOf(
                    parent.getObjectType());
        }
        return false;
    }

    private static Set<String> getFocusNames(PermissionSet permissionSet) {
        Set<String> focusNames = new HashSet<String>();
        for(Permission permission : permissionSet) {
            focusNames.add(
                    EntityHelper.getFocusName(permission.getObjectID()));
        }
        return focusNames;
    }

    /**
     * Retrieves all permission objects (focus->subfocus->tab->form)
     * that related to the geven set of permissions
     *
     * @param ls
     * @param permissions
     * @return
     * @throws EQLException
     */
    private static List<ViewObjectsObject> getRelatedViewObjects(
            LogonSession ls, PermissionSet permissions) throws EQLException {

        List<ViewObjectsObject> viewObjects
                = new ArrayList<ViewObjectsObject>();
        for(String focusName : getFocusNames(permissions)) {
            List<ViewObjectsObject> objs
                    = ViewObjectsObjectHandler.selectByFocus(getJEOManager(),
                    ls, focusName);
            if(objs != null) {
                viewObjects.addAll(objs);
            }
        }

        return viewObjects;
    }

    /**
     * Returns permission object for some object in system for specified user.
     *
     * @param user     user to check permission
     * @param objectID object id in system. (can be focus name, subfocus name, tab name, form name)
     * @param type     type of the object.
     * @return permission object
     */
    public static Permission getPermissionForObject(User user, String objectID,
                                                    PermissionObjectType type) {
        PermissionSet permissions = getPermissionSetForUser(user);
        return permissions.getPermissionObject(type, objectID);
    }

    /**
     * Need this method in IntegratorGetRecords
     *
     * @param user       requesting user
     * @param objectID   object id
     * @param objectType object type
     * @param action     action type
     * @return can this user perform this kind of action under the given object
     */
    public static boolean canUserPerformAction(User user, String objectID,
                                               PermissionObjectType objectType,
                                               CommonActionTypes action) {
        return canUserPerformAction(getPermissionForObject(user, objectID,
                objectType).getLevel(), action);
    }

    /**
     * @param level  access level for that
     * @param action action type
     * @return is this action available for this access level.
     */
    public static boolean canUserPerformAction(AccessLevel level,
                                               CommonActionTypes action) {
        boolean can = false;
        switch(action) {
            case READ:
                can = level.level >= AccessLevel.READ.level;
                break;
            case WRITE:
                can = level.level >= AccessLevel.WRITE.level;
                break;
            case DELETE_OWNED_RECORDS:
                can = level.level >= AccessLevel.OWNER.level;
                break;
            case DELETE_ANY_RECORD:
                can = level.level >= AccessLevel.FULL_CONTROL.level;
                break;
        }
        return can;
    }

    /**
     * Retrieve collection of users that belong to some group
     * Couldn't return null object.
     *
     * @param groupID
     * @return
     * @throws NoSuchGroupException thrown if no such group with the given id
     */
    public static Collection<User> getUsersInGroup(Long groupID)
            throws NoSuchGroupException {
        LogonSession ls = getSystemLogonSession();

        List<UserObject> userObjs;
        try {
            userObjs = UserObjectHandler.selectByWorkgroupID(
                    getJEOManager(), ls, groupID);
        } catch (EQLException e) {
            logger.ERROR("EQLException: " + e.getMessage(), e);
            throw new GenericSystemException("EQLException: " + e.getMessage(),
                    e);
        }
        if(userObjs == null) {
            throw new NoSuchGroupException(groupID);
        }

        return createUsers(ls, userObjs);
    }

    /**
     * Retrieve collection of users that belong to some group with the given tier
     * Couldn't return null object.
     *
     * @param groupID
     * @param tier    tier of the users
     * @return
     * @throws NoSuchGroupException thrown if no such group with the given id
     */
    public static Collection<User> getUsersInGroup(Long groupID, Integer tier)
            throws NoSuchGroupException {
        LogonSession ls = getSystemLogonSession();

        List<UserObject> userObjs;
        try {
            userObjs = UserObjectHandler.selectByWorkgroupIDAndTier(
                    getJEOManager(), ls, groupID, tier);
        } catch (EQLException e) {
            logger.ERROR("EQLException: " + e.getMessage(), e);
            throw new GenericSystemException("EQLException: " + e.getMessage(),
                    e);
        }
        if(userObjs == null) {
            throw new NoSuchGroupException(groupID);
        }

        return createUsers(ls, userObjs);
    }

    /**
     * Retrieve workgroup by it's id.
     * Couldn't return null object.
     *
     * @param groupID
     * @return
     * @throws NoSuchGroupException thrown if no such group with the given id
     */
    public static WorkGroup getGroup(Long groupID) throws NoSuchGroupException {
        WorkgroupObject workgroupObj;
        try {
            workgroupObj = WorkgroupObjectHandler.selectByID(
                    getJEOManager(), getSystemLogonSession(), groupID);
        } catch (EQLException e) {
            logger.ERROR("EQLException: " + e.getMessage(), e);
            throw new GenericSystemException("EQLException: " + e.getMessage(),
                    e);
        }
        if(workgroupObj == null) {
            throw new NoSuchGroupException(groupID);
        }

        WorkGroup workGroup = new WorkGroup(groupID,
                workgroupObj.getName(), workgroupObj.getNotificationaddr(),
                workgroupObj.getNotifymethod().intValue());

        return workGroup;
    }

    private static JEOManagerLocal getJEOManager() {
        return (JEOManagerLocal) com.getLocalObject(JNDINames.JEOManager,
                JEOManagerLocalHome.class);
    }

    private static User createUser(LogonSession ls, UserObject jeoUser) {
        User user = new User();
        user.setUserID(jeoUser.getPkey());
        user.setLoginName(jeoUser.getLoginname());
        user.setPasswordDigest(jeoUser.getPassword());
        user.setFullName(jeoUser.getFullname());
        user.setEmail(jeoUser.getEmail());
        user.setAuthenticationType(jeoUser.getUser_type());

        // User settings
        JEOManagerLocal jeoManager = getJEOManager();
        UserSettingsObject userProp;
        try {
            userProp = UserSettingsObjectHandler.selectByUser(jeoManager, ls,
                    jeoUser);
        } catch (EQLException e) {
            logger.ERROR("EQLException: " + e.getMessage(), e);
            throw new GenericSystemException("EQLException: " + e.getMessage(),
                    e);
        }
        if(userProp == null) {
            user.setLangID(SystemHelper.DEFAULT_LANGUAGE);
            user.setCountryID(SystemHelper.DEFAULT_COUNTRY);
            user.setTimeZoneID(SystemHelper.DEFAULT_TIMEZONE.getID());
            user.setDatePattern(SystemHelper.DEFAULT_DATE_PATTERN);
            user.setTimePattern(SystemHelper.DEFAULT_TIME_PATTERN);
        } else {
            String langID = userProp.textLang();
            user.setLangID(
                    langID != null ? langID:SystemHelper.DEFAULT_LANGUAGE);
            String countryID = userProp.textCountry();
            user.setCountryID(
                    countryID != null ? countryID:SystemHelper.DEFAULT_COUNTRY);
            String timeZoneID = userProp.textTimezone();
            user.setTimeZoneID(timeZoneID != null ? timeZoneID
                    :SystemHelper.DEFAULT_TIMEZONE.getID());
            String datePattern = userProp.textDpattern();
            user.setDatePattern(datePattern != null ? datePattern
                    :SystemHelper.DEFAULT_DATE_PATTERN);
            String timePattern = userProp.textTpattern();
            user.setTimePattern(timePattern != null ? timePattern
                    :SystemHelper.DEFAULT_TIME_PATTERN);
            user.setDatePositionFirst(NumberHelper.num2bool(
                    userProp.getDpos()));
        }

        // All User roles
        List<UserRolesObject> userRoles;
        try {
            userRoles = UserRolesObjectHandler.selectByUser(jeoManager, ls,
                    jeoUser);
            Set<Long> rolesIDs = new HashSet<Long>();
            if(userRoles != null) {
                for(UserRolesObject role : userRoles) {
                    rolesIDs.add(role.getRole_id());

                    Long defaultFocus = role.getDefault_focus_id();
                    if(defaultFocus != null && user.getDefaultFocus() == null) {
                        ViewObjectsObject focus = ViewObjectsObjectHandler
                                .selectByPkey(
                                        jeoManager, ls, defaultFocus);
                        if(focus != null) {
                            user.setDefaultFocus(focus.getName());
                        }
                    }
                }
            }
            user.setRolesIDs(rolesIDs);
        } catch (EQLException e) {
            logger.ERROR("EQLException: " + e.getMessage(), e);
            throw new GenericSystemException("EQLException: " + e.getMessage(),
                    e);
        }

        //  User workgroups
        List<WorkgroupObject> workgroups;
        try {
            workgroups = WorkgroupObjectHandler.selectByUser(jeoManager, ls,
                    jeoUser);
        } catch (EQLException e) {
            logger.ERROR("EQLException: " + e.getMessage(), e);
            throw new GenericSystemException("EQLException: " + e.getMessage(),
                    e);
        }
        if(workgroups != null) {
            List<Long> workgroupIDs = new ArrayList<Long>();
            for(WorkgroupObject workgroup : workgroups) {
                workgroupIDs.add(workgroup.getPkey());
            }
            user.setUserGroups(workgroupIDs);
        }
        return user;
    }

    private static Collection<User> createUsers(LogonSession ls,
                                                List<UserObject> userObjs) {
        List<User> users = new ArrayList<User>();
        for(UserObject userObj : userObjs) {
            users.add(createUser(ls, userObj));
        }
        return users;
    }

}
