/*
 * Copyright 2011 gitblit.com.
 *
 * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
 *
 * 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.gitblit;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.gitblit.Constants.AccessPermission;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.DeepCopier;
import com.gitblit.utils.StringUtils;

/**
 * FileUserService is Gitblit's original default user service implementation.
 * 
 * Users and their repository memberships are stored in a simple properties file
 * which is cached and dynamically reloaded when modified.
 * 
 * This class was deprecated in Gitblit 0.8.0 in favor of ConfigUserService
 * which is still a human-readable, editable, plain-text file but it is more
 * flexible for storing additional fields.
 * 
 * @author James Moger
 * 
 */
@Deprecated
public class FileUserService extends FileSettings implements IUserService {

	private final Logger logger = LoggerFactory.getLogger(FileUserService.class);

	private final Map<String, String> cookies = new ConcurrentHashMap<String, String>();

	private final Map<String, TeamModel> teams = new ConcurrentHashMap<String, TeamModel>();

	public FileUserService(File realmFile) {
		super(realmFile.getAbsolutePath());
	}

	/**
	 * Setup the user service.
	 * 
	 * @param settings
	 * @since 0.7.0
	 */
	@Override
	public void setup(IStoredSettings settings) {
	}

	/**
	 * Does the user service support changes to credentials?
	 * 
	 * @return true or false
	 * @since 1.0.0
	 */
	@Override
	public boolean supportsCredentialChanges() {
		return true;
	}

	/**
	 * Does the user service support changes to user display name?
	 * 
	 * @return true or false
	 * @since 1.0.0
	 */
	@Override
	public boolean supportsDisplayNameChanges() {
		return false;
	}

	/**
	 * Does the user service support changes to user email address?
	 * 
	 * @return true or false
	 * @since 1.0.0
	 */
	@Override
	public boolean supportsEmailAddressChanges() {
		return false;
	}

	/**
	 * Does the user service support changes to team memberships?
	 * 
	 * @return true or false
	 * @since 1.0.0
	 */	
	public boolean supportsTeamMembershipChanges() {
		return true;
	}

	/**
	 * Does the user service support cookie authentication?
	 * 
	 * @return true or false
	 */
	@Override
	public boolean supportsCookies() {
		return true;
	}

	/**
	 * Returns the cookie value for the specified user.
	 * 
	 * @param model
	 * @return cookie value
	 */
	@Override
	public String getCookie(UserModel model) {
		if (!StringUtils.isEmpty(model.cookie)) {
			return model.cookie;
		}
		Properties allUsers = super.read();
		String value = allUsers.getProperty(model.username);
		String[] roles = value.split(",");
		String password = roles[0];
		String cookie = StringUtils.getSHA1(model.username + password);
		return cookie;
	}

	/**
	 * Authenticate a user based on their cookie.
	 * 
	 * @param cookie
	 * @return a user object or null
	 */
	@Override
	public UserModel authenticate(char[] cookie) {
		String hash = new String(cookie);
		if (StringUtils.isEmpty(hash)) {
			return null;
		}
		read();
		UserModel model = null;
		if (cookies.containsKey(hash)) {
			String username = cookies.get(hash);
			model = getUserModel(username);
		}
		return model;
	}

	/**
	 * Authenticate a user based on a username and password.
	 * 
	 * @param username
	 * @param password
	 * @return a user object or null
	 */
	@Override
	public UserModel authenticate(String username, char[] password) {
		Properties allUsers = read();
		String userInfo = allUsers.getProperty(username);
		if (StringUtils.isEmpty(userInfo)) {
			return null;
		}
		UserModel returnedUser = null;
		UserModel user = getUserModel(username);
		if (user.password.startsWith(StringUtils.MD5_TYPE)) {
			// password digest
			String md5 = StringUtils.MD5_TYPE + StringUtils.getMD5(new String(password));
			if (user.password.equalsIgnoreCase(md5)) {
				returnedUser = user;
			}
		} else if (user.password.startsWith(StringUtils.COMBINED_MD5_TYPE)) {
			// username+password digest
			String md5 = StringUtils.COMBINED_MD5_TYPE
					+ StringUtils.getMD5(username.toLowerCase() + new String(password));
			if (user.password.equalsIgnoreCase(md5)) {
				returnedUser = user;
			}
		} else if (user.password.equals(new String(password))) {
			// plain-text password
			returnedUser = user;
		}
		return returnedUser;
	}

