/// <inheritdoc />
        public Transaction CompleteTransactionImport(int splitwiseId, int categoryId, Maybe <int> accountId)
        {
            var splitwiseTransaction = this.Context.SplitwiseTransactions.GetEntity(splitwiseId);
            var splitwiseAccount     = this.Context.Accounts.GetSplitwiseEntity();

            var category = this.Context.Categories.GetEntity(categoryId, allowObsolete: false);

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

                var account = accountId
                              .Select(id => this.Context.Accounts.GetEntity(id))
                              .ValueOrElse(splitwiseAccount);

                var transaction = splitwiseTransaction.ToTransaction(account, category);

                processor.ProcessIfNeeded(transaction);

                this.Context.Transactions.Add(transaction);

                this.Context.SaveChanges();

                return transaction.AsTransaction();
            }));
        }
        /// <inheritdoc />
        public Transaction CompleteTransferImport(int splitwiseId, int accountId)
        {
            var splitwiseTransaction = this.Context.SplitwiseTransactions.GetEntity(splitwiseId);
            var splitwiseAccount     = this.Context.Accounts.GetSplitwiseEntity();

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

                var account = this.Context.Accounts.GetEntity(accountId);
                if (account.Type != AccountType.Normal)
                {
                    throw new ValidationException("A normal account should be specified.");
                }

                var transaction = splitwiseTransaction.ToTransaction(splitwiseAccount, account);

                processor.ProcessIfNeeded(transaction);

                this.Context.Transactions.Add(transaction);

                this.Context.SaveChanges();

                return transaction.AsTransaction();
            }));
        }
        /// <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 Transaction ConfirmTransaction(int id, string dateString, decimal amount)
        {
            var date = this.validator.DateString(dateString, "date");

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

                var entity = this.Context.Transactions.GetEntity(id);
                this.validator.Amount(amount, entity.Type);

                if (!entity.NeedsConfirmation)
                {
                    throw new InvalidOperationException($"This transaction does not need to be confirmed.");
                }

                if (entity.Account.IsObsolete)
                {
                    throw new IsObsoleteException($"Account \"{entity.Account.Description}\" is obsolete.");
                }
                if (entity.CategoryId.HasValue && entity.Category.IsObsolete)
                {
                    throw new IsObsoleteException($"Category \"{entity.Category.Description}\" is obsolete.");
                }
                if (entity.ReceivingAccountId.HasValue && entity.ReceivingAccount.IsObsolete)
                {
                    throw new IsObsoleteException($"Account \"{entity.ReceivingAccount.Description}\" is obsolete.");
                }

                entity.Date = date;
                entity.Amount = amount;
                entity.IsConfirmed = true;

                processor.ProcessIfNeeded(entity);

                this.Context.SaveChanges();

                return entity.AsTransaction();
            }));
        }
        /// <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 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();
            }));
        }
        /// <inheritdoc />
        public Transaction CreateTransaction(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 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();
                }

                var entity = new TransactionEntity
                {
                    Description = input.Description,
                    Type = type,
                    Amount = input.Amount,
                    Date = date,
                    AccountId = input.AccountId,
                    Account = account,
                    Processed = false,
                    CategoryId = input.CategoryId.ToNullable(),
                    Category = category.ToNullIfNone(),
                    ReceivingAccountId = input.ReceivingAccountId.ToNullable(),
                    ReceivingAccount = receivingAccount.ToNullIfNone(),
                    NeedsConfirmation = input.NeedsConfirmation,
                    IsConfirmed = input.NeedsConfirmation ? false : (bool?)null,
                    PaymentRequests = input.PaymentRequests.Select(pr => pr.ToPaymentRequestEntity()).ToList(),
                    SplitDetails = input.SplitwiseSplits.Select(s => s.ToSplitDetailEntity()).ToList(),
                };

                processor.ProcessIfNeeded(entity);

                this.Context.Transactions.Add(entity);

                this.Context.SaveChanges();

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