public Cluster(ClusterStatus status, Cluster copyFrom)
     : this(copyFrom.InternalName,
         status,
         copyFrom.AppCount,
         copyFrom.ServiceCount,
         copyFrom.Address,
         new List<int>(copyFrom.Ports),
         new List<ClusterUser>(copyFrom.Users),
         copyFrom.CreatedOn)
 {
 }
        private async Task<Cluster> ProcessDeletingClusterAsync(Cluster cluster)
        {
            ClusterOperationStatus deleteStatus = await this.clusterOperator.GetClusterStatusAsync(cluster.InternalName);

            switch (deleteStatus)
            {
                case ClusterOperationStatus.Creating:
                case ClusterOperationStatus.Ready:
                    return new Cluster(ClusterStatus.Remove, cluster); // hopefully shouldn't ever get here

                case ClusterOperationStatus.Deleting:
                    return cluster;

                case ClusterOperationStatus.ClusterNotFound:
                    ServiceEventSource.Current.ServiceMessage(this, "Cluster successfully deleted: {0}.", cluster.Address);
                    return new Cluster(ClusterStatus.Deleted, cluster);

                case ClusterOperationStatus.CreateFailed:
                case ClusterOperationStatus.DeleteFailed:
                    ServiceEventSource.Current.ServiceMessage(this, "Cluster failed to delete: {0}.", cluster.Address);
                    return new Cluster(ClusterStatus.Remove, cluster);
            }

            return cluster;
        }
        private async Task<Cluster> ProcessReadyClusterAsync(Cluster cluster)
        {
            if (DateTimeOffset.UtcNow - cluster.CreatedOn.ToUniversalTime() >= this.config.MaximumClusterUptime)
            {
                ServiceEventSource.Current.ServiceMessage(this, "Cluster expired: {0}", cluster.Address);
                return new Cluster(ClusterStatus.Remove, cluster);
            }

            ClusterOperationStatus readyStatus = await this.clusterOperator.GetClusterStatusAsync(cluster.InternalName);
            switch (readyStatus)
            {
                case ClusterOperationStatus.Deleting:
                    return new Cluster(ClusterStatus.Deleting, cluster);
            }

            try
            {
                int deployedApplications = await this.applicationDeployService.GetApplicationCountAsync(cluster.Address, ClusterConnectionPort);
                int deployedServices = await this.applicationDeployService.GetServiceCountAsync(cluster.Address, ClusterConnectionPort);

                return new Cluster(
                    cluster.InternalName,
                    cluster.Status,
                    deployedApplications,
                    deployedServices,
                    cluster.Address,
                    cluster.Ports,
                    cluster.Users,
                    cluster.CreatedOn);
            }
            catch (Exception e)
            {
                ServiceEventSource.Current.ServiceMessage(this, "Unable to determine application and service count. Cluster: {0}. Error: {1}",
                    cluster.Address,
                    e.GetActualMessage());
            }

            return cluster;
        }
        private async Task<Cluster> ProcessRemoveClusterAsync(Cluster cluster)
        {
            ClusterOperationStatus removeStatus = await this.clusterOperator.GetClusterStatusAsync(cluster.InternalName);
            switch (removeStatus)
            {
                case ClusterOperationStatus.Creating:
                case ClusterOperationStatus.Ready:
                case ClusterOperationStatus.CreateFailed:
                case ClusterOperationStatus.DeleteFailed:
                    ServiceEventSource.Current.ServiceMessage(this, "Deleting cluster {0}.", cluster.Address);
                    await this.clusterOperator.DeleteClusterAsync(cluster.InternalName);
                    return new Cluster(ClusterStatus.Deleting, cluster);

                case ClusterOperationStatus.Deleting:
                    return new Cluster(ClusterStatus.Deleting, cluster);

                case ClusterOperationStatus.ClusterNotFound:
                    return new Cluster(ClusterStatus.Deleted, cluster);
            }

            return cluster;
        }
        private async Task<Cluster> ProcessCreatingClusterAsync(Cluster cluster)
        {
            ClusterOperationStatus creatingStatus = await this.clusterOperator.GetClusterStatusAsync(cluster.InternalName);
            switch (creatingStatus)
            {
                case ClusterOperationStatus.Creating:
                    return cluster;

                case ClusterOperationStatus.Ready:
                    IEnumerable<int> ports = await this.clusterOperator.GetClusterPortsAsync(cluster.InternalName);

                    try
                    {
                        await this.applicationDeployService.QueueApplicationDeploymentAsync(cluster.Address, ClusterConnectionPort);
                    }
                    catch (Exception e)
                    {
                        // couldn't queue samples for deployment, but that shouldn't prevent starting the cluster.
                        ServiceEventSource.Current.ServiceMessage(
                            this,
                            "Failed to queue sample deployment. Cluster: {0} Error: {1}",
                            cluster.Address,
                            e.GetActualMessage());
                    }

                    ServiceEventSource.Current.ServiceMessage(
                        this,
                        "Cluster is ready: {0} with ports: {1}",
                        cluster.Address,
                        String.Join(",", cluster.Ports));

                    return new Cluster(
                        cluster.InternalName,
                        ClusterStatus.Ready,
                        cluster.AppCount,
                        cluster.ServiceCount,
                        cluster.Address,
                        new List<int>(ports),
                        new List<ClusterUser>(cluster.Users),
                        DateTimeOffset.UtcNow);

                case ClusterOperationStatus.CreateFailed:
                    ServiceEventSource.Current.ServiceMessage(this, "Cluster failed to create: {0}", cluster.Address);
                    return new Cluster(ClusterStatus.Remove, cluster);

                case ClusterOperationStatus.Deleting:
                    return new Cluster(ClusterStatus.Deleting, cluster);

                default:
                    return cluster;
            }
        }
        private async Task<Cluster> ProcessNewClusterAsync(Cluster cluster)
        {
            try
            {
                string address = await this.clusterOperator.CreateClusterAsync(cluster.InternalName);

                ServiceEventSource.Current.ServiceMessage(this, "Creating cluster: {0}", address);

                return new Cluster(
                    cluster.InternalName,
                    ClusterStatus.Creating,
                    cluster.AppCount,
                    cluster.ServiceCount,
                    address,
                    new List<int>(cluster.Ports),
                    new List<ClusterUser>(cluster.Users),
                    cluster.CreatedOn);
            }
            catch (InvalidOperationException e)
            {
                // cluster with this name might already exist, so remove this one.
                ServiceEventSource.Current.ServiceMessage(this, "Cluster failed to create: {0}. {1}", cluster.Address, e.Message);

                // mark as deleted so it gets removed from the list.
                return new Cluster(ClusterStatus.Deleted, cluster);
            }
        }
        /// <summary>
        /// Processes a cluster based on its current state.
        /// </summary>
        /// <returns></returns>
        internal Task<Cluster> ProcessClusterStatusAsync(Cluster cluster)
        {
            switch (cluster.Status)
            {
                case ClusterStatus.New:
                    return this.ProcessNewClusterAsync(cluster);

                case ClusterStatus.Creating:
                    return this.ProcessCreatingClusterAsync(cluster);

                case ClusterStatus.Ready:
                    return this.ProcessReadyClusterAsync(cluster);

                case ClusterStatus.Remove:
                    return this.ProcessRemoveClusterAsync(cluster);

                case ClusterStatus.Deleting:
                    return this.ProcessDeletingClusterAsync(cluster);

                default:
                    return Task.FromResult(cluster);
            }
        }
        /// <summary>
        /// Processes a request to join a cluster. 
        /// </summary>
        /// <param name="clusterId"></param>
        /// <param name="user"></param>
        /// <returns></returns>
        public async Task JoinClusterAsync(int clusterId, string userEmail)
        {
            if (String.IsNullOrWhiteSpace(userEmail))
            {
                throw new ArgumentNullException("userEmail");
            }

            ServiceEventSource.Current.ServiceMessage(this, "Join cluster request. Cluster: {0}.", clusterId);

            IReliableDictionary<int, Cluster> clusterDictionary =
                await this.StateManager.GetOrAddAsync<IReliableDictionary<int, Cluster>>(ClusterDictionaryName);

            foreach (KeyValuePair<int, Cluster> item in clusterDictionary)
            {
                if (item.Value.Users.Any(x => String.Equals(x.Email, userEmail, StringComparison.OrdinalIgnoreCase)))
                {
                    ServiceEventSource.Current.ServiceMessage(
                        this,
                        "Join cluster request failed. User already exists on cluster: {0}.",
                        item.Key);

                    throw new JoinClusterFailedException(JoinClusterFailedReason.UserAlreadyJoined);
                }
            }

            using (ITransaction tx = this.StateManager.CreateTransaction())
            {
                ConditionalResult<Cluster> result = await clusterDictionary.TryGetValueAsync(tx, clusterId, LockMode.Update);

                if (!result.HasValue)
                {
                    ServiceEventSource.Current.ServiceMessage(
                        this,
                        "Join cluster request failed. Cluster does not exist. Cluster ID: {0}.",
                        clusterId);

                    throw new JoinClusterFailedException(JoinClusterFailedReason.ClusterDoesNotExist);
                }

                Cluster cluster = result.Value;

                // make sure the cluster isn't about to be deleted.
                if ((DateTimeOffset.UtcNow - cluster.CreatedOn.ToUniversalTime()) > (this.config.MaximumClusterUptime))
                {
                    ServiceEventSource.Current.ServiceMessage(
                        this,
                        "Join cluster request failed. Cluster has expired. Cluster: {0}. Cluster creation time: {1}",
                        clusterId,
                        cluster.CreatedOn.ToUniversalTime());

                    throw new JoinClusterFailedException(JoinClusterFailedReason.ClusterExpired);
                }

                // make sure the cluster is ready
                if (cluster.Status != ClusterStatus.Ready)
                {
                    ServiceEventSource.Current.ServiceMessage(
                        this,
                        "Join cluster request failed. Cluster is not ready. Cluster: {0}. Status: {1}",
                        clusterId,
                        cluster.Status);

                    throw new JoinClusterFailedException(JoinClusterFailedReason.ClusterNotReady);
                }

                if (cluster.Users.Count() >= this.config.MaximumUsersPerCluster)
                {
                    ServiceEventSource.Current.ServiceMessage(
                        this,
                        "Join cluster request failed. Cluster is full. Cluster: {0}. Users: {1}",
                        clusterId,
                        cluster.Users.Count());

                    throw new JoinClusterFailedException(JoinClusterFailedReason.ClusterFull);
                }

                int userPort;
                string clusterAddress = cluster.Address;
                TimeSpan clusterTimeRemaining = this.config.MaximumClusterUptime - (DateTimeOffset.UtcNow - cluster.CreatedOn);
                DateTimeOffset clusterExpiration = cluster.CreatedOn + this.config.MaximumClusterUptime;

                try
                {
                    userPort = cluster.Ports.First(port => !cluster.Users.Any(x => x.Port == port));
                }
                catch (InvalidOperationException)
                {
                    ServiceEventSource.Current.ServiceMessage(
                        this,
                        "Join cluster request failed. No available ports. Cluster: {0}. Users: {1}. Ports: {2}",
                        clusterId,
                        cluster.Users.Count(),
                        cluster.Ports.Count());

                    throw new JoinClusterFailedException(JoinClusterFailedReason.NoPortsAvailable);
                }

                try
                {
                    ServiceEventSource.Current.ServiceMessage(this, "Sending join mail. Cluster: {0}.", clusterId);
                    List<HyperlinkView> links = new List<HyperlinkView>();
                    links.Add(new HyperlinkView("http://" + clusterAddress + ":" + ClusterHttpGatewayPort + "/Explorer/index.html", "Service Fabric Explorer", "explore what's on the cluster with the built-in Service Fabric Explorer."));

                    try
                    {
                        IEnumerable<ApplicationView> applications = await this.applicationDeployService.GetApplicationDeploymentsAsync(cluster.Address, ClusterConnectionPort);
                        links.AddRange(applications.Select(x => x.EntryServiceInfo));
                    }
                    catch(Exception e)
                    {
                        ServiceEventSource.Current.ServiceMessage(this, "Failed to get application deployment info. {0}.", e.GetActualMessage());
                    }
                    
                    await this.mailer.SendJoinMail(
                        userEmail,
                        clusterAddress + ":" + ClusterConnectionPort,
                        userPort,
                        clusterTimeRemaining,
                        clusterExpiration, 
                        links);
                }
                catch (Exception e)
                {
                    ServiceEventSource.Current.ServiceMessage(this, "Failed to send join mail. {0}.", e.GetActualMessage());

                    throw new JoinClusterFailedException(JoinClusterFailedReason.SendMailFailed);
                }

                List<ClusterUser> newUserList = new List<ClusterUser>(cluster.Users);
                newUserList.Add(new ClusterUser(userEmail, userPort));

                Cluster updatedCluster = new Cluster(
                    cluster.InternalName,
                    cluster.Status,
                    cluster.AppCount,
                    cluster.ServiceCount,
                    cluster.Address,
                    cluster.Ports,
                    newUserList,
                    cluster.CreatedOn);

                await clusterDictionary.SetAsync(tx, clusterId, updatedCluster);
                await tx.CommitAsync();
            }

            ServiceEventSource.Current.ServiceMessage(this, "Join cluster request completed. Cluster: {0}.", clusterId);
        }
        /// <summary>
        /// Processes a cluster based on its current state.
        /// </summary>
        /// <returns></returns>
        internal async Task ProcessClusterStatusAsync(Cluster cluster)
        {
            switch (cluster.Status)
            {
                case ClusterStatus.New:
                    Random random = new Random();
                    cluster.Address = await this.clusterOperator.CreateClusterAsync(random.Next().ToString());
                    cluster.Status = ClusterStatus.Creating;
                    break;

                case ClusterStatus.Creating:
                    ClusterOperationStatus creatingStatus = await this.clusterOperator.GetClusterStatusAsync(cluster.Address);
                    switch (creatingStatus)
                    {
                        case ClusterOperationStatus.Creating:
                            // still creating
                            break;
                        case ClusterOperationStatus.Ready:
                            cluster.Ports = await this.clusterOperator.GetClusterPortsAsync(cluster.Address);
                            cluster.CreatedOn = DateTimeOffset.UtcNow;
                            cluster.Status = ClusterStatus.Ready;
                            break;
                        case ClusterOperationStatus.CreateFailed:
                            cluster.Status = ClusterStatus.New;
                            break;
                        case ClusterOperationStatus.Deleting:
                            cluster.Status = ClusterStatus.Deleting;
                            break;
                    }
                    break;

                case ClusterStatus.Ready:
                    if (DateTimeOffset.UtcNow - cluster.CreatedOn.ToUniversalTime() >= this.Config.MaxClusterUptime)
                    {
                        await this.clusterOperator.DeleteClusterAsync(cluster.Address);
                        cluster.Status = ClusterStatus.Deleting;
                    }

                    ClusterOperationStatus readyStatus = await this.clusterOperator.GetClusterStatusAsync(cluster.Address);
                    switch (readyStatus)
                    {
                        case ClusterOperationStatus.Deleting:
                            cluster.Status = ClusterStatus.Deleting;
                            break;
                    }

                    //TODO: update application and service count
                    break;

                case ClusterStatus.Remove:
                    ClusterOperationStatus removeStatus = await this.clusterOperator.GetClusterStatusAsync(cluster.Address);
                    switch (removeStatus)
                    {
                        case ClusterOperationStatus.Creating:
                        case ClusterOperationStatus.Ready:
                        case ClusterOperationStatus.CreateFailed:
                        case ClusterOperationStatus.DeleteFailed:
                            await this.clusterOperator.DeleteClusterAsync(cluster.Address);
                            cluster.Status = ClusterStatus.Deleting;
                            break;
                        case ClusterOperationStatus.Deleting:
                            cluster.Status = ClusterStatus.Deleting;
                            break;
                    }
                    break;

                case ClusterStatus.Deleting:
                    ClusterOperationStatus deleteStatus = await this.clusterOperator.GetClusterStatusAsync(cluster.Address);
                    switch (deleteStatus)
                    {
                        case ClusterOperationStatus.Creating:
                        case ClusterOperationStatus.Ready:
                            await this.clusterOperator.DeleteClusterAsync(cluster.Address);
                            break;
                        case ClusterOperationStatus.Deleting:
                            break; // still in progress
                        case ClusterOperationStatus.ClusterNotFound:
                            cluster.Status = ClusterStatus.Deleted;
                            break;
                        case ClusterOperationStatus.CreateFailed:
                        case ClusterOperationStatus.DeleteFailed:
                            cluster.Status = ClusterStatus.Remove;
                            break;
                    }
                    break;

            }
        }