	/**
	 * Logout a user.
	 * 
	 * @param user
	 */
	@Override
	public void logout(UserModel user) {	
	}

	/**
	 * Retrieve the user object for the specified username.
	 * 
	 * @param username
	 * @return a user object or null
	 */
	@Override
	public UserModel getUserModel(String username) {
		Properties allUsers = read();
		String userInfo = allUsers.getProperty(username.toLowerCase());
		if (userInfo == null) {
			return null;
		}
		UserModel model = new UserModel(username.toLowerCase());
		String[] userValues = userInfo.split(",");
		model.password = userValues[0];
		for (int i = 1; i < userValues.length; i++) {
			String role = userValues[i];
			switch (role.charAt(0)) {
			case '#':
				// Permissions
				if (role.equalsIgnoreCase(Constants.ADMIN_ROLE)) {
					model.canAdmin = true;
				} else if (role.equalsIgnoreCase(Constants.FORK_ROLE)) {
					model.canFork = true;
				} else if (role.equalsIgnoreCase(Constants.CREATE_ROLE)) {
					model.canCreate = true;
				} else if (role.equalsIgnoreCase(Constants.NOT_FEDERATED_ROLE)) {
					model.excludeFromFederation = true;
				}
				break;
			default:
				model.addRepositoryPermission(role);
			}
		}
		// set the teams for the user
		for (TeamModel team : teams.values()) {
			if (team.hasUser(username)) {
				model.teams.add(DeepCopier.copy(team));
			}
		}
		return model;
	}

	/**
	 * Updates/writes a complete user object.
	 * 
	 * @param model
	 * @return true if update is successful
	 */
	@Override
	public boolean updateUserModel(UserModel model) {
		return updateUserModel(model.username, model);
	}

	/**
	 * Updates/writes all specified user objects.
	 * 
	 * @param models a list of user models
	 * @return true if update is successful
	 * @since 1.2.0
	 */
	@Override
	public boolean updateUserModels(Collection<UserModel> models) {
		try {			
			Properties allUsers = read();
			for (UserModel model : models) {
				updateUserCache(allUsers, model.username, model);
			}
			write(allUsers);
			return true;
		} catch (Throwable t) {
			logger.error(MessageFormat.format("Failed to update {0} user models!", models.size()),
					t);
		}
		return false;
	}

	/**
	 * Updates/writes and replaces a complete user object keyed by username.
	 * This method allows for renaming a user.
	 * 
	 * @param username
	 *            the old username
	 * @param model
	 *            the user object to use for username
	 * @return true if update is successful
	 */
	@Override
	public boolean updateUserModel(String username, UserModel model) {
		try {			
			Properties allUsers = read();
			updateUserCache(allUsers, username, model);
			write(allUsers);
			return true;
		} catch (Throwable t) {
			logger.error(MessageFormat.format("Failed to update user model {0}!", model.username),
					t);
		}
		return false;
	}
	
	/**
	 * Updates/writes and replaces a complete user object keyed by username.
	 * This method allows for renaming a user.
	 * 
	 * @param username
	 *            the old username
	 * @param model
	 *            the user object to use for username
	 * @return true if update is successful
	 */
	private boolean updateUserCache(Properties allUsers, String username, UserModel model) {
		try {			
			UserModel oldUser = getUserModel(username);
			List<String> roles;
			if (model.permissions == null) {
				roles = new ArrayList<String>();
			} else {
				// discrete repository permissions
				roles = new ArrayList<String>();
				for (Map.Entry<String, AccessPermission> entry : model.permissions.entrySet()) {
					if (entry.getValue().exceeds(AccessPermission.NONE)) {
						// code:repository (e.g. RW+:~james/myrepo.git
						roles.add(entry.getValue().asRole(entry.getKey()));
					}
				}
			}

			// Permissions
			if (model.canAdmin) {
				roles.add(Constants.ADMIN_ROLE);
			}
			if (model.canFork) {
				roles.add(Constants.FORK_ROLE);
			}
			if (model.canCreate) {
				roles.add(Constants.CREATE_ROLE);
			}
			if (model.excludeFromFederation) {
				roles.add(Constants.NOT_FEDERATED_ROLE);
			}

			StringBuilder sb = new StringBuilder();
			if (!StringUtils.isEmpty(model.password)) {
				sb.append(model.password);
			}
			sb.append(',');
			for (String role : roles) {
				sb.append(role);
				sb.append(',');
			}
			// trim trailing comma
			sb.setLength(sb.length() - 1);
			allUsers.remove(username.toLowerCase());
			allUsers.put(model.username.toLowerCase(), sb.toString());

			// null check on "final" teams because JSON-sourced UserModel
			// can have a null teams object
			if (model.teams != null) {
				// update team cache
				for (TeamModel team : model.teams) {
					TeamModel t = getTeamModel(team.name);
					if (t == null) {
						// new team
						t = team;
					}
					t.removeUser(username);
					t.addUser(model.username);
					updateTeamCache(allUsers, t.name, t);
				}

				// check for implicit team removal
				if (oldUser != null) {
					for (TeamModel team : oldUser.teams) {
						if (!model.isTeamMember(team.name)) {
							team.removeUser(username);
							updateTeamCache(allUsers, team.name, team);
						}
					}
				}
			}
			return true;
		} catch (Throwable t) {
			logger.error(MessageFormat.format("Failed to update user model {0}!", model.username),
					t);
		}
		return false;
	}

