import { ref, computed } from 'vue';
import { defineStore } from 'pinia';

import { useSettingsStore } from './setting.ts';
import { useUserStore } from './user.ts';
import { useAccountsStore } from './account.ts';
import { useTransactionCategoriesStore } from './transactionCategory.ts';
import { useOverviewStore } from './overview.ts';
import { useStatisticsStore } from './statistics.ts';
import { useExchangeRatesStore } from './exchangeRates.ts';

import { type BeforeResolveFunction, itemAndIndex, entries, keys } from '@/core/base.ts';
import { type TextualYearMonth, DateRange } from '@/core/datetime.ts';
import { CategoryType } from '@/core/category.ts';
import { TransactionType, TransactionTagFilterType } from '@/core/transaction.ts';
import { TRANSACTION_MIN_AMOUNT, TRANSACTION_MAX_AMOUNT } from '@/consts/transaction.ts';
import {
    type TransactionDraft,
    type TransactionCreateRequest,
    type TransactionInfoResponse,
    type TransactionPageWrapper,
    type TransactionReconciliationStatementResponse,
    Transaction,
    EMPTY_TRANSACTION_RESULT
} from '@/models/transaction.ts';
import type {
    TransactionPictureInfoBasicResponse
} from '@/models/transaction_picture_info.ts';
import {
    type ImportTransactionResponsePageWrapper,
    ImportTransaction
} from '@/models/imported_transaction.ts';
import {
    type ExportTransactionDataRequest
} from '@/models/data_management.ts';
import type {
    RecognizedReceiptImageResponse
} from '@/models/large_language_model.ts';

import {
    getUserTransactionDraft,
    updateUserTransactionDraft,
    clearUserTransactionDraft
} from '@/lib/userstate.ts';
import {
    isDefined,
    isNumber,
    isString,
    isArray1SubsetOfArray2,
    splitItemsToMap,
    countSplitItems
} from '@/lib/common.ts';
import {
    getTimezoneOffsetMinutes,
    getBrowserTimezoneOffsetMinutes,
    getActualUnixTimeForStore,
    parseDateTimeFromUnixTime
} from '@/lib/datetime.ts';
import { getAmountWithDecimalNumberCount } from '@/lib/numeral.ts';
import { getCurrencyFraction } from '@/lib/currency.ts';
import { getFirstAvailableCategoryId } from '@/lib/category.ts';
import services, { type ApiResponsePromise } from '@/lib/services.ts';
import logger from '@/lib/logger.ts';

export interface TransactionListPartialFilter {
    dateType?: number;
    maxTime?: number;
    minTime?: number;
    type?: number;
    categoryIds?: string;
    accountIds?: string;
    tagIds?: string;
    tagFilterType?: number;
    amountFilter?: string;
    keyword?: string;
}

export interface TransactionListFilter extends TransactionListPartialFilter {
    dateType: number;
    maxTime: number;
    minTime: number;
    type: number;
    categoryIds: string;
    accountIds: string;
    tagIds: string;
    tagFilterType: number;
    amountFilter: string;
    keyword: string;
}

export interface TransactionTotalAmount {
    expense: number;
    incompleteExpense: boolean;
    income: number;
    incompleteIncome: boolean;
}

export interface TransactionMonthList {
    readonly year: number;
    readonly month: number; // 1-based (1 = January, 12 = December)
    readonly yearDashMonth: TextualYearMonth;
    opened: boolean;
    readonly items: Transaction[];
    readonly totalAmount: TransactionTotalAmount;
    readonly dailyTotalAmounts: Record<string, TransactionTotalAmount>;
}

