/// <inheritdoc />
        public void DeleteRecurringTransaction(int id, bool deleteInstances)
        {
            this.ConcurrentInvoke(() =>
            {
                var processor = new TransactionProcessor(this.Context, this.splitwiseContext);

                var entity = this.Context.RecurringTransactions.GetEntity(id);

                var instances = this.Context.Transactions.GetTransactionsFromRecurring(entity.Id);
                foreach (var instance in instances)
                {
                    if (deleteInstances)
                    {
                        processor.RevertIfProcessed(instance);
                        this.Context.Remove(instance);
                    }
                    else
                    {
                        instance.RecurringTransactionId = null;
                    }
                }

                this.Context.Remove(entity);

                this.Context.SaveChanges();
            });
        }
        /// <inheritdoc />
        public void UpdateTransactionReceiver(int id, int accountId)
        {
            this.ConcurrentInvoke(() =>
            {
                var transaction = this.Context.Transactions.GetEntity(id);

                if (transaction.Type != TransactionType.Transfer)
                {
                    throw new ValidationException("Only a transfer transaction can have a receiving account.");
                }

                var newReceiver = this.Context.Accounts.GetEntity(accountId);

                var processor = new TransactionProcessor(this.Context, this.splitwiseContext);

                // Transfer transaction do not use budgets or categories, so we can use the normal revert process.
                processor.RevertIfProcessed(transaction, true);

                transaction.ReceivingAccountId = newReceiver.Id;
                transaction.ReceivingAccount   = newReceiver;

                processor.ProcessIfNeeded(transaction);

                this.Context.SaveChanges();
            });
        }
        /// <inheritdoc />
        public void DeleteTransaction(int id)
        {
            this.ConcurrentInvoke(() =>
            {
                var processor = new TransactionProcessor(this.Context, this.splitwiseContext);

                var entity = this.Context.Transactions.GetEntity(id);

                processor.RevertIfProcessed(entity);

                this.Context.Remove(entity);

                this.Context.SaveChanges();
            });
        }
        /// <inheritdoc />
        public ImportResult ImportFromSplitwise()
        {
            lock (SplitwiseManager.lockObj)
            {
                if (SplitwiseManager.importStatus == ImportState.Running)
                {
                    return(ImportResult.AlreadyRunning);
                }

                SplitwiseManager.importStatus = ImportState.Running;
            }

            var lastRan = this.Context.SynchronizationTimes
                          .Single()
                          .SplitwiseLastRun;

            // Get the new and updated expenses from Splitwise.
            var timestamp     = DateTime.UtcNow;
            var newExpenses   = this.splitwiseContext.GetExpenses(lastRan);
            var newExpenseIds = newExpenses.Select(t => t.Id).ToSet();

            // Load relevant entities and store them in a dictionary.
            var splitwiseTransactionsById = this.Context.SplitwiseTransactions
                                            .IncludeAll()
                                            .Where(t => newExpenseIds.Contains(t.Id))
                                            .AsEnumerable()
                                            .ToDictionary(t => t.Id);
            var transactionsBySplitwiseId = this.Context.Transactions
                                            .IncludeAll()
                                            .Where(t => t.SplitwiseTransactionId.HasValue)
                                            .Where(t => newExpenseIds.Contains(t.SplitwiseTransactionId.Value))
                                            .AsEnumerable()
                                            .ToDictionary(t => t.SplitwiseTransactionId.Value);

            this.ConcurrentInvoke(() =>
            {
                var processor = new TransactionProcessor(this.Context, this.splitwiseContext);

                this.Context.SetSplitwiseSynchronizationTime(timestamp);

                foreach (var newExpense in newExpenses)
                {
                    var splitwiseTransactionMaybe = splitwiseTransactionsById.TryGetValue(newExpense.Id);

                    if (splitwiseTransactionMaybe.IsSome &&
                        splitwiseTransactionMaybe.Value.UpdatedAt == newExpense.UpdatedAt)
                    {
                        // The last updated at is equal to the one stored, meaning that the latest update was
                        // triggered by this application and is already handled.
                        continue;
                    }

                    // The new expense is not known and the user has no share, so it's irrelevant.
                    if (splitwiseTransactionMaybe.IsNone && !newExpense.HasShare)
                    {
                        continue;
                    }

                    var transaction = transactionsBySplitwiseId.TryGetValue(newExpense.Id);

                    // Revert the transaction before updating values, don't send the update to Splitwise again.
                    if (transaction.IsSome)
                    {
                        processor.RevertIfProcessed(transaction.Value, true);

                        // Remove the transaction, it is re-added if needed.
                        this.Context.Transactions.Remove(transaction.Value);
                    }

                    // Update the values of the Splitwise transaction, or create a new one.
                    var splitwiseTransaction = splitwiseTransactionMaybe.Match(
                        st =>
                    {
                        this.Context.SplitDetails.RemoveRange(st.SplitDetails);
                        st.UpdateValues(newExpense);
                        st.SplitDetails = newExpense.Splits.Select(s => s.ToSplitDetailEntity()).ToList();

                        return(st);
                    },
                        () =>
                    {
                        var st = newExpense.ToSplitwiseTransactionEntity();
                        this.Context.SplitwiseTransactions.Add(st);
                        return(st);
                    });

                    // Remove the Splitwise transaction if it is irrelevant
                    if (!splitwiseTransaction.HasShare)
                    {
                        this.Context.SplitwiseTransactions.Remove(splitwiseTransaction);
                        continue;
                    }

                    // If the Splitwise transaction was already completely imported and is importable after the update,
                    // then try to update the transaction.
                    if (transaction.IsSome && splitwiseTransaction.Importable &&
                        // If the account or category is now obsolete, then the Splitwise transaction has to be re-imported.
                        !transaction.Value.Account.IsObsolete && !transaction.Value.Category.IsObsolete)
                    {
                        transaction = splitwiseTransaction.ToTransaction(
                            transaction.Value.Account, transaction.Value.Category);

                        this.Context.Transactions.Add(transaction.Value);

                        processor.ProcessIfNeeded(transaction.Value);
                    }
                }

                this.Context.SaveChanges();
            });

            SplitwiseManager.importStatus = ImportState.NotRunning;

            return(ImportResult.Completed);
        }
        /// <inheritdoc />
        public RecurringTransaction UpdateRecurringTransaction(
            int id, InputRecurringTransaction input, bool updateInstances)
        {
            this.validator.Description(input.Description);
            var startPeriod = this.validator.DateString(input.StartDateString, "startDate");
            var endPeriod   = input.EndDateString.Select(d => this.validator.DateString(d, "endDate"));

            if (endPeriod.IsSome)
            {
                this.validator.Period(startPeriod, endPeriod);
            }
            this.validator.Interval(input.Interval);
            var type = this.GetTransactionType(input.CategoryId, input.ReceivingAccountId, input.Amount);

            this.validator.Splits(this.splitwiseContext, input.PaymentRequests, input.SplitwiseSplits, type, input.Amount);

            return(this.ConcurrentInvoke(() =>
            {
                var processor = new TransactionProcessor(this.Context, this.splitwiseContext);

                var entity = this.Context.RecurringTransactions.GetEntity(id);

                this.validator.AccountType(entity.Account.Type);
                if (entity.ReceivingAccount != null)
                {
                    this.validator.AccountType(entity.ReceivingAccount.Type);
                }

                if (type != entity.Type) // TODO: Add test for this case
                {
                    throw new ValidationException("Changing the type of transaction is not possible.");
                }

                if (!updateInstances && startPeriod != entity.StartDate)
                {
                    throw new ValidationException($"Updating the start date without updating already created instances is not supported.");
                }

                var account = this.Context.Accounts.GetEntity(input.AccountId, false);

                this.validator.AccountType(account.Type);

                var category = input.CategoryId.Select(cId => this.Context.Categories.GetEntity(cId, false));

                AccountEntity receivingAccount = null;
                if (input.ReceivingAccountId.IsSome)
                {
                    receivingAccount = this.Context.Accounts.GetEntity(input.ReceivingAccountId.Value, false);

                    if (receivingAccount.Id == account.Id)
                    {
                        throw new ValidationException("Sender account can not be the same as receiver account.");
                    }

                    this.validator.AccountType(receivingAccount.Type);
                }

                // Verify a Splitwise account exists when adding providing splits.
                if (input.SplitwiseSplits.Any())
                {
                    this.Context.Accounts.GetSplitwiseEntity();
                }

                entity.AccountId = input.AccountId;
                entity.Account = account;
                entity.Description = input.Description;
                entity.StartDate = startPeriod;
                entity.EndDate = endPeriod.ToNullable();
                entity.Amount = input.Amount;
                entity.CategoryId = input.CategoryId.ToNullable();
                entity.Category = category.ToNullIfNone();
                entity.ReceivingAccountId = input.ReceivingAccountId.ToNullable();
                entity.ReceivingAccount = receivingAccount;
                entity.NeedsConfirmation = input.NeedsConfirmation;
                entity.Interval = input.Interval;
                entity.IntervalUnit = input.IntervalUnit;
                entity.SplitDetails = input.SplitwiseSplits.Select(s => s.ToSplitDetailEntity()).ToList();

                var instances = this.Context.Transactions.GetTransactionsFromRecurring(entity.Id);
                var instancesToUpdate = updateInstances
                    ? instances.ToList() // Copy the list
                                         // Always update all unprocessed transactions.
                    : instances.Where(t => !t.Processed).ToList();

                foreach (var instance in instancesToUpdate)
                {
                    processor.RevertIfProcessed(instance);
                    instances.Remove(instance);
                    this.Context.Remove(instance);
                }

                entity.LastOccurence = instances
                                       .OrderByDescending(t => t.Date)
                                       .FirstOrNone()
                                       .Select(t => (LocalDate?)t.Date)
                                       .ValueOrElse(() => (LocalDate?)null);
                entity.SetNextOccurrence();

                processor.Process(entity);

                this.Context.SaveChanges();

                return entity.AsRecurringTransaction();
            }));
        }
        /// <inheritdoc />
        public Transaction UpdateTransaction(int id, InputTransaction input)
        {
            this.validator.Description(input.Description);
            var date = this.validator.DateString(input.DateString, "date");
            var type = this.GetTransactionType(input.CategoryId, input.ReceivingAccountId, input.Amount);

            this.validator.Splits(this.splitwiseContext, input.PaymentRequests, input.SplitwiseSplits, type, input.Amount);

            return(this.ConcurrentInvoke(() =>
            {
                var processor = new TransactionProcessor(this.Context, this.splitwiseContext);

                var entity = this.Context.Transactions.GetEntity(id);

                if (!entity.FullyEditable)
                {
                    throw new ValidationException("This transaction should be updated in Splitwise.");
                }

                this.validator.AccountType(entity.Account.Type);
                if (entity.ReceivingAccount != null)
                {
                    this.validator.AccountType(entity.ReceivingAccount.Type);
                }

                if (type != entity.Type)
                {
                    throw new ValidationException("Changing the type of transaction is not possible.");
                }

                var account = this.Context.Accounts.GetEntity(input.AccountId, false);

                this.validator.AccountType(account.Type);

                var category = input.CategoryId.Select(cId => this.Context.Categories.GetEntity(cId, false));

                var receivingAccount = input.ReceivingAccountId.Select(aId => this.Context.Accounts.GetEntity(aId, false));
                if (receivingAccount.IsSome)
                {
                    this.validator.AccountType(receivingAccount.Value.Type);

                    if (receivingAccount.Value.Id == account.Id)
                    {
                        throw new ValidationException("Sender account can not be the same as receiver account.");
                    }
                }

                // Verify a Splitwise account exists when adding providing splits.
                if (input.SplitwiseSplits.Any())
                {
                    this.Context.Accounts.GetSplitwiseEntity();
                }

                processor.RevertIfProcessed(entity);

                entity.AccountId = input.AccountId;
                entity.Account = account;
                entity.Description = input.Description;
                entity.Date = date;
                entity.Amount = input.Amount;
                entity.CategoryId = input.CategoryId.ToNullable();
                entity.Category = category.ToNullIfNone();
                entity.ReceivingAccountId = input.ReceivingAccountId.ToNullable();
                entity.ReceivingAccount = receivingAccount.ToNullIfNone();
                entity.SplitDetails = input.SplitwiseSplits.Select(s => s.ToSplitDetailEntity()).ToList();

                var existingPaymentRequestIds = input.PaymentRequests
                                                .SelectSome(pr => pr.Id)
                                                .ToSet();
                var existingPaymentRequests = entity.PaymentRequests
                                              .Where(pr => existingPaymentRequestIds.Contains(pr.Id))
                                              .ToDictionary(pr => pr.Id);

                var updatedPaymentRequests = new List <PaymentRequestEntity>();

                foreach (var inputPr in input.PaymentRequests)
                {
                    var updatedPr = inputPr.Id
                                    .Select(prId => existingPaymentRequests[prId])
                                    .ValueOrElse(new PaymentRequestEntity());

                    if (updatedPr.PaidCount > inputPr.Count)
                    {
                        throw new ValidationException("A payment request can not be updated resulting in more payments than requested.");
                    }

                    updatedPr.Amount = inputPr.Amount;
                    updatedPr.Count = inputPr.Count;
                    updatedPr.Name = inputPr.Name;

                    updatedPaymentRequests.Add(updatedPr);
                }

                var updatedPaymentRequestIds = updatedPaymentRequests.Select(pr => pr.Id).ToSet();
                var removedPaymentRequests =
                    entity.PaymentRequests.Where(pr => !updatedPaymentRequestIds.Contains(pr.Id));

                this.Context.PaymentRequests.RemoveRange(removedPaymentRequests);
                entity.PaymentRequests = updatedPaymentRequests;

                processor.ProcessIfNeeded(entity);

                this.Context.SaveChanges();

                return entity.AsTransaction();
            }));
        }