/*
 * RHQ Management Platform
 * Copyright (C) 2005-2010 Red Hat, Inc.
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation version 2 of the License.
 *
 * 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package org.rhq.coregui.client.admin.users;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.google.gwt.user.client.rpc.AsyncCallback;
import com.smartgwt.client.data.DSRequest;
import com.smartgwt.client.data.DSResponse;
import com.smartgwt.client.data.DataSourceField;
import com.smartgwt.client.data.Record;
import com.smartgwt.client.data.fields.DataSourceIntegerField;
import com.smartgwt.client.data.fields.DataSourcePasswordField;
import com.smartgwt.client.data.fields.DataSourceTextField;
import com.smartgwt.client.types.FieldType;
import com.smartgwt.client.widgets.form.validator.LengthRangeValidator;
import com.smartgwt.client.widgets.form.validator.MatchesFieldValidator;
import com.smartgwt.client.widgets.form.validator.RegExpValidator;
import com.smartgwt.client.widgets.grid.ListGridRecord;

import org.rhq.core.domain.auth.Subject;
import org.rhq.core.domain.criteria.SubjectCriteria;
import org.rhq.core.domain.util.PageList;
import org.rhq.coregui.client.admin.roles.RolesDataSource;
import org.rhq.coregui.client.gwt.GWTServiceLookup;
import org.rhq.coregui.client.gwt.SubjectGWTServiceAsync;
import org.rhq.coregui.client.util.RPCDataSource;

/**
 * A DataSource for RHQ {@link Subject user}s.
 *
 * @author Greg Hinkle
 * @author Ian Springer
 */
public class UsersDataSource extends RPCDataSource<Subject, SubjectCriteria> {

    private static UsersDataSource INSTANCE;

    private static final String EMAIL_ADDRESS_REGEXP = "^([a-zA-Z0-9_.\\-+])+@([a-zA-Z0-9\\-])+(\\.([a-zA-Z0-9\\-])+)*$";
    private static final String MASKED_PASSWORD_VALUE = "XXXXXXXX";

    public static abstract class Field {
        public static final String ID = "id";
        public static final String NAME = "name";
        public static final String FIRST_NAME = "firstName";
        public static final String LAST_NAME = "lastName";
        public static final String FACTIVE = "factive";
        public static final String FSYSTEM = "fsystem";
        public static final String DEPARTMENT = "department";
        public static final String PHONE_NUMBER = "phoneNumber";
        public static final String EMAIL_ADDRESS = "emailAddress";
        public static final String ROLES = "roles";

        // auth-related fields
        public static final String LDAP = "ldap";
        public static final String PASSWORD = "password";
        public static final String PASSWORD_VERIFY = "passwordVerify";
    }

    public static final int ID_OVERLORD = 1;
    public static final int ID_RHQADMIN = 2;

    private final SubjectGWTServiceAsync subjectService = GWTServiceLookup.getSubjectService();