export const useTransactionsStore = defineStore('transactions', () => {
    const settingsStore = useSettingsStore();
    const userStore = useUserStore();
    const accountsStore = useAccountsStore();
    const transactionCategoriesStore = useTransactionCategoriesStore();
    const overviewStore = useOverviewStore();
    const statisticsStore = useStatisticsStore();
    const exchangeRatesStore = useExchangeRatesStore();

    const transactionDraft = ref<TransactionDraft | null>(getUserTransactionDraft());

    const transactionsFilter = ref<TransactionListFilter>({
        dateType: DateRange.All.type,
        maxTime: 0,
        minTime: 0,
        type: 0,
        categoryIds: '',
        accountIds: '',
        tagIds: '',
        tagFilterType: TransactionTagFilterType.Default.type,
        amountFilter: '',
        keyword: ''
    });

    const transactions = ref<TransactionMonthList[]>([]);
    const transactionsNextTimeId = ref<number>(0);
    const transactionListStateInvalid = ref<boolean>(true);
    const transactionReconciliationStatementStateInvalid = ref<boolean>(true);

    const allFilterCategoryIds = computed<Record<string, boolean>>(() => splitItemsToMap(transactionsFilter.value.categoryIds, ','));
    const allFilterAccountIds = computed<Record<string, boolean>>(() => splitItemsToMap(transactionsFilter.value.accountIds, ','));
    const allFilterTagIds = computed<Record<string, boolean>>(() => splitItemsToMap(transactionsFilter.value.tagIds, ','));

    const allFilterCategoryIdsCount = computed<number>(() => countSplitItems(transactionsFilter.value.categoryIds, ','));
    const allFilterAccountIdsCount = computed<number>(() => countSplitItems(transactionsFilter.value.accountIds, ','));
    const allFilterTagIdsCount = computed<number>(() => countSplitItems(transactionsFilter.value.tagIds, ','));

    const noTransaction = computed<boolean>(() => {
        for (const transactionMonthList of transactions.value) {
            for (const transaction of transactionMonthList.items) {
                if (transaction) {
                    return false;
                }
            }
        }

        return true;
    });

    const hasMoreTransaction = computed<boolean>(() => {
        return transactionsNextTimeId.value > 0;
    });

    function loadTransactionList({ transactionPageWrapper, reload, autoExpand, defaultCurrency, nextTimeSequenceId }: { transactionPageWrapper: TransactionPageWrapper, reload: boolean, autoExpand: boolean, defaultCurrency: string, nextTimeSequenceId?: number }): void {
        if (reload) {
            transactions.value = [];
        }

        if (transactionPageWrapper.items && transactionPageWrapper.items.length) {
            const currentUtcOffset = getTimezoneOffsetMinutes(settingsStore.appSettings.timeZone);
            let currentMonthListIndex = -1;
            let currentMonthList: TransactionMonthList | null = null;

            for (const [item, index] of itemAndIndex(transactionPageWrapper.items)) {
                fillTransactionObject(item, currentUtcOffset);

                const transactionTime = parseDateTimeFromUnixTime(item.time, item.utcOffset, currentUtcOffset);
                const transactionYear = transactionTime.getGregorianCalendarYear();
                const transactionMonth = transactionTime.getGregorianCalendarMonth();
                const transactionYearDashMonth = transactionTime.getGregorianCalendarYearDashMonth();

                if (index === 0 && transactions.value.length > 0) {
                    const lastMonthList = transactions.value[transactions.value.length - 1] as TransactionMonthList;

                    if (lastMonthList.totalAmount.incompleteExpense || lastMonthList.totalAmount.incompleteIncome) {
                        // calculate the total amount of last month which has incomplete total amount before starting to process a new request
                        calculateMonthTotalAmount(lastMonthList, defaultCurrency, transactionsFilter.value.accountIds, false);
                    }
                }

                if (currentMonthList && currentMonthList.year === transactionYear && currentMonthList.month === transactionMonth) {
                    currentMonthList.items.push(Object.freeze(item));

                    if (index === transactionPageWrapper.items.length - 1) {
                        // calculate the total amount of current month when processing the last transaction item of this request
                        calculateMonthTotalAmount(currentMonthList, defaultCurrency, transactionsFilter.value.accountIds, true);
                    }
                    continue;
                }

                for (let j = currentMonthListIndex + 1; j < transactions.value.length; j++) {
                    if (transactions.value[j]!.year === transactionYear && transactions.value[j]!.month === transactionMonth) {
                        currentMonthListIndex = j;
                        currentMonthList = transactions.value[j] as TransactionMonthList;
                        break;
                    }
                }

                if (!currentMonthList || currentMonthList.year !== transactionYear || currentMonthList.month !== transactionMonth) {
                    // calculate the total amount of current month when processing the first transaction item of the next month
                    calculateMonthTotalAmount(currentMonthList, defaultCurrency, transactionsFilter.value.accountIds, false);

                    const monthList: TransactionMonthList = {
                        year: transactionYear,
                        month: transactionMonth,
                        yearDashMonth: transactionYearDashMonth,
                        opened: autoExpand,
                        items: [],
                        totalAmount: {
                            expense: 0,
                            incompleteExpense: true,
                            income: 0,
                            incompleteIncome: true
                        },
                        dailyTotalAmounts: {}
                    };

                    transactions.value.push(monthList);

                    currentMonthListIndex = transactions.value.length - 1;
                    currentMonthList = transactions.value[transactions.value.length - 1] as TransactionMonthList;
                }

                currentMonthList.items.push(Object.freeze(item));
                // init the total amount struct of current month when processing the first transaction item of current month
                calculateMonthTotalAmount(currentMonthList, defaultCurrency, transactionsFilter.value.accountIds, true);
            }
        }

        if (nextTimeSequenceId) {
            transactionsNextTimeId.value = nextTimeSequenceId;
        } else {
            calculateMonthTotalAmount(transactions.value[transactions.value.length - 1] as TransactionMonthList, defaultCurrency, transactionsFilter.value.accountIds, false);
            transactionsNextTimeId.value = -1;
        }
    }

    function updateTransactionInTransactionList({ currentTransaction, defaultCurrency }: { currentTransaction: Transaction, defaultCurrency: string }): void {
        const currentUtcOffset = getTimezoneOffsetMinutes(settingsStore.appSettings.timeZone);
        const transactionTime = parseDateTimeFromUnixTime(currentTransaction.time, currentTransaction.utcOffset, currentUtcOffset);
        const transactionYear = transactionTime.getGregorianCalendarYear();
        const transactionMonth = transactionTime.getGregorianCalendarMonth();

        for (const [transactionMonthList, monthIndex] of itemAndIndex(transactions.value)) {
            if (!transactionMonthList.items) {
                continue;
            }

            for (const [transaction, transactionIndex] of itemAndIndex(transactionMonthList.items)) {
                if (transaction.id === currentTransaction.id) {
                    fillTransactionObject(currentTransaction, currentUtcOffset);

                    if (transactionYear !== transactionMonthList.year ||
                        transactionMonth !== transactionMonthList.month ||
                        currentTransaction.gregorianCalendarDayOfMonth !== transaction.gregorianCalendarDayOfMonth) {
                        transactionListStateInvalid.value = true;
                        return;
                    }

                    if ((transactionsFilter.value.categoryIds && !allFilterCategoryIds.value[currentTransaction.categoryId]) ||
                        (transactionsFilter.value.accountIds && !allFilterAccountIds.value[currentTransaction.sourceAccountId] && !allFilterAccountIds.value[currentTransaction.destinationAccountId] &&
                            (!currentTransaction.sourceAccount || !allFilterAccountIds.value[currentTransaction.sourceAccount.parentId]) &&
                            (!currentTransaction.destinationAccount || !allFilterAccountIds.value[currentTransaction.destinationAccount.parentId])
                        )
                    ) {
                        transactionMonthList.items.splice(transactionIndex, 1);
                    } else {
                        transactionMonthList.items.splice(transactionIndex, 1, currentTransaction);
                    }

                    if (transactionMonthList.items.length < 1) {
                        transactions.value.splice(monthIndex, 1);
                    } else {
                        calculateMonthTotalAmount(transactionMonthList, defaultCurrency, transactionsFilter.value.accountIds, monthIndex >= transactions.value.length - 1 && transactionsNextTimeId.value > 0);
                    }

                    return;
                }
            }
        }
    }

    function removeTransactionFromTransactionList({ currentTransaction, defaultCurrency }: { currentTransaction: TransactionInfoResponse, defaultCurrency: string }): void {
        for (const [transactionMonthList, monthIndex] of itemAndIndex(transactions.value)) {
            if (!transactionMonthList.items ||
                transactionMonthList.items[0]!.time < currentTransaction.time ||
                transactionMonthList.items[transactionMonthList.items.length - 1]!.time > currentTransaction.time) {
                continue;
            }

            for (const [transaction, transactionIndex] of itemAndIndex(transactionMonthList.items)) {
                if (transaction.id === currentTransaction.id) {
                    transactionMonthList.items.splice(transactionIndex, 1);
                }
            }

            if (transactionMonthList.items.length < 1) {
                transactions.value.splice(monthIndex, 1);
            } else {
                calculateMonthTotalAmount(transactionMonthList, defaultCurrency, transactionsFilter.value.accountIds, monthIndex >= transactions.value.length - 1 && transactionsNextTimeId.value > 0);
            }
        }
    }

    function calculateMonthTotalAmount(transactionMonthList: TransactionMonthList | null, defaultCurrency: string, accountIds: string, incomplete: boolean): void {
        if (!transactionMonthList) {
            return;
        }

        let totalExpense = 0;
        let totalIncome = 0;
        let hasUnCalculatedTotalExpense = false;
        let hasUnCalculatedTotalIncome = false;
        const dailyTotalAmounts: Record<string, TransactionTotalAmount> = {};

        const allAccountIdsMap: Record<string, boolean> = {};
        let totalAccountIdsCount = 0;

        if (accountIds && accountIds !== '0') {
            const allAccountIdsArray = accountIds.split(',');

            for (const accountId of allAccountIdsArray) {
                if (accountId) {
                    allAccountIdsMap[accountId] = true;
                    totalAccountIdsCount++;
                }
            }
        }

        for (const transaction of transactionMonthList.items) {
            const transactionDay = isNumber(transaction.gregorianCalendarDayOfMonth) ? transaction.gregorianCalendarDayOfMonth.toString() : '0';
            let dailyTotalAmount = dailyTotalAmounts[transactionDay];

            if (!dailyTotalAmount) {
                dailyTotalAmount = {
                    expense: 0,
                    incompleteExpense: false,
                    income: 0,
                    incompleteIncome: false
                };
                dailyTotalAmounts[transactionDay] = dailyTotalAmount;
            }

            let amount = transaction.sourceAmount;
            let account = transaction.sourceAccount;

            if (totalAccountIdsCount > 0 && transaction.destinationAccount
                && (!allAccountIdsMap[transaction.sourceAccount?.id || ''] && !allAccountIdsMap[transaction.sourceAccount?.parentId || ''])
                && (allAccountIdsMap[transaction.destinationAccount.id] || allAccountIdsMap[transaction.destinationAccount.parentId])) {
                amount = transaction.destinationAmount;
                account = transaction.destinationAccount;
            }

            if (!account) {
                continue;
            }

            if (account.currency !== defaultCurrency) {
                const balance = exchangeRatesStore.getExchangedAmount(amount, account.currency, defaultCurrency);

                if (!isNumber(balance)) {
                    if (transaction.type === TransactionType.Expense) {
                        hasUnCalculatedTotalExpense = true;
                        dailyTotalAmount.incompleteExpense = true;
                    } else if (transaction.type === TransactionType.Income) {
                        hasUnCalculatedTotalIncome = true;
                        dailyTotalAmount.incompleteIncome = true;
                    }

                    continue;
                }

                amount = balance;
            }

            if (transaction.type === TransactionType.Expense) {
                totalExpense += amount;
                dailyTotalAmount.expense += amount;
            } else if (transaction.type === TransactionType.Income) {
                totalIncome += amount;
                dailyTotalAmount.income += amount;
            } else if (transaction.type === TransactionType.Transfer && totalAccountIdsCount > 0) {
                if (allAccountIdsMap[transaction.sourceAccountId] && allAccountIdsMap[transaction.destinationAccountId]) {
                    // Do Nothing
                } else if (transaction.sourceAccount && transaction.destinationAccount && allAccountIdsMap[transaction.sourceAccount.parentId] && allAccountIdsMap[transaction.destinationAccount.parentId]) {
                    // Do Nothing
                } else if (transaction.sourceAccount && allAccountIdsMap[transaction.sourceAccount.parentId] && allAccountIdsMap[transaction.destinationAccountId]) {
                    // Do Nothing
                } else if (transaction.destinationAccount && allAccountIdsMap[transaction.sourceAccountId] && allAccountIdsMap[transaction.destinationAccount.parentId]) {
                    // Do Nothing
                } else if (allAccountIdsMap[transaction.sourceAccountId] || (transaction.sourceAccount && allAccountIdsMap[transaction.sourceAccount.parentId])) {
                    totalExpense += amount;
                    dailyTotalAmount.expense += amount;
                } else if (allAccountIdsMap[transaction.destinationAccountId] || (transaction.destinationAccount && allAccountIdsMap[transaction.destinationAccount.parentId])) {
                    totalIncome += amount;
                    dailyTotalAmount.income += amount;
                }
            }
        }

        transactionMonthList.totalAmount.expense = Math.trunc(totalExpense);
        transactionMonthList.totalAmount.incompleteExpense = incomplete || hasUnCalculatedTotalExpense;
        transactionMonthList.totalAmount.income = Math.trunc(totalIncome);
        transactionMonthList.totalAmount.incompleteIncome = incomplete || hasUnCalculatedTotalIncome;

        for (const day of keys(transactionMonthList.dailyTotalAmounts)) {
            delete transactionMonthList.dailyTotalAmounts[day];
        }

        for (const [day, dailyTotalAmount] of entries(dailyTotalAmounts)) {
            transactionMonthList.dailyTotalAmounts[day] = {
                expense: Math.trunc(dailyTotalAmount.expense),
                incompleteExpense: incomplete || dailyTotalAmount.incompleteExpense,
                income: Math.trunc(dailyTotalAmount.income),
                incompleteIncome: incomplete || dailyTotalAmount.incompleteIncome
            };
        }
    }

    function fillTransactionObject(transaction: Transaction, currentUtcOffset: number): void {
        if (!transaction) {
            return;
        }

        const transactionTime = parseDateTimeFromUnixTime(transaction.time, transaction.utcOffset, currentUtcOffset);
        transaction.setDisplayDate(transactionTime.getGregorianCalendarYearDashMonthDashDay(), transactionTime.getGregorianCalendarDay(), transactionTime.getWeekDay());

        if (transaction.sourceAccountId) {
            transaction.setSourceAccount(accountsStore.allAccountsMap[transaction.sourceAccountId]);
        }

        if (transaction.destinationAccountId) {
            transaction.setDestinationAccount(accountsStore.allAccountsMap[transaction.destinationAccountId]);
        }

        if (transaction.categoryId) {
            transaction.setCategory(transactionCategoriesStore.allTransactionCategoriesMap[transaction.categoryId]);
        }
    }

    function initTransactionDraft(): void {
        if (settingsStore.appSettings.autoSaveTransactionDraft === 'enabled' || settingsStore.appSettings.autoSaveTransactionDraft === 'confirmation') {
            transactionDraft.value = getUserTransactionDraft();
        } else {
            transactionDraft.value = null;
        }
    }

    function isTransactionDraftModified(transaction?: Transaction, initAmount?: number, initCategoryId?: string, initAccountId?: string, initTagIds?: string, firstVisibleAccountId?: string): boolean {
        if (!transaction) {
            return false;
        }

        if (transaction.sourceAmount !== 0 && transaction.sourceAmount !== initAmount) {
            return true;
        }

        if (transaction.type === TransactionType.Transfer && transaction.destinationAmount !== 0) {
            return true;
        }

        if (transaction.sourceAccountId && transaction.sourceAccountId !== '0' && transaction.sourceAccountId !== userStore.currentUserDefaultAccountId && ((userStore.currentUserDefaultAccountId !== '' && userStore.currentUserDefaultAccountId !== '0') || transaction.sourceAccountId !== firstVisibleAccountId) && transaction.sourceAccountId !== initAccountId) {
            return true;
        }

        if (transaction.type === TransactionType.Transfer && transaction.destinationAccountId && transaction.destinationAccountId !== '0' && transaction.destinationAccountId !== userStore.currentUserDefaultAccountId && transaction.destinationAccountId !== initAccountId) {
            return true;
        }

        const allCategories = transactionCategoriesStore.allTransactionCategories;

        if (allCategories) {
            if (transaction.type === TransactionType.Expense) {
                const defaultCategoryId = getFirstAvailableCategoryId(allCategories[CategoryType.Expense]);

                if (transaction.expenseCategoryId && transaction.expenseCategoryId !== '0' && transaction.expenseCategoryId !== defaultCategoryId && transaction.expenseCategoryId !== initCategoryId) {
                    return true;
                }
            } else if (transaction.type === TransactionType.Income) {
                const defaultCategoryId = getFirstAvailableCategoryId(allCategories[CategoryType.Income]);

                if (transaction.incomeCategoryId && transaction.incomeCategoryId !== '0' && transaction.incomeCategoryId !== defaultCategoryId && transaction.incomeCategoryId !== initCategoryId) {
                    return true;
                }
            } else if (transaction.type === TransactionType.Transfer) {
                const defaultCategoryId = getFirstAvailableCategoryId(allCategories[CategoryType.Transfer]);

                if (transaction.transferCategoryId && transaction.transferCategoryId !== '0' && transaction.transferCategoryId !== defaultCategoryId && transaction.transferCategoryId !== initCategoryId) {
                    return true;
                }
            }
        }

        if (transaction.hideAmount) {
            return true;
        }

        if (transaction.tagIds && transaction.tagIds.length > 0) {
            return !initTagIds || !isArray1SubsetOfArray2(transaction.tagIds, initTagIds.split(','));
        }

        if (transaction.pictures && transaction.pictures.length > 0) {
            return true;
        }

        if (transaction.comment && transaction.comment.trim()) {
            return true;
        }

        return false;
    }

    function saveTransactionDraft(transaction?: Transaction, initAmount?: number, initCategoryId?: string, initAccountId?: string, initTagIds?: string, firstVisibleAccountId?: string): void {
        if (settingsStore.appSettings.autoSaveTransactionDraft !== 'enabled' && settingsStore.appSettings.autoSaveTransactionDraft !== 'confirmation') {
            clearTransactionDraft();
            return;
        }

        if (transaction) {
            if (!isTransactionDraftModified(transaction, initAmount, initCategoryId, initAccountId, initTagIds, firstVisibleAccountId)) {
                clearTransactionDraft();
                return;
            }

            transactionDraft.value = transaction.toTransactionDraft();
        }

        updateUserTransactionDraft(transactionDraft.value);
    }

    function clearTransactionDraft(): void {
        transactionDraft.value = null;
        clearUserTransactionDraft();
    }

    function setTransactionSuitableDestinationAmount(transaction: Transaction, oldValue: number, newValue: number): void {
        if (transaction.type === TransactionType.Expense || transaction.type === TransactionType.Income) {
            transaction.destinationAmount = newValue;
        } else if (transaction.type === TransactionType.Transfer) {
            const sourceAccount = accountsStore.allAccountsMap[transaction.sourceAccountId];
            const destinationAccount = accountsStore.allAccountsMap[transaction.destinationAccountId];

            if (sourceAccount && destinationAccount && sourceAccount.currency !== destinationAccount.currency) {
                const decimalNumberCount = getCurrencyFraction(destinationAccount.currency);
                const exchangedOldValue = exchangeRatesStore.getExchangedAmount(oldValue, sourceAccount.currency, destinationAccount.currency);
                const exchangedNewValue = exchangeRatesStore.getExchangedAmount(newValue, sourceAccount.currency, destinationAccount.currency);

                if (isNumber(decimalNumberCount) && isNumber(exchangedOldValue)) {
                    oldValue = Math.trunc(exchangedOldValue);
                    oldValue = getAmountWithDecimalNumberCount(oldValue, decimalNumberCount);
                }

                if (isNumber(decimalNumberCount) && isNumber(exchangedNewValue)) {
                    newValue = Math.trunc(exchangedNewValue);
                    newValue = getAmountWithDecimalNumberCount(newValue, decimalNumberCount);
                } else {
                    return;
                }
            }

            if ((!sourceAccount || !destinationAccount || transaction.destinationAmount === oldValue || transaction.destinationAmount === 0) &&
                (TRANSACTION_MIN_AMOUNT <= newValue && newValue <= TRANSACTION_MAX_AMOUNT)) {
                transaction.destinationAmount = newValue;
            }
        }
    }

    function updateTransactionListInvalidState(invalidState: boolean): void {
        transactionListStateInvalid.value = invalidState;
    }

    function updateTransactionReconciliationStatementInvalidState(invalidState: boolean): void {
        transactionReconciliationStatementStateInvalid.value = invalidState;
    }

    function resetTransactions(): void {
        transactionsFilter.value.dateType = DateRange.All.type;
        transactionsFilter.value.maxTime = 0;
        transactionsFilter.value.minTime = 0;
        transactionsFilter.value.type = 0;
        transactionsFilter.value.categoryIds = '';
        transactionsFilter.value.accountIds = '';
        transactionsFilter.value.tagIds = '';
        transactionsFilter.value.tagFilterType = TransactionTagFilterType.Default.type;
        transactionsFilter.value.amountFilter = '';
        transactionsFilter.value.keyword = '';
        transactions.value = [];
        transactionsNextTimeId.value = 0;
        transactionListStateInvalid.value = true;
        transactionReconciliationStatementStateInvalid.value = true;
    }

    function clearTransactions(): void {
        transactions.value = [];
        transactionsNextTimeId.value = 0;
        transactionListStateInvalid.value = true;
    }

    function initTransactionListFilter(filter: TransactionListPartialFilter): void {
        if (filter && isNumber(filter.dateType)) {
            transactionsFilter.value.dateType = filter.dateType;
        } else {
            transactionsFilter.value.dateType = DateRange.All.type;
        }

        if (filter && isNumber(filter.maxTime)) {
            transactionsFilter.value.maxTime = filter.maxTime;
        } else {
            transactionsFilter.value.maxTime = 0;
        }

        if (filter && isNumber(filter.minTime)) {
            transactionsFilter.value.minTime = filter.minTime;
        } else {
            transactionsFilter.value.minTime = 0;
        }

        if (filter && isNumber(filter.type)) {
            transactionsFilter.value.type = filter.type;
        } else {
            transactionsFilter.value.type = 0;
        }

        if (filter && isString(filter.categoryIds)) {
            transactionsFilter.value.categoryIds = filter.categoryIds;
        } else {
            transactionsFilter.value.categoryIds = '';
        }

        if (filter && isString(filter.accountIds)) {
            transactionsFilter.value.accountIds = filter.accountIds;
        } else {
            transactionsFilter.value.accountIds = '';
        }

        if (filter && isString(filter.tagIds)) {
            transactionsFilter.value.tagIds = filter.tagIds;
        } else {
            transactionsFilter.value.tagIds = '';
        }

        if (filter && isNumber(filter.tagFilterType)) {
            transactionsFilter.value.tagFilterType = filter.tagFilterType;
        } else {
            transactionsFilter.value.tagFilterType = TransactionTagFilterType.Default.type;
        }

        if (filter && isString(filter.amountFilter)) {
            transactionsFilter.value.amountFilter = filter.amountFilter;
        } else {
            transactionsFilter.value.amountFilter = '';
        }

        if (filter && isString(filter.keyword)) {
            transactionsFilter.value.keyword = filter.keyword;
        } else {
            transactionsFilter.value.keyword = '';
        }
    }

    function updateTransactionListFilter(filter: TransactionListPartialFilter): boolean {
        let changed = false;

        if (filter && isNumber(filter.dateType) && transactionsFilter.value.dateType !== filter.dateType) {
            transactionsFilter.value.dateType = filter.dateType;
            changed = true;
        }

        if (filter && isNumber(filter.maxTime) && transactionsFilter.value.maxTime !== filter.maxTime) {
            transactionsFilter.value.maxTime = filter.maxTime;
            changed = true;
        }

        if (filter && isNumber(filter.minTime) && transactionsFilter.value.minTime !== filter.minTime) {
            transactionsFilter.value.minTime = filter.minTime;
            changed = true;
        }

        if (filter && isNumber(filter.type) && transactionsFilter.value.type !== filter.type) {
            transactionsFilter.value.type = filter.type;
            changed = true;
        }

        if (filter && isString(filter.categoryIds) && transactionsFilter.value.categoryIds !== filter.categoryIds) {
            transactionsFilter.value.categoryIds = filter.categoryIds;
            changed = true;
        }

        if (filter && isString(filter.accountIds) && transactionsFilter.value.accountIds !== filter.accountIds) {
            if (DateRange.isBillingCycle(transactionsFilter.value.dateType) &&
                (!accountsStore.getAccountStatementDate(filter.accountIds) || accountsStore.getAccountStatementDate(filter.accountIds) !== accountsStore.getAccountStatementDate(transactionsFilter.value.accountIds))) {
                transactionsFilter.value.dateType = DateRange.Custom.type;
            }

            transactionsFilter.value.accountIds = filter.accountIds;
            changed = true;
        }

        if (filter && isString(filter.tagIds) && transactionsFilter.value.tagIds !== filter.tagIds) {
            transactionsFilter.value.tagIds = filter.tagIds;
            changed = true;
        }

        if (filter && isNumber(filter.tagFilterType) && transactionsFilter.value.tagFilterType !== filter.tagFilterType) {
            transactionsFilter.value.tagFilterType = filter.tagFilterType;
            changed = true;
        }

        if (filter && isString(filter.amountFilter) && transactionsFilter.value.amountFilter !== filter.amountFilter) {
            transactionsFilter.value.amountFilter = filter.amountFilter;
            changed = true;
        }

        if (filter && isString(filter.keyword) && transactionsFilter.value.keyword !== filter.keyword) {
            transactionsFilter.value.keyword = filter.keyword;
            changed = true;
        }

        return changed;
    }

    function getTransactionListPageParams(pageType: number): string {
        const querys: string[] = [];

        querys.push('pageType=' + pageType);

        if (transactionsFilter.value.type) {
            querys.push('type=' + transactionsFilter.value.type);
        }

        if (transactionsFilter.value.accountIds) {
            querys.push('accountIds=' + transactionsFilter.value.accountIds);
        }

        if (transactionsFilter.value.categoryIds) {
            querys.push('categoryIds=' + transactionsFilter.value.categoryIds);
        }

        if (transactionsFilter.value.tagIds) {
            querys.push('tagIds=' + transactionsFilter.value.tagIds);
        }

        if (transactionsFilter.value.tagFilterType) {
            querys.push('tagFilterType=' + transactionsFilter.value.tagFilterType);
        }

        querys.push('dateType=' + transactionsFilter.value.dateType);

        if (DateRange.isBillingCycle(transactionsFilter.value.dateType) || transactionsFilter.value.dateType === DateRange.Custom.type) {
            querys.push('maxTime=' + transactionsFilter.value.maxTime);
            querys.push('minTime=' + transactionsFilter.value.minTime);
        }

        if (transactionsFilter.value.amountFilter) {
            querys.push('amountFilter=' + encodeURIComponent(transactionsFilter.value.amountFilter));
        }

        if (transactionsFilter.value.keyword) {
            querys.push('keyword=' + encodeURIComponent(transactionsFilter.value.keyword));
        }

        return querys.join('&');
    }

    function getExportTransactionDataRequestByTransactionFilter(): ExportTransactionDataRequest {
        return {
            maxTime: transactionsFilter.value.maxTime,
            minTime: transactionsFilter.value.minTime,
            type: transactionsFilter.value.type,
            categoryIds: transactionsFilter.value.categoryIds,
            accountIds: transactionsFilter.value.accountIds,
            tagIds: transactionsFilter.value.tagIds,
            tagFilterType: transactionsFilter.value.tagFilterType,
            amountFilter: transactionsFilter.value.amountFilter,
            keyword: transactionsFilter.value.keyword
        };
    }

    function loadTransactions({ reload, count, page, withCount, autoExpand, defaultCurrency }: { reload?: boolean, count?: number, page?: number, withCount?: boolean, autoExpand: boolean, defaultCurrency: string }): Promise<TransactionPageWrapper> {
        let actualMaxTime = transactionsNextTimeId.value;

        if (reload && transactionsFilter.value.maxTime > 0) {
            actualMaxTime = transactionsFilter.value.maxTime * 1000 + 999;
        } else if (reload && transactionsFilter.value.maxTime <= 0) {
            actualMaxTime = 0;
        }

        return new Promise((resolve, reject) => {
            services.getTransactions({
                maxTime: actualMaxTime,
                minTime: transactionsFilter.value.minTime * 1000,
                count: count || 50,
                page: page || 1,
                withCount: !!withCount,
                type: transactionsFilter.value.type,
                categoryIds: transactionsFilter.value.categoryIds,
                accountIds: transactionsFilter.value.accountIds,
                tagIds: transactionsFilter.value.tagIds,
                tagFilterType: transactionsFilter.value.tagFilterType,
                amountFilter: transactionsFilter.value.amountFilter,
                keyword: transactionsFilter.value.keyword
            }).then(response => {
                const data = response.data;

                if (!data || !data.success || !data.result) {
                    if (reload) {
                        loadTransactionList({
                            transactionPageWrapper: EMPTY_TRANSACTION_RESULT,
                            reload: reload,
                            autoExpand: autoExpand,
                            defaultCurrency: defaultCurrency
                        });

                        if (!transactionListStateInvalid.value) {
                            updateTransactionListInvalidState(true);
                        }
                    }

                    reject({ message: 'Unable to retrieve transaction list' });
                    return;
                }

                const transactionPageWrapper: TransactionPageWrapper = {
                    items: Transaction.ofMulti(data.result.items),
                    totalCount: data.result.totalCount
                };

                loadTransactionList({
                    transactionPageWrapper: transactionPageWrapper,
                    reload: !!reload,
                    autoExpand: autoExpand,
                    defaultCurrency: defaultCurrency,
                    nextTimeSequenceId: data.result.nextTimeSequenceId
                });

                if (reload) {
                    if (transactionListStateInvalid.value) {
                        updateTransactionListInvalidState(false);
                    }
                }

                resolve(transactionPageWrapper);
            }).catch(error => {
                logger.error('failed to load transaction list', error);

                if (reload) {
                    loadTransactionList({
                        transactionPageWrapper: EMPTY_TRANSACTION_RESULT,
                        reload: reload,
                        autoExpand: autoExpand,
                        defaultCurrency: defaultCurrency
                    });

                    if (!transactionListStateInvalid.value) {
                        updateTransactionListInvalidState(true);
                    }
                }

                if (error.response && error.response.data && error.response.data.errorMessage) {
                    reject({ error: error.response.data });
                } else if (!error.processed) {
                    reject({ message: 'Unable to retrieve transaction list' });
                } else {
                    reject(error);
                }
            });
        });
    }

    function loadMonthlyAllTransactions({ year, month, autoExpand, defaultCurrency }: { year: number, month: number, autoExpand: boolean, defaultCurrency: string }): Promise<TransactionPageWrapper> {
        return new Promise((resolve, reject) => {
            services.getAllTransactionsByMonth({
                year: year,
                month: month,
                type: transactionsFilter.value.type,
                categoryIds: transactionsFilter.value.categoryIds,
                accountIds: transactionsFilter.value.accountIds,
                tagIds: transactionsFilter.value.tagIds,
                tagFilterType: transactionsFilter.value.tagFilterType,
                amountFilter: transactionsFilter.value.amountFilter,
                keyword: transactionsFilter.value.keyword
            }).then(response => {
                const data = response.data;

                if (!data || !data.success || !data.result) {
                    loadTransactionList({
                        transactionPageWrapper: EMPTY_TRANSACTION_RESULT,
                        reload: true,
                        autoExpand: autoExpand,
                        defaultCurrency: defaultCurrency
                    });

                    if (!transactionListStateInvalid.value) {
                        updateTransactionListInvalidState(true);
                    }

                    reject({ message: 'Unable to retrieve transaction list' });
                    return;
                }

                const transactionPageWrapper: TransactionPageWrapper = {
                    items: Transaction.ofMulti(data.result.items),
                    totalCount: data.result.totalCount
                };

                loadTransactionList({
                    transactionPageWrapper: transactionPageWrapper,
                    reload: true,
                    autoExpand: autoExpand,
                    defaultCurrency: defaultCurrency
                });

                if (transactionListStateInvalid.value) {
                    updateTransactionListInvalidState(false);
                }

                resolve(transactionPageWrapper);
            }).catch(error => {
                logger.error('failed to load monthly all transaction list', error);

                loadTransactionList({
                    transactionPageWrapper: EMPTY_TRANSACTION_RESULT,
                    reload: true,
                    autoExpand: autoExpand,
                    defaultCurrency: defaultCurrency
                });

                if (!transactionListStateInvalid.value) {
                    updateTransactionListInvalidState(true);
                }

                if (error.response && error.response.data && error.response.data.errorMessage) {
                    reject({ error: error.response.data });
                } else if (!error.processed) {
                    reject({ message: 'Unable to retrieve transaction list' });
                } else {
                    reject(error);
                }
            });
        });
    }

    function getReconciliationStatements({ accountId, startTime, endTime }: { accountId: string, startTime: number, endTime: number }): Promise<TransactionReconciliationStatementResponse> {
        return new Promise((resolve, reject) => {
            services.getReconciliationStatements({
                accountId: accountId,
                startTime: startTime,
                endTime: endTime
            }).then(response => {
                const data = response.data;

                if (!data || !data.success || !data.result) {
                    if (!transactionReconciliationStatementStateInvalid.value) {
                        updateTransactionReconciliationStatementInvalidState(true);
                    }

                    reject({ message: 'Unable to retrieve reconciliation statements' });
                    return;
                }

                if (transactionReconciliationStatementStateInvalid.value) {
                    updateTransactionReconciliationStatementInvalidState(false);
                }

                resolve(data.result);
            }).catch(error => {
                logger.error('failed to load reconciliation statements', error);

                if (!transactionReconciliationStatementStateInvalid.value) {
                    updateTransactionReconciliationStatementInvalidState(true);
                }

                if (error.response && error.response.data && error.response.data.errorMessage) {
                    reject({ error: error.response.data });
                } else if (!error.processed) {
                    reject({ message: 'Unable to retrieve reconciliation statements' });
                } else {
                    reject(error);
                }
            });
        });
    }

    function getTransaction({ transactionId, withPictures }: { transactionId: string, withPictures?: boolean }): Promise<Transaction> {
        return new Promise((resolve, reject) => {
            if (!isDefined(withPictures)) {
                withPictures = true;
            }

            services.getTransaction({
                id: transactionId,
                withPictures: withPictures
            }).then(response => {
                const data = response.data;

                if (!data || !data.success || !data.result) {
                    reject({ message: 'Unable to retrieve transaction' });
                    return;
                }

                const transaction = Transaction.of(data.result);

                resolve(transaction);
            }).catch(error => {
                logger.error('failed to load transaction info', error);

                if (error.response && error.response.data && error.response.data.errorMessage) {
                    reject({ error: error.response.data });
                } else if (!error.processed) {
                    reject({ message: 'Unable to retrieve transaction' });
                } else {
                    reject(error);
                }
            });
        });
    }

    function saveTransaction({ transaction, defaultCurrency, isEdit, clientSessionId }: { transaction: Transaction, defaultCurrency: string, isEdit: boolean, clientSessionId: string }): Promise<Transaction> {
        return new Promise((resolve, reject) => {
            const actualTime = getActualUnixTimeForStore(transaction.time, transaction.utcOffset, getBrowserTimezoneOffsetMinutes());
            let promise: ApiResponsePromise<TransactionInfoResponse>;

            if (transaction.type !== TransactionType.Expense &&
                transaction.type !== TransactionType.Income &&
                transaction.type !== TransactionType.Transfer &&
                transaction.type !== TransactionType.ModifyBalance) {
                reject({ message: 'An error occurred' });
                return;
            } else if (!isEdit && transaction.type === TransactionType.ModifyBalance) {
                reject({ message: 'An error occurred' });
                return;
            }

            if (!isEdit) {
                promise = services.addTransaction(transaction.toCreateRequest(clientSessionId, actualTime));
            } else {
                promise = services.modifyTransaction(transaction.toModifyRequest(actualTime));
            }

            promise.then(response => {
                const data = response.data;

                if (!data || !data.success || !data.result) {
                    if (!isEdit) {
                        reject({ message: 'Unable to add transaction' });
                    } else {
                        reject({ message: 'Unable to save transaction' });
                    }
                }

                const transaction = Transaction.of(data.result);

                if (!isEdit) {
                    if (!transactionListStateInvalid.value) {
                        updateTransactionListInvalidState(true);
                    }
                } else {
                    updateTransactionInTransactionList({
                        currentTransaction: transaction,
                        defaultCurrency: defaultCurrency
                    });
                }

                if (!transactionReconciliationStatementStateInvalid.value) {
                    updateTransactionReconciliationStatementInvalidState(true);
                }

                if (!accountsStore.accountListStateInvalid) {
                    accountsStore.updateAccountListInvalidState(true);
                }

                if (!overviewStore.transactionOverviewStateInvalid) {
                    overviewStore.updateTransactionOverviewInvalidState(true);
                }

                if (!statisticsStore.transactionStatisticsStateInvalid) {
                    statisticsStore.updateTransactionStatisticsInvalidState(true);
                }

                resolve(transaction);
            }).catch(error => {
                logger.error('failed to save transaction', error);

                if (error.response && error.response.data && error.response.data.errorMessage) {
                    reject({ error: error.response.data });
                } else if (!error.processed) {
                    if (!isEdit) {
                        reject({ message: 'Unable to add transaction' });
                    } else {
                        reject({ message: 'Unable to save transaction' });
                    }
                } else {
                    reject(error);
                }
            });
        });
    }

    function deleteTransaction({ transaction, defaultCurrency, beforeResolve }: { transaction: TransactionInfoResponse, defaultCurrency: string, beforeResolve?: BeforeResolveFunction }): Promise<boolean> {
        return new Promise((resolve, reject) => {
            services.deleteTransaction({
                id: transaction.id
            }).then(response => {
                const data = response.data;

                if (!data || !data.success || !data.result) {
                    reject({ message: 'Unable to delete this transaction' });
                    return;
                }

                if (beforeResolve) {
                    beforeResolve(() => {
                        removeTransactionFromTransactionList({
                            currentTransaction: transaction,
                            defaultCurrency: defaultCurrency
                        });
                    });
                } else {
                    removeTransactionFromTransactionList({
                        currentTransaction: transaction,
                        defaultCurrency: defaultCurrency
                    });
                }

                if (!transactionReconciliationStatementStateInvalid.value) {
                    updateTransactionReconciliationStatementInvalidState(true);
                }

                if (!accountsStore.accountListStateInvalid) {
                    accountsStore.updateAccountListInvalidState(true);
                }

                if (!overviewStore.transactionOverviewStateInvalid) {
                    overviewStore.updateTransactionOverviewInvalidState(true);
                }

                if (!statisticsStore.transactionStatisticsStateInvalid) {
                    statisticsStore.updateTransactionStatisticsInvalidState(true);
                }

                resolve(data.result);
            }).catch(error => {
                logger.error('failed to delete transaction', error);

                if (error.response && error.response.data && error.response.data.errorMessage) {
                    reject({ error: error.response.data });
                } else if (!error.processed) {
                    reject({ message: 'Unable to delete this transaction' });
                } else {
                    reject(error);
                }
            });
        });
    }

    function recognizeReceiptImage({ imageFile }: { imageFile: File }): Promise<RecognizedReceiptImageResponse> {
        return new Promise((resolve, reject) => {
            services.recognizeReceiptImage({ imageFile }).then(response => {
                const data = response.data;

                if (!data || !data.success || !data.result) {
                    reject({ message: 'Unable to recognize image' });
                    return;
                }

                resolve(data.result);
            }).catch(error => {
                logger.error('failed to recognize image', error);

                if (error.response && error.response.data && error.response.data.errorMessage) {
                    reject({ error: error.response.data });
                } else if (!error.processed) {
                    reject({ message: 'Unable to recognize image' });
                } else {
                    reject(error);
                }
            });
        });
    }

    function parseImportDsvFile({ fileType, fileEncoding, importFile }: { fileType: string, fileEncoding?: string, importFile: File }): Promise<string[][]> {
        return new Promise((resolve, reject) => {
            services.parseImportDsvFile({ fileType, fileEncoding, importFile }).then(response => {
                const data = response.data;

                if (!data || !data.success || !data.result) {
                    reject({ message: 'Unable to parse import file' });
                    return;
                }

                resolve(data.result);
            }).catch(error => {
                logger.error('Unable to parse import file', error);

                if (error.response && error.response.data && error.response.data.errorMessage) {
                    reject({ error: error.response.data });
                } else if (!error.processed) {
                    reject({ message: 'Unable to parse import file' });
                } else {
                    reject(error);
                }
            });
        });
    }

    function parseImportTransaction({ fileType, fileEncoding, importFile, columnMapping, transactionTypeMapping, hasHeaderLine, timeFormat, timezoneFormat, amountDecimalSeparator, amountDigitGroupingSymbol, geoSeparator, geoOrder, tagSeparator }: { fileType: string, fileEncoding?: string, importFile: File, columnMapping?: Record<number, number>, transactionTypeMapping?: Record<string, TransactionType>, hasHeaderLine?: boolean, timeFormat?: string, timezoneFormat?: string, amountDecimalSeparator?: string, amountDigitGroupingSymbol?: string, geoSeparator?: string, geoOrder?: string, tagSeparator?: string }): Promise<ImportTransactionResponsePageWrapper> {
        return new Promise((resolve, reject) => {
            services.parseImportTransaction({ fileType, fileEncoding, importFile, columnMapping, transactionTypeMapping, hasHeaderLine, timeFormat, timezoneFormat, amountDecimalSeparator, amountDigitGroupingSymbol, geoSeparator, geoOrder, tagSeparator }).then(response => {
                const data = response.data;

                if (!data || !data.success || !data.result) {
                    reject({ message: 'Unable to parse import file' });
                    return;
                }

                resolve(data.result);
            }).catch(error => {
                logger.error('Unable to parse import file', error);

                if (error.response && error.response.data && error.response.data.errorMessage) {
                    reject({ error: error.response.data });
                } else if (!error.processed) {
                    reject({ message: 'Unable to parse import file' });
                } else {
                    reject(error);
                }
            });
        });
    }

    function importTransactions({ transactions, clientSessionId }: { transactions: ImportTransaction[], clientSessionId: string }): Promise<number> {
        const submitTransactions: TransactionCreateRequest[] = [];

        if (transactions) {
            for (const transaction of transactions) {
                const submitTransaction = transaction.toCreateRequest();
                submitTransactions.push(submitTransaction);
            }
        }

        return new Promise((resolve, reject) => {
            services.importTransactions({
                transactions: submitTransactions,
                clientSessionId: clientSessionId
            }).then(response => {
                const data = response.data;

                if (!data || !data.success || !data.result) {
                    reject({ message: 'Unable to import transactions' });
                    return;
                }

                resolve(data.result);
            }).catch(error => {
                logger.error('Unable to import transactions', error);

                if (error.response && error.response.data && error.response.data.errorMessage) {
                    reject({ error: error.response.data });
                } else if (!error.processed) {
                    reject({ message: 'Unable to import transactions' });
                } else {
                    reject(error);
                }
            });
        });
    }

    function getImportTransactionsProcess({ clientSessionId }: { clientSessionId: string }): Promise<number | null> {
        return new Promise((resolve, reject) => {
            services.getImportTransactionsProcess(clientSessionId).then(response => {
                const data = response.data;

                if (!data || !data.success || !data.result) {
                    reject({ message: 'Unable to get transactions import process' });
                    return;
                }

                resolve(data.result);
            }).catch(error => {
                logger.error('Unable to get transactions import process', error);

                if (error.response && error.response.data && error.response.data.errorMessage) {
                    reject({ error: error.response.data });
                } else if (!error.processed) {
                    reject({ message: 'Unable to get transactions import process' });
                } else {
                    reject(error);
                }
            });
        });
    }

    function uploadTransactionPicture({ pictureFile, clientSessionId }: { pictureFile: File, clientSessionId?: string }): Promise<TransactionPictureInfoBasicResponse> {
        return new Promise((resolve, reject) => {
            services.uploadTransactionPicture({ pictureFile, clientSessionId }).then(response => {
                const data = response.data;

                if (!data || !data.success || !data.result) {
                    reject({ message: 'Unable to upload transaction picture' });
                    return;
                }

                resolve(data.result);
            }).catch(error => {
                logger.error('Unable to upload transaction picture', error);

                if (error.response && error.response.data && error.response.data.errorMessage) {
                    reject({ error: error.response.data });
                } else if (!error.processed) {
                    reject({ message: 'Unable to upload transaction picture' });
                } else {
                    reject(error);
                }
            });
        });
    }

    function removeUnusedTransactionPicture({ pictureInfo }: { pictureInfo: TransactionPictureInfoBasicResponse }): Promise<boolean> {
        return new Promise((resolve, reject) => {
            services.removeUnusedTransactionPicture({ id: pictureInfo.pictureId }).then(response => {
                const data = response.data;

                if (!data || !data.success || !data.result) {
                    reject({ message: 'Unable to remove transaction picture' });
                    return;
                }

                resolve(data.result);
            }).catch(error => {
                logger.error('failed to remove transaction picture', error);

                if (error.response && error.response.data && error.response.data.errorMessage) {
                    reject({ error: error.response.data });
                } else if (!error.processed) {
                    reject({ message: 'Unable to remove transaction picture' });
                } else {
                    reject(error);
                }
            });
        });
    }

    function getTransactionPictureUrl(pictureInfo?: TransactionPictureInfoBasicResponse | null, disableBrowserCache?: boolean | string): string | undefined {
        if (!pictureInfo || !pictureInfo.originalUrl) {
            return undefined;
        }

        return services.getTransactionPictureUrlWithToken(pictureInfo.originalUrl, disableBrowserCache);
    }

    function collapseMonthInTransactionList({ monthList, collapse }: { monthList: TransactionMonthList, collapse: boolean }): void {
        if (monthList) {
            monthList.opened = !collapse;
        }
    }

    return {
        // states
        transactionDraft,
        transactionsFilter,
        transactions,
        transactionsNextTimeId,
        transactionListStateInvalid,
        transactionReconciliationStatementStateInvalid,
        // computed states
        allFilterCategoryIds,
        allFilterAccountIds,
        allFilterTagIds,
        allFilterCategoryIdsCount,
        allFilterAccountIdsCount,
        allFilterTagIdsCount,
        noTransaction,
        hasMoreTransaction,
        // functions
        initTransactionDraft,
        isTransactionDraftModified,
        saveTransactionDraft,
        clearTransactionDraft,
        setTransactionSuitableDestinationAmount,
        updateTransactionListInvalidState,
        updateTransactionReconciliationStatementInvalidState,
        resetTransactions,
        clearTransactions,
        initTransactionListFilter,
        updateTransactionListFilter,
        getTransactionListPageParams,
        getExportTransactionDataRequestByTransactionFilter,
        loadTransactions,
        loadMonthlyAllTransactions,
        getReconciliationStatements,
        getTransaction,
        saveTransaction,
        deleteTransaction,
        recognizeReceiptImage,
        parseImportDsvFile,
        parseImportTransaction,
        importTransactions,
        getImportTransactionsProcess,
        uploadTransactionPicture,
        removeUnusedTransactionPicture,
        getTransactionPictureUrl,
        collapseMonthInTransactionList
    };
});