	/**
	 * Deletes the user object from the user service.
	 * 
	 * @param model
	 * @return true if successful
	 */
	@Override
	public boolean deleteUserModel(UserModel model) {
		return deleteUser(model.username);
	}

	/**
	 * Delete the user object with the specified username
	 * 
	 * @param username
	 * @return true if successful
	 */
	@Override
	public boolean deleteUser(String username) {
		try {
			// Read realm file
			Properties allUsers = read();
			UserModel user = getUserModel(username);
			allUsers.remove(username);
			for (TeamModel team : user.teams) {
				TeamModel t = getTeamModel(team.name);
				if (t == null) {
					// new team
					t = team;
				}
				t.removeUser(username);
				updateTeamCache(allUsers, t.name, t);
			}
			write(allUsers);
			return true;
		} catch (Throwable t) {
			logger.error(MessageFormat.format("Failed to delete user {0}!", username), t);
		}
		return false;
	}

	/**
	 * Returns the list of all users available to the login service.
	 * 
	 * @return list of all usernames
	 */
	@Override
	public List<String> getAllUsernames() {
		Properties allUsers = read();
		List<String> list = new ArrayList<String>();
		for (String user : allUsers.stringPropertyNames()) {
			if (user.charAt(0) == '@') {
				// skip team user definitions
				continue;
			}
			list.add(user);
		}
		Collections.sort(list);
		return list;
	}

	/**
	 * Returns the list of all users available to the login service.
	 * 
	 * @return list of all usernames
	 */
	@Override
	public List<UserModel> getAllUsers() {
		read();
		List<UserModel> list = new ArrayList<UserModel>();
		for (String username : getAllUsernames()) {
			list.add(getUserModel(username));
		}
		Collections.sort(list);
		return list;
	}

	/**
	 * Returns the list of all users who are allowed to bypass the access
	 * restriction placed on the specified repository.
	 * 
	 * @param role
	 *            the repository name
	 * @return list of all usernames that can bypass the access restriction
	 */
	@Override
	public List<String> getUsernamesForRepositoryRole(String role) {
		List<String> list = new ArrayList<String>();
		try {
			Properties allUsers = read();
			for (String username : allUsers.stringPropertyNames()) {
				if (username.charAt(0) == '@') {
					continue;
				}
				String value = allUsers.getProperty(username);
				String[] values = value.split(",");
				// skip first value (password)
				for (int i = 1; i < values.length; i++) {
					String r = values[i];
					if (r.equalsIgnoreCase(role)) {
						list.add(username);
						break;
					}
				}
			}
		} catch (Throwable t) {
			logger.error(MessageFormat.format("Failed to get usernames for role {0}!", role), t);
		}
		Collections.sort(list);
		return list;
	}