    public static UsersDataSource getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new UsersDataSource();
        }
        return INSTANCE;
    }

    public static boolean isSystemSubjectId(int subjectId) {
        return (subjectId == ID_OVERLORD || subjectId == ID_RHQADMIN);
    }

    public UsersDataSource() {
        List<DataSourceField> fields = addDataSourceFields();
        addFields(fields);
    }

    @Override
    protected List<DataSourceField> addDataSourceFields() {
        List<DataSourceField> fields = super.addDataSourceFields();

        DataSourceField idDataField = new DataSourceIntegerField(Field.ID, "ID");
        idDataField.setPrimaryKey(true);
        idDataField.setCanEdit(false);
        fields.add(idDataField);

        DataSourceTextField usernameField = createTextField(Field.NAME, MSG.common_title_username(), 3, 100, true);
        // Don't allow characters that could be used in HTML intended for an XSS attack.
        RegExpValidator regExpValidator = new RegExpValidator("[^&<]*");
        usernameField.setValidators(regExpValidator);
        fields.add(usernameField);

        DataSourceTextField ldapField = createBooleanField(Field.LDAP, MSG.dataSource_users_field_ldap(), true);
        ldapField.setCanEdit(false); // read-only
        fields.add(ldapField);

        DataSourcePasswordField passwordField = new DataSourcePasswordField(Field.PASSWORD,
            MSG.common_title_password(), 100, true);
        LengthRangeValidator passwordValidator = new LengthRangeValidator();
        passwordValidator.setMin(6);
        passwordValidator.setMax(100);
        passwordField.setValidators(passwordValidator);
        fields.add(passwordField);

        DataSourcePasswordField passwordVerifyField = new DataSourcePasswordField(Field.PASSWORD_VERIFY,
            MSG.dataSource_users_field_passwordVerify(), 100, true);
        MatchesFieldValidator passwordsEqualValidator = new MatchesFieldValidator();
        passwordsEqualValidator.setOtherField(Field.PASSWORD);
        passwordsEqualValidator.setErrorMessage(MSG.dataSource_users_passwordsDoNotMatch());
        passwordVerifyField.setValidators(passwordsEqualValidator);
        fields.add(passwordVerifyField);

        DataSourceTextField firstNameField = createTextField(Field.FIRST_NAME, MSG.dataSource_users_field_firstName(),
            null, 100, true);
        fields.add(firstNameField);

        DataSourceTextField lastNameField = createTextField(Field.LAST_NAME, MSG.dataSource_users_field_lastName(),
            null, 100, true);
        fields.add(lastNameField);

        DataSourceTextField emailAddressField = createTextField(Field.EMAIL_ADDRESS,
            MSG.dataSource_users_field_emailAddress(), null, 100, true);
        fields.add(emailAddressField);
        RegExpValidator emailAddressValidator = new RegExpValidator(EMAIL_ADDRESS_REGEXP);
        emailAddressValidator.setErrorMessage(MSG.dataSource_users_invalidEmailAddress());
        emailAddressField.setValidators(emailAddressValidator);

        DataSourceTextField phoneNumberField = createTextField(Field.PHONE_NUMBER,
            MSG.dataSource_users_field_phoneNumber(), null, 100, false);
        fields.add(phoneNumberField);

        DataSourceTextField departmentField = createTextField(Field.DEPARTMENT,
            MSG.dataSource_users_field_department(), null, 100, false);
        fields.add(departmentField);

        DataSourceTextField enabledField = createBooleanField(Field.FACTIVE, MSG.dataSource_users_field_factive(), true);
        fields.add(enabledField);

        DataSourceField rolesField = new DataSourceField(Field.ROLES, FieldType.ANY, "Roles");
        fields.add(rolesField);

        return fields;
    }

    @Override
    public void executeFetch(final DSRequest request, final DSResponse response, final SubjectCriteria criteria) {

        subjectService.findSubjectsByCriteria(criteria, new AsyncCallback<PageList<Subject>>() {
            public void onFailure(Throwable caught) {
                String message = "Failed to fetch user(s).";
                sendFailureResponse(request, response, message, caught);
            }

            public void onSuccess(final PageList<Subject> fetchedSubjects) {
                final PageList<Record> userRecordsPageList = new PageList<Record>(fetchedSubjects.getPageControl());
                userRecordsPageList.setTotalSize(fetchedSubjects.getTotalSize());
                userRecordsPageList.setUnbounded(fetchedSubjects.isUnbounded());
                final boolean[] failed = { false };
                for (int i = 0, fetchedSubjectsSize = fetchedSubjects.size(); i < fetchedSubjectsSize; i++) {
                    if (failed[0]) {
                        break;
                    }
                    final Subject fetchedSubject = fetchedSubjects.get(i);
                    final String username = fetchedSubject.getName();
                    subjectService.isUserWithPrincipal(username, new AsyncCallback<Boolean>() {
                        public void onFailure(Throwable caught) {
                            failed[0] = true;
                            String message = "Failed to check if user [" + username + "] is an LDAP user.";
                            sendFailureResponse(request, response, message, caught);
                        }

                        public void onSuccess(Boolean hasPrincipal) {
                            boolean isLdap = (!hasPrincipal);
                            Record userRecord = copyUserValues(fetchedSubject, isLdap);
                            userRecordsPageList.add(userRecord);
                            if (userRecordsPageList.size() == fetchedSubjects.size()) {
                                sendSuccessResponseRecords(request, response, userRecordsPageList);
                            }
                        }
                    });
                }
            }
        });
    }

    @Override
    protected void executeAdd(final Record recordToAdd, final DSRequest request, final DSResponse response) {
        final Subject newSubject = copyValues(recordToAdd);
        String password = recordToAdd.getAttribute(Field.PASSWORD);

        subjectService.createSubject(newSubject, password, new AsyncCallback<Subject>() {
            public void onFailure(Throwable caught) {
                // TODO: Throw more specific SLSB exceptions so we can set the right validation errors.
                String message = caught.getMessage();
                if (message != null && message.contains("javax.persistence.EntityExistsException")) {
                    Map<String, String> errorMessages = new HashMap<String, String>();
                    errorMessages.put(Field.NAME,
                        MSG.view_adminUsers_failCreateUserWithExistingName(newSubject.getName()));
                    sendValidationErrorResponse(request, response, errorMessages);
                } else {
                    throw new RuntimeException(caught);
                }
            }

            public void onSuccess(final Subject createdSubject) {
                Record createdUserRecord = copyUserValues(createdSubject, false);
                sendSuccessResponse(request, response, createdUserRecord);
            }
        });
    }

    @Override
    protected void executeUpdate(final Record editedUserRecord, Record oldUserRecord, final DSRequest request,
        final DSResponse response) {
        Subject editedSubject = copyValues(editedUserRecord);
        final String username = editedSubject.getName();

        final String editedPassword = editedUserRecord.getAttributeAsString(Field.PASSWORD);
        boolean passwordWasEdited = !MASKED_PASSWORD_VALUE.equals(editedPassword);
        String newPassword = (passwordWasEdited) ? editedPassword : null;

        subjectService.updateSubject(editedSubject, newPassword, new AsyncCallback<Subject>() {
            public void onFailure(Throwable caught) {
                String message = "Failed to update user [" + username + "].";
                sendFailureResponse(request, response, message, caught);
            }

            public void onSuccess(final Subject updatedSubject) {
                sendSuccessResponse(request, response, editedUserRecord);
            }
        });
    }

    public Subject copyValues(Record from) {
        Subject to = new Subject();

        to.setId(from.getAttributeAsInt(Field.ID));
        to.setName(from.getAttributeAsString(Field.NAME));
        to.setFirstName(from.getAttributeAsString(Field.FIRST_NAME));
        to.setLastName(from.getAttributeAsString(Field.LAST_NAME));
        to.setFactive(Boolean.valueOf(from.getAttributeAsString(Field.FACTIVE)));
        to.setFsystem(Boolean.valueOf(from.getAttributeAsString(Field.FSYSTEM)));
        to.setDepartment(from.getAttributeAsString(Field.DEPARTMENT));
        to.setPhoneNumber(from.getAttributeAsString(Field.PHONE_NUMBER));
        to.setEmailAddress(from.getAttributeAsString(Field.EMAIL_ADDRESS));

        Record[] roleRecords = from.getAttributeAsRecordArray(Field.ROLES);
        to.setRoles(RolesDataSource.getInstance().buildDataObjects(roleRecords));

        return to;
    }

    public Record copyUserValues(Subject subject, boolean isLdap) {
        ListGridRecord targetRecord = copyValues(subject);

        targetRecord.setAttribute(Field.LDAP, isLdap);

        // Leave the password fields blank if username is null (i.e. it's a new user).
        if (subject.getName() != null) {
            targetRecord.setAttribute(Field.PASSWORD, MASKED_PASSWORD_VALUE);
            targetRecord.setAttribute(Field.PASSWORD_VERIFY, MASKED_PASSWORD_VALUE);
        }

        return targetRecord;
    }

    public ListGridRecord copyValues(Subject from) {
        return copyValues(from, true);
    }

    @Override
    public ListGridRecord copyValues(Subject from, boolean cascade) {
        ListGridRecord to = new ListGridRecord();

        to.setAttribute(Field.ID, from.getId());
        to.setAttribute(Field.NAME, from.getName());
        to.setAttribute(Field.FIRST_NAME, from.getFirstName());
        to.setAttribute(Field.LAST_NAME, from.getLastName());
        to.setAttribute(Field.FACTIVE, from.getFactive());
        to.setAttribute(Field.FSYSTEM, from.getFsystem());
        to.setAttribute(Field.DEPARTMENT, from.getDepartment());
        to.setAttribute(Field.PHONE_NUMBER, from.getPhoneNumber());
        to.setAttribute(Field.EMAIL_ADDRESS, from.getEmailAddress());

        if (cascade) {
            ListGridRecord[] roleRecords = RolesDataSource.getInstance().buildRecords(from.getRoles(), false);
            to.setAttribute(Field.ROLES, roleRecords);
        }

        return to;
    }

    @Override
    protected SubjectCriteria getFetchCriteria(DSRequest request) {
        SubjectCriteria criteria = new SubjectCriteria();

        // Filtering
        Integer subjectId = getFilter(request, Field.ID, Integer.class);
        criteria.addFilterId(subjectId);
        // Always filter out the overlord - mortal users need not know the overlord even exists.
        criteria.addFilterFsystem(false);

        // Fetching
        if (subjectId != null) {
            // If we're fetching a single Subject, then fetch the related Set of Roles.
            criteria.fetchRoles(true);
        }

        // TODO: For the list view, use a composite object that will pull the role
        //       count across the wire.  this count will not require permission checks at all.

        return criteria;
    }

    @Override
    protected String getSortFieldForColumn(String columnName) {

        // this is a calculated field, can't perform server-side sort
        if (Field.LDAP.equals(columnName)) {
            return null;
        }

        return super.getSortFieldForColumn(columnName);
    }

}
