protected void UpdateStateOfExternalItem(Card card, List<string> states, BoardMapping mapping, bool runOnlyOnce)
		{
			if (string.IsNullOrEmpty(card.ExternalSystemName) ||
			    !card.ExternalSystemName.Equals(ServiceName, StringComparison.OrdinalIgnoreCase))
				return;

			if (string.IsNullOrEmpty(card.ExternalCardID))
			{
				Log.Debug("Ignoring card [{0}] with missing external id value.", card.Id);
				return;
			}


			if (states == null || states.Count == 0)
				return;

			int tries = 0;
			bool success = false;
			while (tries < 10 && !success && (!runOnlyOnce || tries == 0))
			{
				if (tries > 0)
				{
					Log.Warn(string.Format("Attempting to update external work item [{0}] attempt number [{1}]",
						card.ExternalCardID,
						tries));
					// wait 5 seconds before trying again
					Thread.Sleep(new TimeSpan(0, 0, 5));
				}

				//https://yoursite.atlassian.net/rest/api/latest/issue/{issueIdOrKey}
				var request = CreateRequest(string.Format("rest/api/latest/issue/{0}", card.ExternalCardID), Method.GET);
				var jiraResp = ExecuteRequest(request);

				if (jiraResp.StatusCode != HttpStatusCode.OK)
				{
					var serializer = new JsonSerializer<ErrorMessage>();
					var errorMessage = serializer.DeserializeFromString(jiraResp.Content);
					Log.Error(
						string.Format(
							"Unable to get issues from Jira, Error: {0}. Check your board/repo mapping configuration.",
							errorMessage.Message));
				}
				else
				{
					var issueToUpdate = new JsonSerializer<Issue>().DeserializeFromString(jiraResp.Content);

					// Check for a workflow mapping to the closed state
					if (states != null && states.Count > 0 && states[0].Contains(">"))
					{
						var workflowStates = states[0].Split('>');

						// check to see if the workitem is already in one of the workflow states
						var alreadyInState =
							workflowStates.FirstOrDefault(
								x => x.Trim().ToLowerInvariant() == issueToUpdate.Fields.Status.Name.ToLowerInvariant());
						if (!string.IsNullOrEmpty(alreadyInState))
						{
							// change workflowStates to only use the states after the currently set state
							var currentIndex = Array.IndexOf(workflowStates, alreadyInState);
							if (currentIndex < workflowStates.Length - 1)
							{
								var updatedWorkflowStates = new List<string>();
								for (int i = currentIndex + 1; i < workflowStates.Length; i++)
								{
									updatedWorkflowStates.Add(workflowStates[i]);
								}
								workflowStates = updatedWorkflowStates.ToArray();
							}
						}
						if (workflowStates.Length > 0)
						{
							foreach (string workflowState in workflowStates)
							{
								UpdateStateOfExternalItem(card, new List<string> {workflowState.Trim()}, mapping,
									runOnlyOnce);
							}
							return;
						}
					}

					foreach (var state in states)
					{
						if (issueToUpdate.Fields.Status.Name.ToLowerInvariant() == state.ToLowerInvariant())
						{
							Log.Debug(string.Format("Issue [{0}] is already in state [{1}]", issueToUpdate.Key, state));
							return;
						}
					}

					try
					{
						// first get a list of available transitions
						var transitionsRequest =
							CreateRequest(
								string.Format("rest/api/2/issue/{0}/transitions?expand=transitions.fields",
									card.ExternalCardID), Method.GET);
						var transitionsResponse = ExecuteRequest(transitionsRequest);

						if (transitionsResponse.StatusCode != HttpStatusCode.OK)
						{
							var serializer = new JsonSerializer<ErrorMessage>();
							var errorMessage = serializer.DeserializeFromString(jiraResp.Content);
							Log.Error(string.Format("Unable to get available transitions from Jira, Error: {0}.",
								errorMessage.Message));
						}
						else
						{
							var availableTransitions =
								new JsonSerializer<TransitionsResponse>().DeserializeFromString(
									transitionsResponse.Content);

							if (availableTransitions != null &&
							    availableTransitions.Transitions != null &&
							    availableTransitions.Transitions.Any())
							{
								// now find match from available transitions to states
								var valid = false;
								Transition validTransition = null;
								foreach (var st in states)
								{
									validTransition = availableTransitions.Transitions.FirstOrDefault(
										x =>
											x.Name.ToLowerInvariant() == st.ToLowerInvariant() ||
											x.To.Name.ToLowerInvariant() == st.ToLowerInvariant());
									if (validTransition != null)
									{
										// if you find one then set it
										valid = true;
										break;
									}
								}

								if (!valid)
								{
									// if not then write an error message
									Log.Error(
										string.Format(
											"Unable to update Issue [{0}] to [{1}] because the status transition is invalid. Try adding additional states to the config.",
											card.ExternalCardID, states.Join(",")));
								}
								else
								{
									// go ahead and try to update the state of the issue in JIRA
									//https://yoursite.atlassian.net/rest/api/latest/issue/{issueIdOrKey}/transitions?expand=transitions.fields
									var updateRequest =
										CreateRequest(
											string.Format(
												"rest/api/latest/issue/{0}/transitions?expand=transitions.fields",
												card.ExternalCardID), Method.POST);
									updateRequest.AddParameter("application/json",
										"{ \"transition\": { \"id\": \"" + validTransition.Id + "\"}}",
										ParameterType.RequestBody);
									var resp = ExecuteRequest(updateRequest);

									if (resp.StatusCode != HttpStatusCode.OK &&
									    resp.StatusCode != HttpStatusCode.NoContent)
									{
										var serializer = new JsonSerializer<ErrorMessage>();
										var errorMessage = serializer.DeserializeFromString(resp.Content);
										Log.Error(
											string.Format(
												"Unable to update Issue [{0}] to [{1}], Description: {2}, Message: {3}",
												card.ExternalCardID, validTransition.To.Name, resp.StatusDescription,
												errorMessage.Message));
									}
									else
									{
										success = true;
										Log.Debug(String.Format("Updated state for Issue [{0}] to [{1}]",
											card.ExternalCardID, validTransition.To.Name));
									}
								}
							}
							else
							{
								Log.Error(
									string.Format(
										"Unable to update Issue [{0}] to [{1}] because no transitions were available from its current status [{2}]. The user account you are using to connect may not have proper privileges.",
										card.ExternalCardID, states.Join(","), issueToUpdate.Fields.Status.Name));
							}
						}
					}
					catch (Exception ex)
					{
						Log.Error(string.Format("Unable to update Issue [{0}] to [{1}], Exception: {2}",
							card.ExternalCardID, states.Join(","), ex.Message));
					}
				}
				tries++;
			}
		}
		protected override void CardUpdated(Card updatedCard, List<string> updatedItems, BoardMapping boardMapping)
		{
			if (!updatedCard.ExternalSystemName.Equals(ServiceName, StringComparison.OrdinalIgnoreCase))
				return;

			if (string.IsNullOrEmpty(updatedCard.ExternalCardID))
				return;

			if (string.IsNullOrEmpty(updatedCard.ExternalCardID))
			{
				Log.Debug("Ignoring card [{0}] with missing external id value.", updatedCard.ExternalCardID);
				return;
			}

			//https://yoursite.atlassian.net/rest/api/latest/issue/{issueIdOrKey}
			var request = CreateRequest(string.Format("rest/api/latest/issue/{0}", updatedCard.ExternalCardID),
				Method.GET);
			var jiraResp = ExecuteRequest(request);

			if (jiraResp.StatusCode != HttpStatusCode.OK)
			{
				var serializer = new JsonSerializer<ErrorMessage>();
				var errorMessage = serializer.DeserializeFromString(jiraResp.Content);
				Log.Error(
					string.Format(
						"Unable to get issues from Jira, Error: {0}. Check your board/repo mapping configuration.",
						errorMessage.Message));
			}
			else
			{
				var issueToUpdate = new JsonSerializer<Issue>().DeserializeFromString(jiraResp.Content);

				if (issueToUpdate != null && issueToUpdate.Key == updatedCard.ExternalCardID)
				{
					bool isDirty = false;
					bool updateEpicName = false;

					var updateJson = "{ \"fields\": { ";

					if (updatedItems.Contains("Title") && issueToUpdate.Fields.Summary != updatedCard.Title)
					{
						issueToUpdate.Fields.Summary = updatedCard.Title;
						isDirty = true;
						updateEpicName = true;
					}

					updateJson += "\"summary\": \"" + issueToUpdate.Fields.Summary.Replace("\"", "\\\"") + "\"";

					if (updateEpicName)
					{
						if (issueToUpdate.Fields.IssueType.Name.ToLowerInvariant() == "epic")
						{
							if (CustomFields.Any())
							{
								var epicNameField = CustomFields.FirstOrDefault(x => x.Name == "Epic Name");
								if (epicNameField != null)
								{
									updateJson += ", \"" + epicNameField.Id + "\": \"" +
									              updatedCard.Title.Replace("\"", "\\\"") + "\"";
								}
							}
						}
					}

					if (updatedItems.Contains("Description") &&
					    issueToUpdate.Fields.Description.SanitizeCardDescription().JiraPlainTextToLeanKitHtml() !=
					    updatedCard.Description)
					{
						var updatedDescription = updatedCard.Description.LeanKitHtmlToJiraPlainText();
						updateJson += ", \"description\": \"" + updatedDescription + "\"";
						isDirty = true;
					}

					if (updatedItems.Contains("Priority"))
					{
						updateJson += ", \"priority\": { \"name\": \"" + GetPriority(updatedCard.Priority) + "\"}";
						isDirty = true;
					}

					if (updatedItems.Contains("DueDate") && CurrentUser != null)
					{
						try
						{
							var dateFormat = CurrentUser.DateFormat ?? "MM/dd/yyyy";
							var parsed = DateTime.ParseExact(updatedCard.DueDate, dateFormat,
								CultureInfo.InvariantCulture);

							updateJson += ", \"duedate\": \"" + parsed.ToString("o") + "\"";
						}
						catch (Exception ex)
						{
							Log.Warn(ex, "Could not parse due date: {0}", updatedCard.DueDate);
						}
					}

					if (updatedItems.Contains("Tags"))
					{
						var newLabels = updatedCard.Tags.Split(',');
						string updateLabels = "";
						int ctr = 0;
						foreach (string newLabel in newLabels)
						{
							if (ctr > 0)
								updateLabels += ", ";

							updateLabels += "\"" + newLabel.Trim() + "\"";

							ctr++;
						}
						updateJson += ", \"labels\": [" + updateLabels + "]";
						isDirty = true;
					}

					string comment = "";
					if (updatedItems.Contains("Size"))
					{
						comment += "LeanKit card Size changed to " + updatedCard.Size + ". ";
					}
					if (updatedItems.Contains("Blocked"))
					{
						if (updatedCard.IsBlocked)
							comment += "LeanKit card is blocked: " + updatedCard.BlockReason + ". ";
						else
							comment += "LeanKit card is no longer blocked: " + updatedCard.BlockReason + ". ";
					}

					updateJson += "}}";

					if (isDirty)
					{
						try
						{
							//https://yoursite.atlassian.net/rest/api/latest/issue/{issueIdOrKey}
							var updateRequest =
								CreateRequest(string.Format("rest/api/latest/issue/{0}", updatedCard.ExternalCardID),
									Method.PUT);
							updateRequest.AddParameter("application/json", updateJson, ParameterType.RequestBody);

							var resp = ExecuteRequest(updateRequest);

							if (resp.StatusCode != HttpStatusCode.OK && resp.StatusCode != HttpStatusCode.NoContent)
							{
								var serializer = new JsonSerializer<ErrorMessage>();
								var errorMessage = serializer.DeserializeFromString(resp.Content);
								Log.Error(string.Format("Unable to update Issue [{0}], Description: {1}, Message: {2}",
									updatedCard.ExternalCardID, resp.StatusDescription, errorMessage.Message));
							}
							else
							{
								Log.Debug(String.Format("Updated Issue [{0}]", updatedCard.ExternalCardID));
							}
						}
						catch (Exception ex)
						{
							Log.Error(string.Format("Unable to update Issue [{0}], Exception: {1}",
								updatedCard.ExternalCardID, ex.Message));
						}
					}

					if (!string.IsNullOrEmpty(comment))
					{
						try
						{
							//https://yoursite.atlassian.net/rest/api/latest/issue/{issueIdOrKey}
							var updateRequest =
								CreateRequest(
									string.Format("rest/api/latest/issue/{0}/comment", updatedCard.ExternalCardID),
									Method.POST);
							updateRequest.AddParameter(
								"application/json",
								"{ \"body\": \"" + comment + "\"}",
								ParameterType.RequestBody);

							var resp = ExecuteRequest(updateRequest);

							if (resp.StatusCode != HttpStatusCode.OK &&
							    resp.StatusCode != HttpStatusCode.NoContent &&
							    resp.StatusCode != HttpStatusCode.Created)
							{
								var serializer = new JsonSerializer<ErrorMessage>();
								var errorMessage = serializer.DeserializeFromString(resp.Content);
								Log.Error(
									string.Format(
										"Unable to create comment for updated Issue [{0}], Description: {1}, Message: {2}",
										updatedCard.ExternalCardID, resp.StatusDescription, errorMessage.Message));
							}
							else
							{
								Log.Debug(String.Format("Created comment for updated Issue [{0}]",
									updatedCard.ExternalCardID));
							}
						}
						catch (Exception ex)
						{
							Log.Error(string.Format("Unable to create comment for updated Issue [{0}], Exception: {1}",
								updatedCard.ExternalCardID, ex.Message));
						}
					}
				}
			}
		}
		protected override void Synchronize(BoardMapping project)
		{
			Log.Debug("Polling Jira for Issues");

			var queryAsOfDate = QueryDate.AddMilliseconds(Configuration.PollingFrequency*-1.5);

			string jqlQuery;
			var formattedQueryDate = queryAsOfDate.ToString(QueryDateFormat, CultureInfo.InvariantCulture);
			if (!string.IsNullOrEmpty(project.Query))
			{
				jqlQuery = string.Format(project.Query, formattedQueryDate);
			}
			else
			{
				var queryFilter = string.Format(" and ({0})",
					string.Join(" or ", project.QueryStates.Select(x => "status = '" + x.Trim() + "'").ToList()));
				if (!string.IsNullOrEmpty(project.ExcludedTypeQuery))
				{
					queryFilter += project.ExcludedTypeQuery;
				}
				jqlQuery = string.Format("project=\"{0}\" {1} and updated > \"{2}\" order by created asc",
					project.Identity.Target, queryFilter, formattedQueryDate);
			}

			//https://yoursite.atlassian.net/rest/api/latest/search?jql=project=%22More+Tests%22+and+status=%22open%22+and+created+%3E+%222008/12/31+12:00%22+order+by+created+asc&fields=id,status,priority,summary,description
			var request = CreateRequest("rest/api/latest/search", Method.GET);

			request.AddParameter("jql", jqlQuery);
			request.AddParameter("fields",
				"id,status,priority,summary,description,issuetype,type,assignee,duedate,labels");
			request.AddParameter("maxResults", "9999");

			var jiraResp = ExecuteRequest(request);

			if (jiraResp.StatusCode != HttpStatusCode.OK)
			{
				var serializer = new JsonSerializer<ErrorMessage>();
				var errorMessage = serializer.DeserializeFromString(jiraResp.Content);
				Log.Error(
					string.Format(
						"Unable to get issues from Jira, Error: {0}. Check your board/project mapping configuration.",
						errorMessage.Message));
				return;
			}

			var resp = new JsonSerializer<IssuesResponse>().DeserializeFromString(jiraResp.Content);

			Log.Info("\nQueried [{0}] at {1} for changes after {2}", project.Identity.Target, QueryDate,
				queryAsOfDate.ToString("o"));

			if (resp != null && resp.Issues != null && resp.Issues.Any())
			{
				var issues = resp.Issues;
				foreach (var issue in issues)
				{
					Log.Info("Issue [{0}]: {1}, {2}, {3}", issue.Key, issue.Fields.Summary, issue.Fields.Status.Name,
						issue.Fields.Priority.Name);

					// does this workitem have a corresponding card?
					var card = LeanKit.GetCardByExternalId(project.Identity.LeanKit, issue.Key);

					if (card == null || !card.ExternalSystemName.Equals(ServiceName, StringComparison.OrdinalIgnoreCase))
					{
						Log.Debug("Create new card for Issue [{0}]", issue.Key);
						CreateCardFromItem(project, issue);
					}
					else
					{
						Log.Debug("Previously created a card for Issue [{0}]", issue.Key);
						if (project.UpdateCards)
							IssueUpdated(issue, card, project);
						else
							Log.Info("Skipped card update because 'UpdateCards' is disabled.");
					}
				}
				Log.Info("{0} item(s) queried.\n", issues.Count);
			}
		}
        protected void UpdateStateOfExternalItem(Card card, List<string> states, BoardMapping boardMapping, bool runOnlyOnce)
	    {
		    if (!card.ExternalSystemName.Equals(ServiceName, StringComparison.OrdinalIgnoreCase))
			    return;

		    if (string.IsNullOrEmpty(card.ExternalCardID))
			    return;

			if (states == null || states.Count == 0)
				return;

			long issueNumber;

			string target = boardMapping.Identity.Target;

        	// use external card id to get the GitHub issue
        	try {
        		issueNumber = Convert.ToInt32(card.ExternalCardID.Split('|')[1]);
        	} catch (Exception) {
        		Log.Debug("Ignoring card [{0}] with missing external id value.", card.Id);
        		return;
        	}

			int tries = 0;
			bool success = false;
			while (tries < 10 && !success && (!runOnlyOnce || tries == 0))
			{
				if (tries > 0)
				{
					Log.Error(string.Format("Attempting to update external issue [{0}] attempt number [{1}]",issueNumber, tries));
					// wait 5 seconds before trying again
					Thread.Sleep(new TimeSpan(0, 0, 5));
				}

				//"https://api.github.com/repos/{0}/{1}/issues/{2}
				var request = new RestRequest(string.Format("repos/{0}/{1}/issues/{2}", Configuration.Target.Host, target, issueNumber), Method.GET);
				var ghResp = _restClient.Execute(request);

				if (ghResp.StatusCode != HttpStatusCode.OK)
				{
					var serializer = new JsonSerializer<ErrorMessage>();
					var errorMessage = serializer.DeserializeFromString(ghResp.Content);
					Log.Error(string.Format("Unable to get issue from GitHub, Error: {0}. Check your board mapping configuration.", errorMessage.Message));
				}
				else
				{
					var issueToUpdate = new JsonSerializer<Issue>().DeserializeFromString(ghResp.Content);

					if (issueToUpdate != null && issueToUpdate.Number == issueNumber)
					{
						if (issueToUpdate.State.ToLowerInvariant() == states[0].ToLowerInvariant())
						{
							Log.Debug(string.Format("Issue [{0}] is already in state [{1}]", issueToUpdate.Id, states[0]));
							return;											
						}

						issueToUpdate.State = states[0];

						try
						{
							//"https://api.github.com/repos/{0}/{1}/issues/{2}
							var updateRequest = new RestRequest(string.Format("repos/{0}/{1}/issues/{2}", Configuration.Target.Host, target, issueNumber), Method.PATCH);
							updateRequest.AddParameter("application/json", "{ \"state\": \"" + issueToUpdate.State + "\"}", ParameterType.RequestBody);

							var resp = _restClient.Execute(updateRequest);

							if (resp.StatusCode != HttpStatusCode.OK)
							{
								var serializer = new JsonSerializer<ErrorMessage>();
								var errorMessage = serializer.DeserializeFromString(resp.Content);
								Log.Error(string.Format("Unable to update Issue [{0}] to [{1}], Description: {2}, Message: {3}", issueNumber, issueToUpdate.State, resp.StatusDescription, errorMessage.Message));
							}
							else
							{
								success = true;
								Log.Debug(String.Format("Updated state for Issue [{0}] to [{1}]", issueNumber, issueToUpdate.State));
							}
						}
						catch (Exception ex)
						{
							Log.Error(string.Format("Unable to update Issue [{0}] to [{1}], Exception: {2}", issueNumber, issueToUpdate.State, ex.Message));
						}
					}
					else
					{
						Log.Debug(String.Format("Could not retrieve Issue [{0}] for updating state to [{1}]", issueNumber, issueToUpdate.State));
					}
				}
				tries++;
			}
		}
        protected override void CardUpdated(Card updatedCard, List<string> updatedItems, BoardMapping boardMapping)
        {
	        if (!updatedCard.ExternalSystemName.Equals(ServiceName, StringComparison.OrdinalIgnoreCase))
		        return;

			long issueNumber;

			string target = boardMapping.Identity.Target;

			// use external card id to get the GitHub Issue
			try {
				issueNumber = Convert.ToInt32(updatedCard.ExternalCardID.Split('|')[1]);
			} catch (Exception) {
				Log.Debug("Ignoring card [{0}] with missing external id value.", updatedCard.Id);
				return;
			}

			//"https://api.github.com/repos/{0}/{1}/issues/{2}
			var request = new RestRequest(string.Format("repos/{0}/{1}/issues/{2}", Configuration.Target.Host, target, issueNumber), Method.GET);
			var ghResp = _restClient.Execute(request);

	        if (ghResp.StatusCode != HttpStatusCode.OK)
	        {
		        var serializer = new JsonSerializer<ErrorMessage>();
		        var errorMessage = serializer.DeserializeFromString(ghResp.Content);
		        Log.Error(string.Format("Unable to get issue from GitHub, Error: {0}. Check your board mapping configuration.", errorMessage.Message));
	        }
	        else
	        {
		        var issueToUpdate = new JsonSerializer<Issue>().DeserializeFromString(ghResp.Content);

		        if (issueToUpdate != null && issueToUpdate.Number == issueNumber)
		        {
			        bool isDirty = false;

			        if (updatedItems.Contains("Title") && issueToUpdate.Title != updatedCard.Title)
			        {
				        issueToUpdate.Title = updatedCard.Title;
				        isDirty = true;
			        }

					string updateJson = "{ \"title\": \"" + issueToUpdate.Title.Replace("\"", "\\\"") + "\"";

			        if (updatedItems.Contains("Description") && issueToUpdate.Body.SanitizeCardDescription() != updatedCard.Description)
			        {
						updateJson += ", \"body\": \"" + updatedCard.Description.Replace("\"", "\\\"") + "\"";
				        isDirty = true;
			        }

					if (updatedItems.Contains("Tags"))
					{						
						var newLabels = updatedCard.Tags.Split(',');
						string updateLabels = "";
						int ctr = 0;
						foreach (string newLabel in newLabels)
						{
							if (ctr > 0)
								updateLabels += ", ";

							updateLabels += "{ \"name\": \"" + newLabel.Trim() + "\"}";

							ctr++;
						}
						updateJson += ", \"labels\": [" + updateLabels + "]";
						isDirty = true;
					}

					updateJson += "}";

			        string comment = "";
					if (updatedItems.Contains("Priority"))
					{
						comment += "LeanKit card Priority changed to " + updatedCard.Priority + ".<br />";
					}
			        if (updatedItems.Contains("DueDate"))
			        {
				        comment += "LeanKit card DueDate changed to " + updatedCard.DueDate + ".<br />";
			        }
			        if (updatedItems.Contains("Size"))
			        {
						comment += "LeanKit card Size changed to " + updatedCard.Size + ".<br />";
			        }
			        if (updatedItems.Contains("Blocked"))
			        {
						if (updatedCard.IsBlocked)
							comment += "LeanKit card is blocked: " + updatedCard.BlockReason + ".<br />";
						else
							comment += "LeanKit card is no longer blocked: " + updatedCard.BlockReason + ".<br />";
			        }

					if (isDirty)
					{
						try 
						{
							//"https://api.github.com/repos/{0}/{1}/issues/{2}
							var updateRequest = new RestRequest(string.Format("repos/{0}/{1}/issues/{2}", Configuration.Target.Host, target, issueNumber), Method.PATCH);
							updateRequest.AddParameter(
									"application/json",
									updateJson, 
									ParameterType.RequestBody
								);

							var resp = _restClient.Execute(updateRequest);

							if (resp.StatusCode != HttpStatusCode.OK) 
							{
								var serializer = new JsonSerializer<ErrorMessage>();
								var errorMessage = serializer.DeserializeFromString(resp.Content);
								Log.Error(string.Format("Unable to update Issue [{0}], Description: {1}, Message: {2}", issueNumber, resp.StatusDescription, errorMessage.Message));
							} 
							else 
							{
								Log.Debug(String.Format("Updated Issue [{0}]", issueNumber));
							}
						} 
						catch (Exception ex) 
						{
							Log.Error(string.Format("Unable to update Issue [{0}], Exception: {1}", issueNumber, ex.Message));
						}						
					}				

					if (!string.IsNullOrEmpty(comment))
					{
						try 
						{
							//"https://api.github.com/repos/{0}/{1}/issues/{2}/comments
							var newCommentRequest = new RestRequest(string.Format("repos/{0}/{1}/issues/{2}/comments", Configuration.Target.Host, target, issueNumber), Method.POST);
							newCommentRequest.AddParameter(
									"application/json",
									"{ \"body\": \"" + comment + "\"}",
									ParameterType.RequestBody
								);

							var resp = _restClient.Execute(newCommentRequest);

							if (resp.StatusCode != HttpStatusCode.OK || resp.StatusCode != HttpStatusCode.Created) 
							{
								var serializer = new JsonSerializer<ErrorMessage>();
								var errorMessage = serializer.DeserializeFromString(resp.Content);
								Log.Error(string.Format("Unable to create comment on updated Issue [{0}], Description: {1}, Message: {2}", issueNumber, resp.StatusDescription, errorMessage.Message));
							} 
							else 
							{
								Log.Debug(String.Format("Created comment on Updated Issue [{0}]", issueNumber));
							}
						} 
						catch (Exception ex) 
						{
							Log.Error(string.Format("Unable to create comment on updated Issue [{0}], Exception: {1}", issueNumber, ex.Message));
						}							
					}
		        }
	        }         
        }
		protected override void CreateNewItem(Card card, BoardMapping boardMapping)
		{
			var jiraIssueType = GetJiraIssueType(boardMapping, card.TypeId);

			string json = "{ \"fields\": { ";
			json += "\"project\":  { \"key\": \"" + boardMapping.Identity.Target + "\" }";
			json += ", \"summary\": \"" + card.Title + "\" ";
			json += ", \"description\": \"" + (card.Description != null ? card.Description.Replace("</p>", "").Replace("<p>", "").Replace("\n","\\n") : "") + "\" ";
			json += ", \"issuetype\": { \"name\": \"" + jiraIssueType + "\" }";
			json += ", \"priority\": { \"name\": \"" + GetPriority(card.Priority) + "\" }";

			if (jiraIssueType.ToLowerInvariant() == "epic")
			{
				if (CustomFields.Any())
				{
					var epicNameField = CustomFields.FirstOrDefault(x => x.Name == "Epic Name");
					if (epicNameField != null)
					{
						json += ", \"" + epicNameField.Id + "\": \"" + card.Title + "\"";
					}
				}
			}

			if (!string.IsNullOrEmpty(card.DueDate)) 
			{
				DateTime updatedDate;
				var isDate = DateTime.TryParse(card.DueDate, out updatedDate);
				if (isDate) 
				{
					json += ", \"duedate\": \"" + updatedDate.ToString("o") + "\"";
				}
			}

			if (!string.IsNullOrEmpty(card.Tags)) 
			{
				var newLabels = card.Tags.Split(',');
				string updateLabels = "";
				int ctr = 0;
				foreach (string newLabel in newLabels) {
					if (ctr > 0)
						updateLabels += ", ";

					updateLabels += "\"" + newLabel.Trim() + "\"";

					ctr++;
				}
				json += ", \"labels\": [" + updateLabels + "]";
			}

			json += "}}";

			Issue newIssue = null;
			try 
			{
				//https://yoursite.atlassian.net/rest/api/latest/issue
				var createRequest = new RestRequest("/rest/api/latest/issue", Method.POST);
				createRequest.AddParameter("application/json", json, ParameterType.RequestBody);
				var resp = _restClient.Execute(createRequest);

				if (resp.StatusCode != HttpStatusCode.OK && resp.StatusCode != HttpStatusCode.Created) 
				{
					var serializer = new JsonSerializer<ErrorMessage>();
					var errorMessage = serializer.DeserializeFromString(resp.Content);
					Log.Error(string.Format("Unable to create Issue from card [{0}], Description: {1}, Message: {2}",
											 card.ExternalCardID, resp.StatusDescription, errorMessage.Message));
				} 
				else 
				{
					newIssue = new JsonSerializer<Issue>().DeserializeFromString(resp.Content);
					Log.Debug(String.Format("Created Issue [{0}]", newIssue.Key));
				}
			} 
			catch (Exception ex) 
			{
				Log.Error(string.Format("Unable to create Issue from Card [{0}], Exception: {1}", card.ExternalCardID, ex.Message));
			}

			if (newIssue != null)
			{
				try
				{
					card.ExternalCardID = newIssue.Key;
					card.ExternalSystemName = ServiceName;
					card.ExternalSystemUrl = string.Format(_externalUrlTemplate, newIssue.Key);

					// now that we've created the work item let's try to set it to any matching state defined by lane
					var states = boardMapping.LaneToStatesMap[card.LaneId];
                    if (states != null) 
					{
						UpdateStateOfExternalItem(card, states, boardMapping, true);
					}	

					LeanKit.UpdateCard(boardMapping.Identity.LeanKit, card);
				}
				catch (Exception ex)
				{
					Log.Error(string.Format("Error updating Card [{0}] after creating new Issue, Exception: {1}", card.ExternalCardID,
					                         ex.Message));
				}
			}
		}
        protected override void Synchronize(BoardMapping project)
        {
			Log.Debug("Polling GitHub for Issues");

			var queryAsOfDate = QueryDate.AddMilliseconds(Configuration.PollingFrequency * -1.5);

			// GitHub will only let us query one state at a time :(
	        foreach (var state in project.QueryStates)
	        {
				//https://api.github.com/repos/{0}/{1}/issues?state=Open&since={2}			
				var request = new RestRequest(string.Format("repos/{0}/{1}/issues", Configuration.Target.Host, project.Identity.Target), Method.GET);
				request.AddParameter("state", state);
				request.AddParameter("since", queryAsOfDate.ToString("o"));

				var resp = _restClient.Execute(request);

				if (resp.StatusCode != HttpStatusCode.OK)
				{
					var serializer = new JsonSerializer<ErrorMessage>();
					var errorMessage = serializer.DeserializeFromString(resp.Content);
					Log.Error(string.Format("Unable to get issues from GitHub, Error: {0}. Check your board/repo mapping configuration.", errorMessage.Message));
					return;
				}

				var issues = new JsonSerializer<List<Issue>>().DeserializeFromString(resp.Content);

				Log.Info("\nQueried [{0}] at {1} for changes after {2}", project.Identity.Target, QueryDate, queryAsOfDate.ToString("o"));

		        if (issues == null || !issues.Any() || issues[0].Id <= 0) continue;

		        foreach (var issue in issues.Where(issue => issue.Id > 0))
		        {
			        Log.Info("Issue [{0}]: {1}, {2}, {3}", issue.Number, issue.Title, issue.User.Login, issue.State);

			        // does this workitem have a corresponding card?
			        var card = LeanKit.GetCardByExternalId(project.Identity.LeanKit, issue.Id + "|" + issue.Number.ToString());

					if (card == null || !card.ExternalSystemName.Equals(ServiceName, StringComparison.OrdinalIgnoreCase))
			        {
				        Log.Debug("Create new card for Issue [{0}]", issue.Number);
				        CreateCardFromItem(project, issue);
			        }
			        else
			        {
				        Log.Debug("Previously created a card for Issue [{0}]", issue.Number);
				        if (project.UpdateCards)
					        IssueUpdated(issue, card, project);
				        else
					        Log.Info("Skipped card update because 'UpdateCards' is disabled.");
			        }
		        }
		        Log.Info("{0} item(s) queried.\n", issues.Count);
	        }
        }
        protected override void CardUpdated(Card updatedCard, List<string> updatedItems, BoardMapping boardMapping)
        {
			if (!updatedCard.ExternalSystemName.Equals(ServiceName, StringComparison.OrdinalIgnoreCase))
				return;

			if (string.IsNullOrEmpty(updatedCard.ExternalCardID))
				return;

			long issueNumber;

			string target = boardMapping.Identity.Target;

			// use external card id to get the GitHub Issue
			try 
			{
				issueNumber = Convert.ToInt32(updatedCard.ExternalCardID.Split('|')[1]);
			} 
			catch (Exception) 
			{
				Log.Debug("Ignoring card [{0}] with missing external id value.", updatedCard.Id);
				return;
			}

			//"https://api.github.com/repos/{0}/{1}/pulls/{2}
			var request = new RestRequest(string.Format("repos/{0}/{1}/pulls/{2}", Configuration.Target.Host, target, issueNumber), Method.GET);
			var ghResp = _restClient.Execute(request);

			if (ghResp.StatusCode != HttpStatusCode.OK) 
			{
				var serializer = new JsonSerializer<ErrorMessage>();
				var errorMessage = serializer.DeserializeFromString(ghResp.Content);
				Log.Error(string.Format("Unable to get Pull Request from GitHub, Error: {0}. Check your board mapping configuration.", errorMessage.Message));
			} 
			else 
			{
				var pullToUpdate = new JsonSerializer<Pull>().DeserializeFromString(ghResp.Content);

				if (pullToUpdate != null && pullToUpdate.Number == issueNumber) 
				{
					bool isDirty = false;

					if (updatedItems.Contains("Title") && pullToUpdate.Title != updatedCard.Title) 
					{
						pullToUpdate.Title = updatedCard.Title.Replace("\"", "\\\"");
						isDirty = true;
					}

					if (updatedItems.Contains("Description") && pullToUpdate.Body.SanitizeCardDescription() != updatedCard.Description) 
					{
						pullToUpdate.Body = updatedCard.Description;
						isDirty = true;
					}

					// Do nothing with Priority, DueDate, Size, Blocked, or Tags because GitHub pull requests do not have comments
					//if (updatedItems.Contains("Priority")) 
					//if (updatedItems.Contains("DueDate")) 
					//if (updatedItems.Contains("Size")) 
					//if (updatedItems.Contains("Blocked")) 
					//if (updatedItems.Contains("Tags")) 

					if (isDirty) 
					{
						try 
						{
							//"https://api.github.com/repos/{0}/{1}/pulls/{2}
							var updateRequest = new RestRequest(string.Format("repos/{0}/{1}/pulls/{2}", Configuration.Target.Host, target, issueNumber), Method.PATCH);						
							updateRequest.AddParameter(
									"application/json",
									"{ \"title\": \"" + pullToUpdate.Title + "\", \"body\": \"" + pullToUpdate.Body + "\"}",
									ParameterType.RequestBody
								);

							var resp = _restClient.Execute(updateRequest);

							if (resp.StatusCode != HttpStatusCode.OK) 
							{
								var serializer = new JsonSerializer<ErrorMessage>();
								var errorMessage = serializer.DeserializeFromString(resp.Content);
								Log.Error(string.Format("Unable to update Pull Request [{0}], Description: {1}, Message: {2}", issueNumber, resp.StatusDescription, errorMessage.Message));
							} 
							else 
							{
								Log.Debug(String.Format("Updated Pull Request [{0}]", issueNumber));
							}
						} 
						catch (Exception ex) 
						{
							Log.Error(string.Format("Unable to update Pull Request [{0}], Exception: {1}", issueNumber, ex.Message));
						}
					}
				}
			}
        }
        protected override void CardUpdated(Card updatedCard, List<string> updatedItems, BoardMapping boardMapping)
        {
			if (!updatedCard.ExternalSystemName.Equals(ServiceName, StringComparison.OrdinalIgnoreCase))
				return;

	        var target = boardMapping.Identity.Target;

			if (string.IsNullOrEmpty(updatedCard.ExternalCardID)) 
			{
				Log.Debug("Ignoring card [{0}] with missing external id value.", updatedCard.ExternalCardID);
				return;
			}

			//https://mysubdomain.unfuddle.com/api/v1/projects/{id}/tickets/{id}
			var request = new RestRequest(string.Format("/api/v1/projects/{0}/tickets/{1}", target, updatedCard.ExternalCardID), Method.GET);
			var unfuddleResp = _restClient.Execute(request);

			if (unfuddleResp.StatusCode != HttpStatusCode.OK)
			{
				var serializer = new JsonSerializer<ErrorMessage>();
				var errorMessage = serializer.DeserializeFromString(unfuddleResp.Content);
				Log.Error(string.Format("Unable to get tickets from Unfuddle, Error: {0}. Check your board/repo mapping configuration.", errorMessage.Message));
			}
			else
			{
				var ticketToUpdate = new JsonSerializer<Ticket>().DeserializeFromString(unfuddleResp.Content);

				if (ticketToUpdate != null && ticketToUpdate.Id.ToString() == updatedCard.ExternalCardID)
				{
					bool isDirty = false;

					string summaryXml = "";
					string descriptionXml = "";
					string priorityXml = "";
					string dueDateXml = "";

					if (updatedItems.Contains("Title") && ticketToUpdate.Summary != updatedCard.Title)
					{
						summaryXml = "<summary>" + updatedCard.Title + "</summary>";
						isDirty = true;
					}

					if (updatedItems.Contains("Description") && ticketToUpdate.Description.SanitizeCardDescription() != updatedCard.Description)
					{
						var updatedDescription = updatedCard.Description;
						if (!string.IsNullOrEmpty(updatedDescription))
						{
							updatedDescription = updatedDescription.Replace("<p>", "").Replace("</p>", "");
						}
						descriptionXml = "<description>" + updatedDescription + "</description>";
						isDirty = true;
					}

					if (updatedItems.Contains("Priority"))
					{
						priorityXml = "<priority>" + GetPriority(updatedCard.Priority) + "</priority>";
						isDirty = true;
					}

					if (updatedItems.Contains("DueDate"))
					{
						DateTime updatedDate;
						var isDate = DateTime.TryParse(updatedCard.DueDate, out updatedDate);
						if (isDate)
						{
							dueDateXml = "<due-on>" + updatedDate.ToString("o") + "</due-on>";
							isDirty = true;
						}
					}

					string comment = "";
					if (updatedItems.Contains("Size")) 
					{
						comment += "LeanKit card Size changed to " + updatedCard.Size + ". ";
					}
					if (updatedItems.Contains("Tags")) 
					{
						comment += "LeanKit card Tags changed to " + updatedCard.Tags + ". ";
					}
					if (updatedItems.Contains("Blocked")) 
					{
						if (updatedCard.IsBlocked)
							comment += "LeanKit card is blocked: " + updatedCard.BlockReason + ". ";
						else
							comment += "LeanKit card is no longer blocked: " + updatedCard.BlockReason + ". ";
					}

					if (isDirty) 
					{
						try 
						{
							//https://mysubdomain.unfuddle.com/api/v1/api/v1/projects/{id}/tickets/{id}
							var updateRequest = new RestRequest(string.Format("/api/v1/projects/{0}/tickets/{1}", target, updatedCard.ExternalCardID), Method.PUT);
							updateRequest.AddHeader("Accept", "application/json");
							updateRequest.AddHeader("Content-type", "application/xml");
							updateRequest.AddParameter(
									"application/xml", 
									string.Format("<ticket>{0}{1}{2}{3}</ticket>", summaryXml, descriptionXml, priorityXml, dueDateXml), 
									ParameterType.RequestBody
								);

							var resp = _restClient.Execute(updateRequest);

							if (resp.StatusCode != HttpStatusCode.OK) 
							{
								if (resp.Content != null && !string.IsNullOrEmpty(resp.Content.Trim())) 
								{
									var serializer = new JsonSerializer<ErrorMessage>();
									var errorMessage = serializer.DeserializeFromString(resp.Content);
									Log.Error(string.Format("Unable to update Ticket [{0}], Description: {1}, Message: {2}",
															 updatedCard.ExternalCardID, resp.StatusDescription, errorMessage.Message));
								} 
								else 
								{
									Log.Error(string.Format("Unable to update Ticket [{0}], Description: {1}, Message: {2}",
															updatedCard.ExternalCardID, resp.StatusDescription, resp.StatusDescription));
								}
							} 
							else 
							{
								Log.Debug(String.Format("Updated Ticket [{0}]", updatedCard.ExternalCardID, ticketToUpdate.Status));
							}
						} 
						catch (Exception ex) 
						{
							Log.Error(string.Format("Unable to update Ticket [{0}], Exception: {1}", updatedCard.ExternalCardID, ex.Message));
						}
					}

					if (!string.IsNullOrEmpty(comment))
					{
						try {
							//https://mysubdomain.unfuddle.com/api/v1/api/v1/projects/{id}/tickets/{id}/comments
							var updateRequest = new RestRequest(string.Format("/api/v1/projects/{0}/tickets/{1}/comments", target, updatedCard.ExternalCardID), Method.POST);
							updateRequest.AddHeader("Accept", "application/json");
							updateRequest.AddHeader("Content-type", "application/xml");
							updateRequest.AddParameter(
									"application/xml",
									string.Format("<comment><body>{0}</body></comment>", comment),
									ParameterType.RequestBody
								);

							var resp = _restClient.Execute(updateRequest);

							if (resp.StatusCode != HttpStatusCode.OK && resp.StatusCode != HttpStatusCode.Created) 
							{
								if (resp.Content != null && !string.IsNullOrEmpty(resp.Content.Trim())) 
								{
									var serializer = new JsonSerializer<ErrorMessage>();
									var errorMessage = serializer.DeserializeFromString(resp.Content);
									Log.Error(string.Format("Unable to create comment on updated Ticket [{0}], Description: {1}, Message: {2}", updatedCard.ExternalCardID, resp.StatusDescription, errorMessage.Message));
								} 
								else 
								{
									Log.Error(string.Format("Unable to create comment on updated Ticket [{0}], Description: {1}, Message: {2}", updatedCard.ExternalCardID, resp.StatusDescription, resp.StatusDescription));
								}
							} 
							else 
							{
								Log.Debug(String.Format("Created comment on updated Ticket [{0}]", updatedCard.ExternalCardID, ticketToUpdate.Status));
							}
						} 
						catch (Exception ex) 
						{
							Log.Error(string.Format("Unable to create comment on updated Ticket [{0}], Exception: {1}", updatedCard.ExternalCardID, ex.Message));
						}						
					}
				}
			}		
        }
        protected void UpdateStateOfExternalItem(Card card, List<string> states, BoardMapping mapping, bool runOnlyOnce)
		{
			if (!card.ExternalSystemName.Equals(ServiceName, StringComparison.OrdinalIgnoreCase))
				return;

			if (states == null || states.Count == 0)
				return;

			if (string.IsNullOrEmpty(card.ExternalCardID))
			{
				Log.Debug("Ignoring card [{0}] with missing external id value.", card.Id);
				return;
			}

			int tries = 0;
			bool success = false;
			while (tries < 10 && !success && (!runOnlyOnce || tries == 0))
			{
				if (tries > 0)
				{
					Log.Error(string.Format("Attempting to update external ticket [{0}] attempt number [{1}]", card.ExternalCardID,
					                         tries));
					// wait 5 seconds before trying again
					Thread.Sleep(new TimeSpan(0, 0, 5));
				}

				//https://mysubdomain.unfuddle.com/api/v1/projects/{id}/tickets/{id}
				var request = new RestRequest(string.Format("/api/v1/projects/{0}/tickets/{1}", mapping.Identity.Target, card.ExternalCardID), Method.GET);
				var unfuddleResp = _restClient.Execute(request);

				if (unfuddleResp.StatusCode != HttpStatusCode.OK)
				{
					var serializer = new JsonSerializer<ErrorMessage>();
					var errorMessage = serializer.DeserializeFromString(unfuddleResp.Content);
					Log.Error(string.Format("Unable to get tickets from Unfuddle, Error: {0}. Check your board/repo mapping configuration.", errorMessage.Message));
				}
				else
				{
					var ticketToUpdate = new JsonSerializer<Ticket>().DeserializeFromString(unfuddleResp.Content);

					if (ticketToUpdate != null && ticketToUpdate.Id.ToString() == card.ExternalCardID) 
					{
						// Check for a workflow mapping to the closed state
						if (states[0].Contains(">"))
						{
							var workflowStates = states[0].Split('>');

							// check to see if the workitem is already in one of the workflow states
							var alreadyInState = workflowStates.FirstOrDefault(x => x.Trim().ToLowerInvariant() == ticketToUpdate.Status.ToLowerInvariant());
							if (!string.IsNullOrEmpty(alreadyInState))
							{
								// change workflowStates to only use the states after the currently set state
								var currentIndex = Array.IndexOf(workflowStates, alreadyInState);
								if (currentIndex < workflowStates.Length - 1)
								{
									var updatedWorkflowStates = new List<string>();
									for (int i = currentIndex + 1; i < workflowStates.Length; i++)
									{
										updatedWorkflowStates.Add(workflowStates[i]);
									}
									workflowStates = updatedWorkflowStates.ToArray();
								}
							}

							if (workflowStates.Length > 0) 
							{
								foreach (string workflowState in workflowStates) 
								{
									//UpdateStateOfExternalItem(card, new LaneStateMap() { State = workflowState.Trim(), States = new List<string>() { workflowState.Trim() } }, mapping, runOnlyOnce);
								    UpdateStateOfExternalItem(card, new List<string> {workflowState.Trim()}, mapping, runOnlyOnce);
								}
								return;
							}
						}

						if (ticketToUpdate.Status.ToLowerInvariant() == states[0].ToLowerInvariant()) 
						{
							Log.Debug(string.Format("Ticket [{0}] is already in state [{1}]", ticketToUpdate.Id, states[0]));
							return;
						}

						ticketToUpdate.Status = states[0];

						try 
						{
							//https://mysubdomain.unfuddle.com/api/v1/api/v1/projects/{id}/tickets/{id}
							var updateRequest = new RestRequest(string.Format("/api/v1/projects/{0}/tickets/{1}", mapping.Identity.Target, card.ExternalCardID), Method.PUT);
							updateRequest.AddHeader("Accept", "application/json");
							updateRequest.AddHeader("Content-type", "application/xml");
							updateRequest.AddParameter("application/xml", "<ticket><status>" + ticketToUpdate.Status + "</status></ticket>", ParameterType.RequestBody);

							var resp = _restClient.Execute(updateRequest);

							if (resp.StatusCode != HttpStatusCode.OK) 
							{
								if (resp.Content != null && !string.IsNullOrEmpty(resp.Content.Trim()))
								{
									var serializer = new JsonSerializer<ErrorMessage>();
									var errorMessage = serializer.DeserializeFromString(resp.Content);
									Log.Error(string.Format("Unable to update Ticket [{0}] to [{1}], Description: {2}, Message: {3}",
									                         card.ExternalCardID, ticketToUpdate.Status, resp.StatusDescription, errorMessage.Message));
								}
								else
								{
									Log.Error(string.Format("Unable to update Ticket [{0}] to [{1}], Description: {2}, Message: {3}",
															card.ExternalCardID, ticketToUpdate.Status, resp.StatusDescription, resp.StatusDescription));
								}
							} 
							else 
							{
								success = true;
								Log.Debug(String.Format("Updated state for Ticket [{0}] to [{1}]", card.ExternalCardID, ticketToUpdate.Status));
							}
						} 
						catch (Exception ex) 
						{
							Log.Error(string.Format("Unable to update Ticket [{0}] to [{1}], Exception: {2}", card.ExternalCardID, ticketToUpdate.Status, ex.Message));
						}
					} 
					else 
					{
						Log.Debug(String.Format("Could not retrieve Ticket [{0}] for updating state to [{1}]", card.ExternalCardID, ticketToUpdate.Status));
					}
				}
				tries++;
			}
		}
		public long? CalculateAssignedUserId(long boardId, Ticket ticket) 
		{
			if (ticket == null)
				return null;

			if (ticket.Assignee_Id > 0)
			{
				// http://mysubdomain.unfuddle.com/api/v1/people/{id}
				var request = new RestRequest(string.Format("/api/v1/people/{0}", ticket.Assignee_Id), Method.GET);
				var unfuddleResp = _restClient.Execute(request);

				if (unfuddleResp.StatusCode != HttpStatusCode.OK)
				{
					var serializer = new JsonSerializer<ErrorMessage>();
					var errorMessage = serializer.DeserializeFromString(unfuddleResp.Content);
					Log.Warn(string.Format("Unable to get user from Unfuddle, Error: {0}", errorMessage.Message));
				}
				else
				{
					var user = new JsonSerializer<Person>().DeserializeFromString(unfuddleResp.Content);

					if (user != null) 
					{
						var lkUser = LeanKit.GetBoard(boardId).BoardUsers.FirstOrDefault(x => x!= null && 
							((!string.IsNullOrEmpty(x.EmailAddress)) && (!string.IsNullOrEmpty(user.Email)) && x.EmailAddress.ToLowerInvariant() == user.Email.ToLowerInvariant()) ||
							((!string.IsNullOrEmpty(x.FullName)) && (!string.IsNullOrEmpty(user.Last_Name)) && x.FullName.ToLowerInvariant() == (user.First_Name + " " + user.Last_Name).ToLowerInvariant()) ||
							((!string.IsNullOrEmpty(x.UserName)) && (!string.IsNullOrEmpty(user.Username)) && x.UserName.ToLowerInvariant() == user.Username.ToLowerInvariant()));
						if (lkUser != null)
							return lkUser.Id;
					}
				}
			}

			return null;
		}
        protected override void Synchronize(BoardMapping project)
        {
            Log.Debug("Polling Unfuddle for Tickets");

			var queryAsOfDate = QueryDate.AddMilliseconds(Configuration.PollingFrequency * -1.5);

			var unfuddleQuery = !string.IsNullOrEmpty(project.Query) 
				? string.Format(project.Query, queryAsOfDate.ToString("yyyy/MM/dd hh:mm")) 
				: string.Format("status-eq-{0},created_at-gt-{1}", project.QueryStates[0], queryAsOfDate.ToString("yyyy/MM/dd hh:mm"));

			//http://mysubdomain.unfuddle.com/api/v1/projects/{id}/ticket_reports/dynamic?sort_by=created_at&sort_direction=ASC&conditions_string=status-eq-new,created_at-gt-yyyy/MM/dd hh:mm
			var request = new RestRequest(string.Format("/api/v1/projects/{0}/ticket_reports/dynamic", project.Identity.Target), Method.GET);
	        request.AddParameter("sort_by", "created_at");
			request.AddParameter("sort_direction", "ASC");
	        request.AddParameter("conditions_string", unfuddleQuery);
			//, "id,status,priority,summary,description,type");
	        request.AddParameter("limit", "500");

			var unfuddleResp = _restClient.Execute(request);

			if (unfuddleResp.StatusCode != HttpStatusCode.OK) 
			{
				var serializer = new JsonSerializer<ErrorMessage>();
				var errorMessage = serializer.DeserializeFromString(unfuddleResp.Content);
				Log.Error(string.Format("Unable to get tickets from Unfuddle, Error: {0}. Check your board/project mapping configuration.", errorMessage.Message));
				return;
			}

			var resp = new JsonSerializer<TicketsResponse>().DeserializeFromString(unfuddleResp.Content);

			Log.Info("\nQueried [{0}] at {1} for changes after {2}", project.Identity.Target, QueryDate, queryAsOfDate.ToString("o"));
			
			if (resp != null && resp.Groups != null && resp.Groups.Any())
			{

				foreach (var group in resp.Groups)
				{
					if (group != null && group.Tickets != null && group.Tickets.Any())
					{
						var tickets = group.Tickets;
						foreach (var ticket in tickets) 
						{
							Log.Info("Ticket [{0}]: {1}, {2}, {3}", ticket.Id, ticket.Summary, ticket.Status, ticket.Priority);

							// does this workitem have a corresponding card?
							var card = LeanKit.GetCardByExternalId(project.Identity.LeanKit, ticket.Id.ToString());

							if (card == null || !card.ExternalSystemName.Equals(ServiceName, StringComparison.OrdinalIgnoreCase)) 
							{
								Log.Debug("Create new card for Ticket [{0}]", ticket.Id);
								CreateCardFromItem(project, ticket);
							} 
							else 
							{
								Log.Debug("Previously created a card for Ticket [{0}]", ticket.Id);
								if (project.UpdateCards)
									TicketUpdated(ticket, card, project);
								else
									Log.Info("Skipped card update because 'UpdateCards' is disabled.");
							}
						}
														
					}
				}
				Log.Info("{0} item(s) queried.\n", resp.Count);		
			}     
        }