	/**
	 * Sets the list of all users who are allowed to bypass the access
	 * restriction placed on the specified repository.
	 * 
	 * @param role
	 *            the repository name
	 * @param usernames
	 * @return true if successful
	 */
	@Override
	public boolean setUsernamesForRepositoryRole(String role, List<String> usernames) {
		try {
			Set<String> specifiedUsers = new HashSet<String>(usernames);
			Set<String> needsAddRole = new HashSet<String>(specifiedUsers);
			Set<String> needsRemoveRole = new HashSet<String>();

			// identify users which require add and remove role
			Properties allUsers = read();
			for (String username : allUsers.stringPropertyNames()) {
				String value = allUsers.getProperty(username);
				String[] values = value.split(",");
				// skip first value (password)
				for (int i = 1; i < values.length; i++) {
					String r = values[i];
					if (r.equalsIgnoreCase(role)) {
						// user has role, check against revised user list
						if (specifiedUsers.contains(username)) {
							needsAddRole.remove(username);
						} else {
							// remove role from user
							needsRemoveRole.add(username);
						}
						break;
					}
				}
			}

			// add roles to users
			for (String user : needsAddRole) {
				String userValues = allUsers.getProperty(user);
				userValues += "," + role;
				allUsers.put(user, userValues);
			}

			// remove role from user
			for (String user : needsRemoveRole) {
				String[] values = allUsers.getProperty(user).split(",");
				String password = values[0];
				StringBuilder sb = new StringBuilder();
				sb.append(password);
				sb.append(',');

				// skip first value (password)
				for (int i = 1; i < values.length; i++) {
					String value = values[i];
					if (!value.equalsIgnoreCase(role)) {
						sb.append(value);
						sb.append(',');
					}
				}
				sb.setLength(sb.length() - 1);

				// update properties
				allUsers.put(user, sb.toString());
			}

			// persist changes
			write(allUsers);
			return true;
		} catch (Throwable t) {
			logger.error(MessageFormat.format("Failed to set usernames for role {0}!", role), t);
		}
		return false;
	}

	/**
	 * Renames a repository role.
	 * 
	 * @param oldRole
	 * @param newRole
	 * @return true if successful
	 */
	@Override
	public boolean renameRepositoryRole(String oldRole, String newRole) {
		try {
			Properties allUsers = read();
			Set<String> needsRenameRole = new HashSet<String>();

			// identify users which require role rename
			for (String username : allUsers.stringPropertyNames()) {
				String value = allUsers.getProperty(username);
				String[] roles = value.split(",");
				// skip first value (password)
				for (int i = 1; i < roles.length; i++) {
					String repository = AccessPermission.repositoryFromRole(roles[i]);
					if (repository.equalsIgnoreCase(oldRole)) {
						needsRenameRole.add(username);
						break;
					}
				}
			}

			// rename role for identified users
			for (String user : needsRenameRole) {
				String userValues = allUsers.getProperty(user);
				String[] values = userValues.split(",");
				String password = values[0];
				StringBuilder sb = new StringBuilder();
				sb.append(password);
				sb.append(',');
				sb.append(newRole);
				sb.append(',');

				// skip first value (password)
				for (int i = 1; i < values.length; i++) {
					String repository = AccessPermission.repositoryFromRole(values[i]);
					if (repository.equalsIgnoreCase(oldRole)) {
						AccessPermission permission = AccessPermission.permissionFromRole(values[i]);
						sb.append(permission.asRole(newRole));
						sb.append(',');
					} else {
						sb.append(values[i]);
						sb.append(',');
					}
				}
				sb.setLength(sb.length() - 1);

				// update properties
				allUsers.put(user, sb.toString());
			}

			// persist changes
			write(allUsers);
			return true;
		} catch (Throwable t) {
			logger.error(
					MessageFormat.format("Failed to rename role {0} to {1}!", oldRole, newRole), t);
		}
		return false;
	}

	/**
	 * Removes a repository role from all users.
	 * 
	 * @param role
	 * @return true if successful
	 */
	@Override
	public boolean deleteRepositoryRole(String role) {
		try {
			Properties allUsers = read();
			Set<String> needsDeleteRole = new HashSet<String>();

			// identify users which require role rename
			for (String username : allUsers.stringPropertyNames()) {
				String value = allUsers.getProperty(username);
				String[] roles = value.split(",");
				// skip first value (password)
				for (int i = 1; i < roles.length; i++) {					
					String repository = AccessPermission.repositoryFromRole(roles[i]);
					if (repository.equalsIgnoreCase(role)) {
						needsDeleteRole.add(username);
						break;
					}
				}
			}

			// delete role for identified users
			for (String user : needsDeleteRole) {
				String userValues = allUsers.getProperty(user);
				String[] values = userValues.split(",");
				String password = values[0];
				StringBuilder sb = new StringBuilder();
				sb.append(password);
				sb.append(',');
				// skip first value (password)
				for (int i = 1; i < values.length; i++) {					
					String repository = AccessPermission.repositoryFromRole(values[i]);
					if (!repository.equalsIgnoreCase(role)) {
						sb.append(values[i]);
						sb.append(',');
					}
				}
				sb.setLength(sb.length() - 1);

				// update properties
				allUsers.put(user, sb.toString());
			}

			// persist changes
			write(allUsers);
			return true;
		} catch (Throwable t) {
			logger.error(MessageFormat.format("Failed to delete role {0}!", role), t);
		}
		return false;
	}

	/**
	 * Writes the properties file.
	 * 
	 * @param properties
	 * @throws IOException
	 */
	private void write(Properties properties) throws IOException {
		// Write a temporary copy of the users file
		File realmFileCopy = new File(propertiesFile.getAbsolutePath() + ".tmp");
		FileWriter writer = new FileWriter(realmFileCopy);
		properties
				.store(writer,
						" Gitblit realm file format:\n   username=password,\\#permission,repository1,repository2...\n   @teamname=!username1,!username2,!username3,repository1,repository2...");
		writer.close();
		// If the write is successful, delete the current file and rename
		// the temporary copy to the original filename.
		if (realmFileCopy.exists() && realmFileCopy.length() > 0) {
			if (propertiesFile.exists()) {
				if (!propertiesFile.delete()) {
					throw new IOException(MessageFormat.format("Failed to delete {0}!",
							propertiesFile.getAbsolutePath()));
				}
			}
			if (!realmFileCopy.renameTo(propertiesFile)) {
				throw new IOException(MessageFormat.format("Failed to rename {0} to {1}!",
						realmFileCopy.getAbsolutePath(), propertiesFile.getAbsolutePath()));
			}
		} else {
			throw new IOException(MessageFormat.format("Failed to save {0}!",
					realmFileCopy.getAbsolutePath()));
		}
	}

	/**
	 * Reads the properties file and rebuilds the in-memory cookie lookup table.
	 */
	@Override
	protected synchronized Properties read() {
		long lastRead = lastModified();
		boolean reload = forceReload();
		Properties allUsers = super.read();
		if (reload || (lastRead != lastModified())) {
			// reload hash cache
			cookies.clear();
			teams.clear();

			for (String username : allUsers.stringPropertyNames()) {
				String value = allUsers.getProperty(username);
				String[] roles = value.split(",");
				if (username.charAt(0) == '@') {
					// team definition
					TeamModel team = new TeamModel(username.substring(1));
					List<String> repositories = new ArrayList<String>();
					List<String> users = new ArrayList<String>();
					List<String> mailingLists = new ArrayList<String>();
					List<String> preReceive = new ArrayList<String>();
					List<String> postReceive = new ArrayList<String>();
					for (String role : roles) {
						if (role.charAt(0) == '!') {
							users.add(role.substring(1));
						} else if (role.charAt(0) == '&') {
							mailingLists.add(role.substring(1));
						} else if (role.charAt(0) == '^') {
							preReceive.add(role.substring(1));
						} else if (role.charAt(0) == '%') {
							postReceive.add(role.substring(1));
						} else {
							switch (role.charAt(0)) {
							case '#':
								// Permissions
								if (role.equalsIgnoreCase(Constants.ADMIN_ROLE)) {
									team.canAdmin = true;
								} else if (role.equalsIgnoreCase(Constants.FORK_ROLE)) {
									team.canFork = true;
								} else if (role.equalsIgnoreCase(Constants.CREATE_ROLE)) {
									team.canCreate = true;
								}
								break;
							default:
								repositories.add(role);
							}
							repositories.add(role);
						}
					}
					if (!team.canAdmin) {
						// only read permissions for non-admin teams
						team.addRepositoryPermissions(repositories);
					}
					team.addUsers(users);
					team.addMailingLists(mailingLists);
					team.preReceiveScripts.addAll(preReceive);
					team.postReceiveScripts.addAll(postReceive);
					teams.put(team.name.toLowerCase(), team);
				} else {
					// user definition
					String password = roles[0];
					cookies.put(StringUtils.getSHA1(username.toLowerCase() + password), username.toLowerCase());
				}
			}
		}
		return allUsers;
	}

	@Override
	public String toString() {
		return getClass().getSimpleName() + "(" + propertiesFile.getAbsolutePath() + ")";
	}

	/**
	 * Returns the list of all teams available to the login service.
	 * 
	 * @return list of all teams
	 * @since 0.8.0
	 */
	@Override
	public List<String> getAllTeamNames() {
		List<String> list = new ArrayList<String>(teams.keySet());
		Collections.sort(list);
		return list;
	}

	/**
	 * Returns the list of all teams available to the login service.
	 * 
	 * @return list of all teams
	 * @since 0.8.0
	 */
	@Override
	public List<TeamModel> getAllTeams() {
		List<TeamModel> list = new ArrayList<TeamModel>(teams.values());
		list = DeepCopier.copy(list);
		Collections.sort(list);
		return list;
	}

	/**
	 * Returns the list of all teams who are allowed to bypass the access
	 * restriction placed on the specified repository.
	 * 
	 * @param role
	 *            the repository name
	 * @return list of all teamnames that can bypass the access restriction
	 */
	@Override
	public List<String> getTeamnamesForRepositoryRole(String role) {
		List<String> list = new ArrayList<String>();
		try {
			Properties allUsers = read();
			for (String team : allUsers.stringPropertyNames()) {
				if (team.charAt(0) != '@') {
					// skip users
					continue;
				}
				String value = allUsers.getProperty(team);
				String[] values = value.split(",");
				for (int i = 0; i < values.length; i++) {
					String r = values[i];
					if (r.equalsIgnoreCase(role)) {
						// strip leading @
						list.add(team.substring(1));
						break;
					}
				}
			}
		} catch (Throwable t) {
			logger.error(MessageFormat.format("Failed to get teamnames for role {0}!", role), t);
		}
		Collections.sort(list);
		return list;
	}

	/**
	 * Sets the list of all teams who are allowed to bypass the access
	 * restriction placed on the specified repository.
	 * 
	 * @param role
	 *            the repository name
	 * @param teamnames
	 * @return true if successful
	 */
	@Override
	public boolean setTeamnamesForRepositoryRole(String role, List<String> teamnames) {
		try {
			Set<String> specifiedTeams = new HashSet<String>(teamnames);
			Set<String> needsAddRole = new HashSet<String>(specifiedTeams);
			Set<String> needsRemoveRole = new HashSet<String>();

			// identify teams which require add and remove role
			Properties allUsers = read();
			for (String team : allUsers.stringPropertyNames()) {
				if (team.charAt(0) != '@') {
					// skip users
					continue;
				}
				String name = team.substring(1);
				String value = allUsers.getProperty(team);
				String[] values = value.split(",");
				for (int i = 0; i < values.length; i++) {
					String r = values[i];
					if (r.equalsIgnoreCase(role)) {
						// team has role, check against revised team list
						if (specifiedTeams.contains(name)) {
							needsAddRole.remove(name);
						} else {
							// remove role from team
							needsRemoveRole.add(name);
						}
						break;
					}
				}
			}

			// add roles to teams
			for (String name : needsAddRole) {
				String team = "@" + name;
				String teamValues = allUsers.getProperty(team);
				teamValues += "," + role;
				allUsers.put(team, teamValues);
			}

			// remove role from team
			for (String name : needsRemoveRole) {
				String team = "@" + name;
				String[] values = allUsers.getProperty(team).split(",");
				StringBuilder sb = new StringBuilder();
				for (int i = 0; i < values.length; i++) {
					String value = values[i];
					if (!value.equalsIgnoreCase(role)) {
						sb.append(value);
						sb.append(',');
					}
				}
				sb.setLength(sb.length() - 1);

				// update properties
				allUsers.put(team, sb.toString());
			}

			// persist changes
			write(allUsers);
			return true;
		} catch (Throwable t) {
			logger.error(MessageFormat.format("Failed to set teamnames for role {0}!", role), t);
		}
		return false;
	}

	/**
	 * Retrieve the team object for the specified team name.
	 * 
	 * @param teamname
	 * @return a team object or null
	 * @since 0.8.0
	 */
	@Override
	public TeamModel getTeamModel(String teamname) {
		read();
		TeamModel team = teams.get(teamname.toLowerCase());
		if (team != null) {
			// clone the model, otherwise all changes to this object are
			// live and unpersisted
			team = DeepCopier.copy(team);
		}
		return team;
	}

	/**
	 * Updates/writes a complete team object.
	 * 
	 * @param model
	 * @return true if update is successful
	 * @since 0.8.0
	 */
	@Override
	public boolean updateTeamModel(TeamModel model) {
		return updateTeamModel(model.name, model);
	}
	
	/**
	 * Updates/writes all specified team objects.
	 * 
	 * @param models a list of team models
	 * @return true if update is successful
	 * @since 1.2.0
	 */
	public boolean updateTeamModels(Collection<TeamModel> models) {
		try {
			Properties allUsers = read();
			for (TeamModel model : models) {
				updateTeamCache(allUsers, model.name, model);
			}
			write(allUsers);
			return true;
		} catch (Throwable t) {
			logger.error(MessageFormat.format("Failed to update {0} team models!", models.size()), t);
		}
		return false;
	}

	/**
	 * Updates/writes and replaces a complete team object keyed by teamname.
	 * This method allows for renaming a team.
	 * 
	 * @param teamname
	 *            the old teamname
	 * @param model
	 *            the team object to use for teamname
	 * @return true if update is successful
	 * @since 0.8.0
	 */
	@Override
	public boolean updateTeamModel(String teamname, TeamModel model) {
		try {
			Properties allUsers = read();
			updateTeamCache(allUsers, teamname, model);
			write(allUsers);
			return true;
		} catch (Throwable t) {
			logger.error(MessageFormat.format("Failed to update team model {0}!", model.name), t);
		}
		return false;
	}

	private void updateTeamCache(Properties allUsers, String teamname, TeamModel model) {
		StringBuilder sb = new StringBuilder();
		List<String> roles;
		if (model.permissions == null) {
			// legacy, use repository list
			if (model.repositories != null) {
				roles = new ArrayList<String>(model.repositories);
			} else {
				roles = new ArrayList<String>();
			}
		} else {
			// discrete repository permissions
			roles = new ArrayList<String>();
			for (Map.Entry<String, AccessPermission> entry : model.permissions.entrySet()) {
				if (entry.getValue().exceeds(AccessPermission.NONE)) {
					// code:repository (e.g. RW+:~james/myrepo.git
					roles.add(entry.getValue().asRole(entry.getKey()));
				}
			}
		}
		
		// Permissions
		if (model.canAdmin) {
			roles.add(Constants.ADMIN_ROLE);
		}
		if (model.canFork) {
			roles.add(Constants.FORK_ROLE);
		}
		if (model.canCreate) {
			roles.add(Constants.CREATE_ROLE);
		}

		for (String role : roles) {
				sb.append(role);
				sb.append(',');
		}
		
		if (!ArrayUtils.isEmpty(model.users)) {
			for (String user : model.users) {
				sb.append('!');
				sb.append(user);
				sb.append(',');
			}
		}
		if (!ArrayUtils.isEmpty(model.mailingLists)) {
			for (String address : model.mailingLists) {
				sb.append('&');
				sb.append(address);
				sb.append(',');
			}
		}
		if (!ArrayUtils.isEmpty(model.preReceiveScripts)) {
			for (String script : model.preReceiveScripts) {
				sb.append('^');
				sb.append(script);
				sb.append(',');
			}
		}
		if (!ArrayUtils.isEmpty(model.postReceiveScripts)) {
			for (String script : model.postReceiveScripts) {
				sb.append('%');
				sb.append(script);
				sb.append(',');
			}
		}
		// trim trailing comma
		sb.setLength(sb.length() - 1);
		allUsers.remove("@" + teamname);
		allUsers.put("@" + model.name, sb.toString());

		// update team cache
		teams.remove(teamname.toLowerCase());
		teams.put(model.name.toLowerCase(), model);
	}

	/**
	 * Deletes the team object from the user service.
	 * 
	 * @param model
	 * @return true if successful
	 * @since 0.8.0
	 */
	@Override
	public boolean deleteTeamModel(TeamModel model) {
		return deleteTeam(model.name);
	}

	/**
	 * Delete the team object with the specified teamname
	 * 
	 * @param teamname
	 * @return true if successful
	 * @since 0.8.0
	 */
	@Override
	public boolean deleteTeam(String teamname) {
		Properties allUsers = read();
		teams.remove(teamname.toLowerCase());
		allUsers.remove("@" + teamname);
		try {
			write(allUsers);
			return true;
		} catch (Throwable t) {
			logger.error(MessageFormat.format("Failed to delete team {0}!", teamname), t);
		}
		return false;
	}
}
