import React, { useState, useRef, useMemo } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { Redirect, useParams, useHistory, useLocation } from 'react-router-dom';

import { debounce } from 'debounce';
import { bindActionCreators } from 'redux';

import * as actions from 'loot-core/src/client/actions';
import {
  SchedulesProvider,
  useCachedSchedules,
} from 'loot-core/src/client/data-hooks/schedules';
import * as queries from 'loot-core/src/client/queries';
import q, { runQuery, pagedQuery } from 'loot-core/src/client/query-helpers';
import { send, listen } from 'loot-core/src/platform/client/fetch';
import { currentDay } from 'loot-core/src/shared/months';
import {
  deleteTransaction,
  updateTransaction,
  realizeTempTransactions,
  ungroupTransaction,
  ungroupTransactions,
} from 'loot-core/src/shared/transactions';
import {
  currencyToInteger,
  applyChanges,
  groupById,
} from 'loot-core/src/shared/util';
import {
  View,
  Text,
  Button,
  Input,
  InputWithContent,
  InitialFocus,
  Tooltip,
  Menu,
  Stack,
} from 'loot-design/src/components/common';
import { KeyHandlers } from 'loot-design/src/components/KeyHandlers';
import NotesButton from 'loot-design/src/components/NotesButton';
import CellValue from 'loot-design/src/components/spreadsheet/CellValue';
import format from 'loot-design/src/components/spreadsheet/format';
import useSheetValue from 'loot-design/src/components/spreadsheet/useSheetValue';
import { SelectedItemsButton } from 'loot-design/src/components/table';
import {
  SelectedProviderWithItems,
  useSelectedItems,
} from 'loot-design/src/components/useSelected';
import { styles, colors } from 'loot-design/src/style';
import Loading from 'loot-design/src/svg/AnimatedLoading';
import Add from 'loot-design/src/svg/v1/Add';
import DotsHorizontalTriple from 'loot-design/src/svg/v1/DotsHorizontalTriple';
import ArrowButtonRight1 from 'loot-design/src/svg/v2/ArrowButtonRight1';
import ArrowsExpand3 from 'loot-design/src/svg/v2/ArrowsExpand3';
import ArrowsShrink3 from 'loot-design/src/svg/v2/ArrowsShrink3';
import CheckCircle1 from 'loot-design/src/svg/v2/CheckCircle1';
import DownloadThickBottom from 'loot-design/src/svg/v2/DownloadThickBottom';
import Pencil1 from 'loot-design/src/svg/v2/Pencil1';
import SvgRemove from 'loot-design/src/svg/v2/Remove';
import SearchAlternate from 'loot-design/src/svg/v2/SearchAlternate';

import { authorizeBank } from '../../plaid';
import { useActiveLocation } from '../ActiveLocation';
import AnimatedRefresh from '../AnimatedRefresh';

import { FilterButton, AppliedFilters } from './Filters';
import TransactionList from './TransactionList';
import {
  SplitsExpandedProvider,
  useSplitsExpanded,
  isPreviewId,
} from './TransactionsTable';

function EmptyMessage({ onAdd }) {
  return (
    <View
      style={{
        backgroundColor: 'white',
        flex: 1,
        alignItems: 'center',
        borderTopWidth: 1,
        borderColor: colors.n9,
      }}
    >
      <View
        style={{
          width: 550,
          marginTop: 75,
          fontSize: 15,
          alignItems: 'center',
        }}
      >
        <Text style={{ textAlign: 'center', lineHeight: '1.4em' }}>
          For Actual to be useful, you need to <strong>add an account</strong>.
          You can link an account to automatically download transactions, or
          manage it locally yourself.
        </Text>

        <Button primary style={{ marginTop: 20 }} onClick={onAdd}>
          Add account
        </Button>

        <View style={{ marginTop: 20, fontSize: 13, color: colors.n5 }}>
          In the future, you can add accounts from the sidebar.
        </View>
      </View>
    </View>
  );
}

function ReconcilingMessage({
  balanceQuery,
  targetBalance,
  onDone,
  onCreateTransaction,
}) {
  let cleared = useSheetValue({
    name: balanceQuery.name + '-cleared',
    value: 0,
    query: balanceQuery.query.filter({ cleared: true }),
  });
  let targetDiff = targetBalance - cleared;

  return (
    <View
      style={{
        flexDirection: 'row',
        alignSelf: 'center',
        backgroundColor: 'white',
        ...styles.shadow,
        borderRadius: 4,
        marginTop: 5,
        marginBottom: 15,
        padding: 10,
      }}
    >
      <View style={{ flexDirection: 'row', alignItems: 'center' }}>
        {targetDiff === 0 ? (
          <View
            style={{
              color: colors.g4,
              flex: 1,
              flexDirection: 'row',
              alignItems: 'center',
              justifyContent: 'center',
            }}
          >
            <CheckCircle1
              style={{
                width: 13,
                height: 13,
                color: colors.g5,
                marginRight: 3,
              }}
            />
            All reconciled!
          </View>
        ) : (
          <View style={{ color: colors.n3 }}>
            <Text style={{ fontStyle: 'italic', textAlign: 'center' }}>
              Your cleared balance{' '}
              <strong>{format(cleared, 'financial')}</strong> needs{' '}
              <strong>
                {(targetDiff > 0 ? '+' : '') + format(targetDiff, 'financial')}
              </strong>{' '}
              to match
              <br /> your bank{"'"}s balance of{' '}
              <Text style={{ fontWeight: 700 }}>
                {format(targetBalance, 'financial')}
              </Text>
            </Text>
          </View>
        )}
        <View style={{ marginLeft: 15 }}>
          <Button primary onClick={onDone}>
            Done Reconciling
          </Button>
        </View>
        {targetDiff !== 0 && (
          <View style={{ marginLeft: 15 }}>
            <Button onClick={() => onCreateTransaction(targetDiff)}>
              Create Reconciliation Transaction
            </Button>
          </View>
        )}
      </View>
    </View>
  );
}

function ReconcileTooltip({ account, onReconcile, onClose }) {
  let balance = useSheetValue(queries.accountBalance(account));

  function onSubmit(e) {
    let input = e.target.elements[0];
    let amount = currencyToInteger(input.value);
    onReconcile(amount == null ? balance : amount);
    onClose();
  }

  return (
    <Tooltip position="bottom-right" width={275} onClose={onClose}>
      <View style={{ padding: '5px 8px' }}>
        <Text>
          Enter the current balance of your bank account that you want to
          reconcile with:
        </Text>
        <form onSubmit={onSubmit}>
          {balance != null && (
            <InitialFocus>
              <Input
                defaultValue={format(balance, 'financial')}
                style={{ margin: '7px 0' }}
              />
            </InitialFocus>
          )}
          <Button primary>Reconcile</Button>
        </form>
      </View>
    </Tooltip>
  );
}

function MenuButton({ onClick }) {
  return (
    <Button bare onClick={onClick}>
      <DotsHorizontalTriple
        width={15}
        height={15}
        style={{ color: 'inherit', transform: 'rotateZ(90deg)' }}
      />
    </Button>
  );
}

function MenuTooltip({ onClose, children }) {
  return (
    <Tooltip
      position="bottom-right"
      width={200}
      style={{ padding: 0 }}
      onClose={onClose}
    >
      {children}
    </Tooltip>
  );
}

function AccountMenu({
  account,
  canSync,
  syncEnabled,
  showBalances,
  canShowBalances,
  showCleared,
  onClose,
  onReconcile,
  onMenuSelect,
}) {
  let [tooltip, setTooltip] = useState('default');

  return tooltip === 'reconcile' ? (
    <ReconcileTooltip
      account={account}
      onClose={onClose}
      onReconcile={onReconcile}
    />
  ) : (
    <MenuTooltip onClose={onClose}>
      <Menu
        onMenuSelect={item => {
          if (item === 'reconcile') {
            setTooltip('reconcile');
          } else {
            onMenuSelect(item);
          }
        }}
        items={[
          canShowBalances && {
            name: 'toggle-balance',
            text: (showBalances ? 'Hide' : 'Show') + ' Running Balance',
          },
          {
            name: 'toggle-cleared',
            text: (showCleared ? 'Hide' : 'Show') + ' "Cleared" Checkboxes',
          },
          { name: 'export', text: 'Export' },
          { name: 'reconcile', text: 'Reconcile' },
          syncEnabled &&
            account &&
            !account.closed &&
            (canSync
              ? { name: 'unlink', text: 'Unlink Account' }
              : { name: 'link', text: 'Link Account' }),
          account.closed
            ? { name: 'reopen', text: 'Reopen Account' }
            : { name: 'close', text: 'Close Account' },
        ].filter(x => x)}
      />
    </MenuTooltip>
  );
}

function CategoryMenu({ onClose, onMenuSelect }) {
  return (
    <MenuTooltip onClose={onClose}>
      <Menu
        onMenuSelect={item => {
          onMenuSelect(item);
        }}
        items={[{ name: 'export', text: 'Export' }]}
      />
    </MenuTooltip>
  );
}

function DetailedBalance({ name, balance }) {
  return (
    <Text
      style={{
        marginLeft: 15,
        backgroundColor: colors.n10,
        borderRadius: 4,
        padding: '4px 6px',
        color: colors.n5,
      }}
    >
      {name}{' '}
      <Text style={{ fontWeight: 600 }}>{format(balance, 'financial')}</Text>
    </Text>
  );
}

function SelectedBalance({ selectedItems }) {
  let name = `selected-balance-${[...selectedItems].join('-')}`;

  let rows = useSheetValue({
    name,
    query: q('transactions')
      .filter({
        id: { $oneof: [...selectedItems] },
        parent_id: { $oneof: [...selectedItems] },
      })
      .select('id'),
  });
  let ids = new Set((rows || []).map(r => r.id));

  let finalIds = [...selectedItems].filter(id => !ids.has(id));
  let balance = useSheetValue({
    name: name + '-sum',
    query: q('transactions')
      .filter({ id: { $oneof: finalIds } })
      .options({ splits: 'all' })
      .calculate({ $sum: '$amount' }),
  });

  if (balance == null) {
    return null;
  }
  return <DetailedBalance name="Selected balance:" balance={balance} />;
}

function MoreBalances({ balanceQuery }) {
  let cleared = useSheetValue({
    name: balanceQuery.name + '-cleared',
    query: balanceQuery.query.filter({ cleared: true }),
  });
  let uncleared = useSheetValue({
    name: balanceQuery.name + '-uncleared',
    query: balanceQuery.query.filter({ cleared: false }),
  });

  return (
    <View style={{ flexDirection: 'row' }}>
      <DetailedBalance name="Cleared total:" balance={cleared} />
      <DetailedBalance name="Uncleared total:" balance={uncleared} />
    </View>
  );
}

function Balances({ balanceQuery, showExtraBalances, onToggleExtraBalances }) {
  let selectedItems = useSelectedItems();

  return (
    <View
      style={{
        flexDirection: 'row',
        alignItems: 'center',
        marginTop: -5,
        marginLeft: -5,
      }}
    >
      <Button
        bare
        onClick={onToggleExtraBalances}
        style={{
          '& svg': {
            opacity: selectedItems.size > 0 || showExtraBalances ? 1 : 0,
          },
          '&:hover svg': { opacity: 1 },
        }}
      >
        <CellValue
          binding={{ ...balanceQuery, value: 0 }}
          type="financial"
          style={{ fontSize: 22, fontWeight: 400 }}
          getStyle={value => ({
            color: value < 0 ? colors.r5 : value > 0 ? colors.g5 : colors.n8,
          })}
        />

        <ArrowButtonRight1
          style={{
            width: 10,
            height: 10,
            marginLeft: 10,
            color: colors.n5,
            transform: showExtraBalances ? 'rotateZ(180deg)' : 'rotateZ(0)',
          }}
        />
      </Button>
      {showExtraBalances && <MoreBalances balanceQuery={balanceQuery} />}

      {selectedItems.size > 0 && (
        <SelectedBalance selectedItems={selectedItems} />
      )}
    </View>
  );
}

// function ScheduleMenu({ onSelect, onClose }) {
//   let params = useParams();
//   let scheduleData = useCachedSchedules();
//   let payees = useSelector(state => state.queries.payees);
//   let byId = getPayeesById(payees);

//   if (scheduleData == null) {
//     return null;
//   }

//   return (
//     <Tooltip
//       position="bottom-right"
//       width={200}
//       style={{ padding: 0 }}
//       onClose={onClose}
//     >
//       <Menu
//         onMenuSelect={name => {
//           onSelect(name);
//           onClose();
//         }}
//         items={scheduleData.schedules.map(s => {
//           let desc = s._payee
//             ? `${byId[s._payee].name} (${s.next_date})`
//             : `No payee (${s.next_date})`;

//           return { name: s.id, text: desc };
//         })}
//       />
//     </Tooltip>
//   );
// }

function SelectedTransactionsButton({
  style,
  getTransaction,
  onShow,
  onDuplicate,
  onDelete,
  onEdit,
  onUnlink,
  onScheduleAction,
}) {
  let selectedItems = useSelectedItems();
  let history = useHistory();

  let types = useMemo(() => {
    let items = [...selectedItems];
    return {
      preview: !!items.find(id => isPreviewId(id)),
      trans: !!items.find(id => !isPreviewId(id)),
    };
  }, [selectedItems]);

  let ambiguousDuplication = useMemo(() => {
    let transactions = [...selectedItems].map(id => getTransaction(id));

    return transactions.some(t => t && t.is_child);
  }, [selectedItems]);

  let linked = useMemo(() => {
    return (
      !types.preview &&
      [...selectedItems].every(id => {
        let t = getTransaction(id);
        return t && t.schedule;
      })
    );
  }, [types.preview, selectedItems, getTransaction]);

  return (
    <SelectedItemsButton
      name="transactions"
      keyHandlers={
        types.trans && {
          f: () => onShow([...selectedItems]),
          d: () => onDelete([...selectedItems]),
          a: () => onEdit('account', [...selectedItems]),
          p: () => onEdit('payee', [...selectedItems]),
          n: () => onEdit('notes', [...selectedItems]),
          c: () => onEdit('category', [...selectedItems]),
          l: () => onEdit('cleared', [...selectedItems]),
        }
      }
      items={[
        ...(!types.trans
          ? [
              { name: 'view-schedule', text: 'View schedule' },
              { name: 'post-transaction', text: 'Post transaction' },
              { name: 'skip', text: 'Skip scheduled date' },
            ]
          : [
              { name: 'show', text: 'Show', key: 'F' },
              {
                name: 'duplicate',
                text: 'Duplicate',
                disabled: ambiguousDuplication,
              },
              { name: 'delete', text: 'Delete', key: 'D' },
              ...(linked
                ? [
                    {
                      name: 'view-schedule',
                      text: 'View schedule',
                      disabled: selectedItems.size > 1,
                    },
                    { name: 'unlink-schedule', text: 'Unlink schedule' },
                  ]
                : [
                    {
                      name: 'link-schedule',
                      text: 'Link schedule',
                    },
                  ]),
              Menu.line,
              { type: Menu.label, name: 'Edit field' },
              { name: 'date', text: 'Date' },
              { name: 'account', text: 'Account', key: 'A' },
              { name: 'payee', text: 'Payee', key: 'P' },
              { name: 'notes', text: 'Notes', key: 'N' },
              { name: 'category', text: 'Category', key: 'C' },
              { name: 'amount', text: 'Amount' },
              { name: 'cleared', text: 'Cleared', key: 'L' },
            ]),
      ]}
      onSelect={name => {
        switch (name) {
          case 'show':
            onShow([...selectedItems]);
            break;
          case 'duplicate':
            onDuplicate([...selectedItems]);
            break;
          case 'delete':
            onDelete([...selectedItems]);
            break;
          case 'post-transaction':
          case 'skip':
            onScheduleAction(name, selectedItems);
            break;
          case 'view-schedule':
            let firstId = [...selectedItems][0];
            let scheduleId;
            if (isPreviewId(firstId)) {
              let parts = firstId.split('/');
              scheduleId = parts[1];
            } else {
              let trans = getTransaction(firstId);
              scheduleId = trans && trans.schedule;
            }

            if (scheduleId) {
              history.push(`/schedule/edit/${scheduleId}`, {
                locationPtr: history.location,
              });
            }
            break;
          case 'link-schedule':
            history.push(`/schedule/link`, {
              locationPtr: history.location,
              transactionIds: [...selectedItems],
            });
            break;
          case 'unlink-schedule':
            onUnlink([...selectedItems]);
            break;
          default:
            onEdit(name, [...selectedItems]);
        }
      }}
    ></SelectedItemsButton>
  );
}

const AccountHeader = React.memo(
  ({
    tableRef,
    editingName,
    isNameEditable,
    workingHard,
    accountName,
    account,
    accountsSyncing,
    accounts,
    transactions,
    syncEnabled,
    showBalances,
    showExtraBalances,
    showCleared,
    showEmptyMessage,
    balanceQuery,
    reconcileAmount,
    canCalculateBalance,
    search,
    filters,
    savePrefs,
    onSearch,
    onAddTransaction,
    onShowTransactions,
    onDoneReconciling,
    onCreateReconciliationTransaction,
    onToggleExtraBalances,
    onSaveName,
    onExposeName,
    onSync,
    onImport,
    onMenuSelect,
    onReconcile,
    onBatchDelete,
    onBatchDuplicate,
    onBatchEdit,
    onBatchUnlink,
    onApplyFilter,
    onUpdateFilter,
    onDeleteFilter,
    onScheduleAction,
  }) => {
    let [menuOpen, setMenuOpen] = useState(false);
    let searchInput = useRef(null);
    let splitsExpanded = useSplitsExpanded();

    let canSync = syncEnabled && account && account.account_id;
    if (!account) {
      // All accounts - check for any syncable account
      canSync = !!accounts.find(account => !!account.account_id);
    }

    function onToggleSplits() {
      if (tableRef.current) {
        splitsExpanded.dispatch({
          type: 'switch-mode',
          id: tableRef.current.getScrolledItem(),
        });

        savePrefs({
          'expand-splits': !(splitsExpanded.state.mode === 'expand'),
        });
      }
    }

    return (
      <>
        <KeyHandlers
          keys={{
            'mod+f': () => {
              if (searchInput.current) {
                searchInput.current.focus();
              }
            },
          }}
        />

        <View
          style={[styles.pageContent, { paddingBottom: 10, flexShrink: 0 }]}
        >
          <View style={{ marginTop: 2, alignItems: 'flex-start' }}>
            <View>
              {editingName ? (
                <InitialFocus>
                  <Input
                    defaultValue={accountName}
                    onEnter={e => onSaveName(e.target.value)}
                    onBlur={() => onExposeName(false)}
                    style={{
                      fontSize: 25,
                      fontWeight: 500,
                      marginTop: -5,
                      marginBottom: -2,
                      marginLeft: -5,
                    }}
                  />
                </InitialFocus>
              ) : isNameEditable ? (
                <View
                  style={{
                    flexDirection: 'row',
                    alignItems: 'center',
                    gap: 3,
                    '& .hover-visible': {
                      opacity: 0,
                      transition: 'opacity .25s',
                    },
                    '&:hover .hover-visible': {
                      opacity: 1,
                    },
                  }}
                >
                  <View
                    style={{
                      fontSize: 25,
                      fontWeight: 500,
                      marginRight: 5,
                      marginBottom: 5,
                    }}
                  >
                    {account && account.closed
                      ? 'Closed: ' + accountName
                      : accountName}
                  </View>

                  {account && <NotesButton id={`account-${account.id}`} />}
                  <Button
                    bare
                    className="hover-visible"
                    onClick={() => onExposeName(true)}
                  >
                    <Pencil1
                      style={{
                        width: 11,
                        height: 11,
                        color: colors.n8,
                      }}
                    />
                  </Button>
                </View>
              ) : (
                <View
                  style={{ fontSize: 25, fontWeight: 500, marginBottom: 5 }}
                >
                  {account && account.closed
                    ? 'Closed: ' + accountName
                    : accountName}
                </View>
              )}
            </View>
          </View>

          <Balances
            balanceQuery={balanceQuery}
            showExtraBalances={showExtraBalances}
            onToggleExtraBalances={onToggleExtraBalances}
          />

          <Stack
            spacing={2}
            direction="row"
            align="center"
            style={{ marginTop: 12 }}
          >
            {((account && !account.closed) || canSync) && (
              <Button bare onClick={canSync ? onSync : onImport}>
                {canSync ? (
                  <>
                    <AnimatedRefresh
                      width={13}
                      height={13}
                      animating={
                        (account && accountsSyncing === account.name) ||
                        accountsSyncing === '__all'
                      }
                      style={{ color: 'currentColor', marginRight: 4 }}
                    />{' '}
                    Sync
                  </>
                ) : (
                  <>
                    <DownloadThickBottom
                      width={13}
                      height={13}
                      style={{ color: 'currentColor', marginRight: 4 }}
                    />{' '}
                    Import
                  </>
                )}
              </Button>
            )}
            {!showEmptyMessage && (
              <Button bare onClick={onAddTransaction}>
                <Add
                  width={10}
                  height={10}
                  style={{ color: 'inherit', marginRight: 3 }}
                />{' '}
                Add New
              </Button>
            )}
            <View>
              <FilterButton onApply={onApplyFilter} />
            </View>
            <InputWithContent
              leftContent={
                <SearchAlternate
                  style={{
                    width: 13,
                    height: 13,
                    flexShrink: 0,
                    color: search ? colors.p7 : 'inherit',
                    margin: 5,
                    marginRight: 0,
                  }}
                />
              }
              rightContent={
                search && (
                  <Button
                    bare
                    style={{ padding: 8 }}
                    onClick={() => onSearch('')}
                    title="Clear search term"
                  >
                    <SvgRemove
                      style={{
                        width: 8,
                        height: 8,
                        color: 'inherit',
                      }}
                    />
                  </Button>
                )
              }
              inputRef={searchInput}
              value={search}
              placeholder="Search"
              onKeyDown={e => {
                if (e.key === 'Escape') onSearch('');
              }}
              getStyle={focused => [
                {
                  backgroundColor: 'transparent',
                  borderWidth: 0,
                  boxShadow: 'none',
                  transition: 'color .15s',
                  '& input::placeholder': {
                    color: colors.n1,
                    transition: 'color .25s',
                  },
                },
                focused && { boxShadow: '0 0 0 2px ' + colors.b5 },
                !focused && search !== '' && { color: colors.p4 },
              ]}
              onChange={e => onSearch(e.target.value)}
            />
            {workingHard ? (
              <View>
                <Loading color={colors.n1} style={{ width: 16, height: 16 }} />
              </View>
            ) : (
              <SelectedTransactionsButton
                getTransaction={id => transactions.find(t => t.id === id)}
                onShow={onShowTransactions}
                onDuplicate={onBatchDuplicate}
                onDelete={onBatchDelete}
                onEdit={onBatchEdit}
                onUnlink={onBatchUnlink}
                onScheduleAction={onScheduleAction}
              />
            )}
            <Button
              bare
              disabled={search !== '' || filters.length > 0}
              style={{ padding: 6 }}
              onClick={onToggleSplits}
              title={
                splitsExpanded.state.mode === 'collapse'
                  ? 'Collapse split transactions'
                  : 'Expand split transactions'
              }
            >
              {splitsExpanded.state.mode === 'collapse' ? (
                <ArrowsShrink3
                  style={{
                    width: 14,
                    height: 14,
                    color: 'inherit',
                  }}
                />
              ) : (
                <ArrowsExpand3
                  style={{
                    width: 14,
                    height: 14,
                    color: 'inherit',
                  }}
                />
              )}
            </Button>
            {account ? (
              <View>
                <MenuButton onClick={() => setMenuOpen(true)} />

                {menuOpen && (
                  <AccountMenu
                    account={account}
                    canSync={canSync}
                    syncEnabled={syncEnabled}
                    canShowBalances={canCalculateBalance()}
                    showBalances={showBalances}
                    showCleared={showCleared}
                    onMenuSelect={item => {
                      setMenuOpen(false);
                      onMenuSelect(item);
                    }}
                    onReconcile={onReconcile}
                    onClose={() => setMenuOpen(false)}
                  />
                )}
              </View>
            ) : (
              <View>
                <MenuButton onClick={() => setMenuOpen(true)} />

                {menuOpen && (
                  <CategoryMenu
                    onMenuSelect={item => {
                      setMenuOpen(false);
                      onMenuSelect(item);
                    }}
                    onClose={() => setMenuOpen(false)}
                  />
                )}
              </View>
            )}
          </Stack>

          {filters && filters.length > 0 && (
            <AppliedFilters
              filters={filters}
              onUpdate={onUpdateFilter}
              onDelete={onDeleteFilter}
            />
          )}
        </View>
        {reconcileAmount != null && (
          <ReconcilingMessage
            targetBalance={reconcileAmount}
            balanceQuery={balanceQuery}
            onDone={onDoneReconciling}
            onCreateTransaction={onCreateReconciliationTransaction}
          />
        )}
      </>
    );
  },
);

function AllTransactions({ transactions, filtered, children }) {
  let scheduleData = useCachedSchedules();

  let schedules = useMemo(
    () =>
      scheduleData
        ? scheduleData.schedules.filter(
            s =>
              !s.completed &&
              ['due', 'upcoming', 'missed'].includes(
                scheduleData.statuses.get(s.id),
              ),
          )
        : [],
    [scheduleData],
  );

  let prependTransactions = useMemo(() => {
    return schedules.map(schedule => ({
      id: 'preview/' + schedule.id,
      payee: schedule._payee,
      account: schedule._account,
      amount: schedule._amount,
      date: schedule.next_date,
      notes: scheduleData.statuses.get(schedule.id),
      schedule: schedule.id,
    }));
  }, [schedules]);

  let allTransactions = useMemo(() => {
    // Don't prepend scheduled transactions if we are filtering
    if (!filtered && prependTransactions.length > 0) {
      return prependTransactions.concat(transactions);
    }
    return transactions;
  }, [filtered, prependTransactions, transactions]);

  if (scheduleData == null) {
    return children(null);
  }
  return children(allTransactions);
}

class AccountInternal extends React.PureComponent {
  constructor(props) {
    super(props);
    this.paged = null;
    this.table = React.createRef();
    this.animated = true;

    this.state = {
      search: '',
      filters: [],
      loading: true,
      workingHard: false,
      reconcileAmount: null,
      transactions: [],
      transactionsCount: 0,
      showBalances: props.showBalances,
      balances: [],
      showCleared: props.showCleared,
      editingName: false,
      isAdding: false,
      latestDate: null,
    };
  }

  async componentDidMount() {
    let maybeRefetch = tables => {
      if (
        tables.includes('transactions') ||
        tables.includes('category_mapping') ||
        tables.includes('payee_mapping')
      ) {
        return this.refetchTransactions();
      }
    };

    let onUndo = async ({ tables, messages, undoTag }) => {
      await maybeRefetch(tables);

      // If all the messages are dealing with transactions, find the
      // first message referencing a non-deleted row so that we can
      // highlight the row
      //
      let focusId;
      if (
        messages.every(msg => msg.dataset === 'transactions') &&
        !messages.find(msg => msg.column === 'tombstone')
      ) {
        let focusableMsgs = messages.filter(
          msg =>
            msg.dataset === 'transactions' && !(msg.column === 'tombstone'),
        );

        focusId = focusableMsgs.length === 1 ? focusableMsgs[0].row : null;

        // Highlight the transactions
        // this.table && this.table.highlight(focusableMsgs.map(msg => msg.row));
      }

      if (this.table.current) {
        this.table.current.edit(null);

        // Focus a transaction if applicable. There is a chance if the
        // user navigated away that focusId is a transaction that has
        // been "paged off" and we won't focus it. That's ok, we just
        // do our best.
        if (focusId) {
          this.table.current.scrollTo(focusId);
        }
      }

      this.props.setLastUndoState(null);
    };

    let unlistens = [listen('undo-event', onUndo)];

    this.unlisten = () => {
      unlistens.forEach(unlisten => unlisten());
    };

    // Important that any async work happens last so that the
    // listeners are set up synchronously
    if (this.props.categoryGroups.length === 0) {
      await this.props.getCategories();
    }
    await this.props.initiallyLoadPayees();
    await this.fetchTransactions();

    // If there is a pending undo, apply it immediately (this happens
    // when an undo changes the location to this page)
    if (this.props.lastUndoState && this.props.lastUndoState.current) {
      onUndo(this.props.lastUndoState.current);
    }
  }

  componentDidUpdate(prevProps) {
    // If the user was on a different screen and is now coming back to
    // the transactions, automatically refresh the transaction to make
    // sure we have updated state
    if (prevProps.modalShowing && !this.props.modalShowing) {
      // This is clearly a hack. Need a better way to track which
      // things are listening to transactions and refetch
      // automatically (use ActualQL?)
      setTimeout(() => {
        this.refetchTransactions();
      }, 100);
    }
  }

  componentWillUnmount() {
    if (this.unlisten) {
      this.unlisten();
    }
    if (this.paged) {
      this.paged.unsubscribe();
    }
  }

  fetchAllIds = async () => {
    let { data } = await runQuery(this.paged.getQuery().select('id'));
    // Remember, this is the `grouped` split type so we need to deal
    // with the `subtransactions` property
    return data.reduce((arr, t) => {
      arr.push(t.id);
      t.subtransactions.forEach(sub => arr.push(sub.id));
      return arr;
    }, []);
  };

  refetchTransactions = async () => {
    this.paged && this.paged.run();
  };

  fetchTransactions = () => {
    let query = this.makeRootQuery();
    this.rootQuery = this.currentQuery = query;
    this.updateQuery(query);

    if (this.props.accountId) {
      this.props.markAccountRead(this.props.accountId);
    }
  };

  makeRootQuery = () => {
    let locationState = this.props.location.state;
    let accountId = this.props.accountId;

    if (locationState && locationState.filter) {
      return q('transactions')
        .options({ splits: 'grouped' })
        .filter({
          'account.offbudget': false,
          ...locationState.filter,
        });
    }

    return queries.makeTransactionsQuery(accountId);
  };

  updateQuery(query, isFiltered) {
    if (this.paged) {
      this.paged.unsubscribe();
    }

    this.paged = pagedQuery(
      query.select('*'),
      async (data, prevData) => {
        const firstLoad = prevData == null;

        if (firstLoad) {
          this.table.current && this.table.current.setRowAnimation(false);

          if (isFiltered) {
            this.props.splitsExpandedDispatch({
              type: 'set-mode',
              mode: 'collapse',
            });
          } else {
            this.props.splitsExpandedDispatch({
              type: 'set-mode',
              mode: this.props.expandSplits ? 'expand' : 'collapse',
            });
          }
        }

        this.setState(
          {
            transactions: data,
            transactionCount: this.paged.getTotalCount(),
            transactionsFiltered: isFiltered,
            loading: false,
            workingHard: false,
          },
          () => {
            if (this.state.showBalances) {
              this.calculateBalances();
            }

            if (firstLoad) {
              this.table.current && this.table.current.scrollToTop();
            }

            setTimeout(() => {
              this.table.current && this.table.current.setRowAnimation(true);
            }, 0);
          },
        );
      },
      {
        pageCount: 150,
        onlySync: true,
        mapper: ungroupTransactions,
      },
    );
  }

  componentWillReceiveProps(nextProps) {
    if (this.props.match !== nextProps.match) {
      this.setState(
        {
          editingName: false,
          loading: true,
          search: '',
          showBalances: nextProps.showBalances,
          balances: [],
          showCleared: nextProps.showCleared,
        },
        () => {
          this.fetchTransactions();
        },
      );
    }
  }

  onSearch = value => {
    this.paged.unsubscribe();
    this.setState({ search: value }, this.onSearchDone);
  };

  onSearchDone = debounce(() => {
    if (this.state.search === '') {
      this.updateQuery(this.currentQuery, this.state.filters.length > 0);
    } else {
      this.updateQuery(
        queries.makeTransactionSearchQuery(
          this.currentQuery,
          this.state.search,
          this.props.dateFormat,
        ),
        true,
      );
    }
  }, 150);

  onSync = async () => {
    const accountId = this.props.accountId;
    const account = this.props.accounts.find(acct => acct.id === accountId);

    await this.props.syncAndDownload(account ? account.id : null);
  };

  onImport = async () => {
    const accountId = this.props.accountId;
    const account = this.props.accounts.find(acct => acct.id === accountId);

    if (account) {
      const res = await window.Actual.openFileDialog({
        filters: [
          { name: 'Financial Files', extensions: ['qif', 'ofx', 'qfx', 'csv'] },
        ],
      });

      if (res) {
        this.props.pushModal('import-transactions', {
          accountId,
          filename: res[0],
          onImported: didChange => {
            if (didChange) {
              this.fetchTransactions();
            }
          },
        });
      }
    }
  };

  onExport = async accountName => {
    let exportedTransactions = await send('transactions-export-query', {
      query: this.currentQuery.serialize(),
    });
    let normalizedName =
      accountName && accountName.replace(/[()]/g, '').replace(/\s+/g, '-');
    let filename = `${normalizedName || 'transactions'}.csv`;

    window.Actual.saveFile(
      exportedTransactions,
      filename,
      'Export Transactions',
    );
  };

  onTransactionsChange = (newTransaction, data) => {
    // Apply changes to pagedQuery data
    this.paged.optimisticUpdate(
      data => {
        if (newTransaction._deleted) {
          return data.filter(t => t.id !== newTransaction.id);
        } else {
          return data.map(t => {
            return t.id === newTransaction.id ? newTransaction : t;
          });
        }
      },
      mappedData => {
        return data;
      },
    );

    this.props.updateNewTransactions(newTransaction.id);
  };

  canCalculateBalance = () => {
    let accountId = this.props.accountId;
    let account = this.props.accounts.find(account => account.id === accountId);
    return (
      account && this.state.search === '' && this.state.filters.length === 0
    );
  };

  async calculateBalances() {
    if (!this.canCalculateBalance()) {
      return;
    }

    let { data } = await runQuery(
      this.paged
        .getQuery()
        .options({ splits: 'none' })
        .select([{ balance: { $sumOver: '$amount' } }]),
    );

    this.setState({ balances: groupById(data) });
  }

  onAddTransaction = () => {
    this.setState({ isAdding: true });
  };

  onExposeName = flag => {
    this.setState({ editingName: flag });
  };

  onSaveName = name => {
    if (name.trim().length) {
      const accountId = this.props.accountId;
      const account = this.props.accounts.find(
        account => account.id === accountId,
      );
      this.props.updateAccount({ ...account, name });
      this.setState({ editingName: false });
    }
  };

  onToggleExtraBalances = () => {
    let { accountId, showExtraBalances } = this.props;
    let key = 'show-extra-balances-' + accountId;

    this.props.savePrefs({ [key]: !showExtraBalances });
  };

  onMenuSelect = async item => {
    const accountId = this.props.accountId;
    const account = this.props.accounts.find(
      account => account.id === accountId,
    );

    switch (item) {
      case 'link':
        authorizeBank(this.props.pushModal, { upgradingId: accountId });
        break;
      case 'unlink':
        this.props.unlinkAccount(accountId);
        break;
      case 'close':
        this.props.openAccountCloseModal(accountId);
        break;
      case 'reopen':
        this.props.reopenAccount(accountId);
        break;
      case 'export':
        const accountName = this.getAccountTitle(account, accountId);
        this.onExport(accountName);
        break;
      case 'toggle-balance':
        if (this.state.showBalances) {
          this.props.savePrefs({ ['show-balances-' + accountId]: false });
          this.setState({ showBalances: false, balances: [] });
        } else {
          this.props.savePrefs({ ['show-balances-' + accountId]: true });
          this.setState({ showBalances: true });
          this.calculateBalances();
        }
        break;
      case 'toggle-cleared':
        if (this.state.showCleared) {
          this.props.savePrefs({ ['hide-cleared-' + accountId]: true });
          this.setState({ showCleared: false });
        } else {
          this.props.savePrefs({ ['hide-cleared-' + accountId]: false });
          this.setState({ showCleared: true });
        }
        break;
      default:
    }
  };

  getAccountTitle(account, id) {
    let { filterName } = this.props.location.state || {};

    if (filterName) {
      return filterName;
    }

    if (!account) {
      if (id === 'budgeted') {
        return 'Budgeted Accounts';
      } else if (id === 'offbudget') {
        return 'Off Budget Accounts';
      } else if (id === 'uncategorized') {
        return 'Uncategorized';
      } else if (!id) {
        return 'All Accounts';
      }
      return null;
    }

    return account.name;
  }

  getBalanceQuery(account, id) {
    return {
      name: `balance-query-${id}`,
      query: this.makeRootQuery().calculate({ $sum: '$amount' }),
    };
  }

  isNew = id => {
    return this.props.newTransactions.includes(id);
  };

  isMatched = id => {
    return this.props.matchedTransactions.includes(id);
  };

  onCreatePayee = name => {
    let trimmed = name.trim();
    if (trimmed !== '') {
      return this.props.createPayee(name);
    }
    return null;
  };

  onReconcile = balance => {
    this.setState({ reconcileAmount: balance });
  };

  onDoneReconciling = () => {
    this.setState({ reconcileAmount: null });
  };

  onCreateReconciliationTransaction = async diff => {
    // Create a new reconciliation transaction
    const reconciliationTransactions = realizeTempTransactions([
      {
        id: 'temp',
        account: this.props.accountId,
        cleared: true,
        amount: diff,
        date: currentDay(),
        notes: 'Reconciliation balance adjustment',
      },
    ]);

    // Optimistic UI: update the transaction list before sending the data to the database
    this.setState({
      transactions: [...this.state.transactions, ...reconciliationTransactions],
    });

    // sync the reconciliation transaction
    await send('transactions-batch-update', {
      added: reconciliationTransactions,
    });
    await this.refetchTransactions();
  };

  onShowTransactions = async ids => {
    this.onApplyFilter({
      customName: 'Selected transactions',
      filter: { id: { $oneof: ids } },
    });
  };

  onBatchEdit = async (name, ids) => {
    let onChange = async (name, value) => {
      this.setState({ workingHard: true });

      let { data } = await runQuery(
        q('transactions')
          .filter({ id: { $oneof: ids } })
          .select('*')
          .options({ splits: 'grouped' }),
      );
      let transactions = ungroupTransactions(data);

      let changes = { deleted: [], updated: [] };

      // Cleared is a special case right now
      if (name === 'cleared') {
        // Clear them if any are uncleared, otherwise unclear them
        value = !!transactions.find(t => !t.cleared);
      }

      const idSet = new Set(ids);

      transactions.forEach(trans => {
        if (!idSet.has(trans.id)) {
          // Skip transactions which aren't actually selected, since the query
          // above also retrieves the siblings & parent of any selected splits.
          return;
        }

        let { diff } = updateTransaction(transactions, {
          ...trans,
          [name]: value,
        });

        // TODO: We need to keep an updated list of transactions so
        // the logic in `updateTransaction`, particularly about
        // updating split transactions, works. This isn't ideal and we
        // should figure something else out
        transactions = applyChanges(diff, transactions);

        changes.deleted = changes.deleted
          ? changes.deleted.concat(diff.deleted)
          : diff.deleted;
        changes.updated = changes.updated
          ? changes.updated.concat(diff.updated)
          : diff.updated;
        changes.added = changes.added
          ? changes.added.concat(diff.added)
          : diff.added;
      });

      await send('transactions-batch-update', changes);
      await this.refetchTransactions();

      if (this.table.current) {
        this.table.current.edit(transactions[0].id, 'select', false);
      }
    };

    if (name === 'cleared') {
      // Cleared just toggles it on/off and it depends on the data
      // loaded. Need to clean this up in the future.
      onChange('cleared', null);
    } else {
      this.props.pushModal('edit-field', { name, onSubmit: onChange });
    }
  };

  onBatchDuplicate = async ids => {
    this.setState({ workingHard: true });

    let { data } = await runQuery(
      q('transactions')
        .filter({ id: { $oneof: ids } })
        .select('*')
        .options({ splits: 'grouped' }),
    );

    let changes = {
      added: data
        .reduce((newTransactions, trans) => {
          return newTransactions.concat(
            realizeTempTransactions(ungroupTransaction(trans)),
          );
        }, [])
        .map(({ sort_order, ...trans }) => ({ ...trans })),
    };

    await send('transactions-batch-update', changes);

    await this.refetchTransactions();
  };

  onBatchDelete = async ids => {
    this.setState({ workingHard: true });

    let { data } = await runQuery(
      q('transactions')
        .filter({ id: { $oneof: ids } })
        .select('*')
        .options({ splits: 'grouped' }),
    );
    let transactions = ungroupTransactions(data);

    let idSet = new Set(ids);
    let changes = { deleted: [], updated: [] };

    transactions.forEach(trans => {
      let parentId = trans.parent_id;

      // First, check if we're actually deleting this transaction by
      // checking `idSet`. Then, we don't need to do anything if it's
      // a child transaction and the parent is already being deleted
      if (!idSet.has(trans.id) || (parentId && idSet.has(parentId))) {
        return;
      }

      let { diff } = deleteTransaction(transactions, trans.id);

      // TODO: We need to keep an updated list of transactions so
      // the logic in `updateTransaction`, particularly about
      // updating split transactions, works. This isn't ideal and we
      // should figure something else out
      transactions = applyChanges(diff, transactions);

      changes.deleted = diff.deleted
        ? changes.deleted.concat(diff.deleted)
        : diff.deleted;
      changes.updated = diff.updated
        ? changes.updated.concat(diff.updated)
        : diff.updated;
    });

    await send('transactions-batch-update', changes);
    await this.refetchTransactions();
  };

  onBatchUnlink = async ids => {
    await send('transactions-batch-update', {
      updated: ids.map(id => ({ id, schedule: null })),
    });

    await this.refetchTransactions();
  };

  onUpdateFilter = (oldFilter, updatedFilter) => {
    this.applyFilters(
      this.state.filters.map(f => (f === oldFilter ? updatedFilter : f)),
    );
  };

  onDeleteFilter = filter => {
    this.applyFilters(this.state.filters.filter(f => f !== filter));
  };

  onApplyFilter = async cond => {
    let filters = this.state.filters;
    if (cond.customName) {
      filters = filters.filter(f => f.customName !== cond.customName);
    }
    this.applyFilters([...filters, cond]);
  };

  onScheduleAction = async (name, ids) => {
    switch (name) {
      case 'post-transaction':
        for (let id of ids) {
          let parts = id.split('/');
          await send('schedule/post-transaction', { id: parts[1] });
        }
        this.refetchTransactions();
        break;
      case 'skip':
        for (let id of ids) {
          let parts = id.split('/');
          await send('schedule/skip-next-date', { id: parts[1] });
        }
        break;
      default:
    }
  };

  applyFilters = async conditions => {
    if (conditions.length > 0) {
      let customFilters = conditions
        .filter(cond => !!cond.customName)
        .map(f => f.filter);
      let { filters } = await send('make-filters-from-conditions', {
        conditions: conditions.filter(cond => !cond.customName),
      });

      this.currentQuery = this.rootQuery.filter({
        $and: [...filters, ...customFilters],
      });
      this.updateQuery(this.currentQuery, true);
      this.setState({ filters: conditions, search: '' });
    } else {
      this.setState({ transactions: [], transactionCount: 0 });
      this.fetchTransactions();
      this.setState({ filters: conditions, search: '' });
    }
  };

  render() {
    let {
      accounts,
      categoryGroups,
      payees,
      syncEnabled,
      dateFormat,
      addNotification,
      accountsSyncing,
      replaceModal,
      showExtraBalances,
      accountId,
    } = this.props;
    let {
      transactions,
      loading,
      workingHard,
      reconcileAmount,
      transactionsFiltered,
      editingName,
      showBalances,
      balances,
      showCleared,
    } = this.state;

    let account = accounts.find(account => account.id === accountId);
    const accountName = this.getAccountTitle(account, accountId);

    if (!accountName && !loading) {
      // This is probably an account that was deleted, so redirect to
      // all accounts
      return <Redirect to="/accounts" />;
    }

    let showEmptyMessage = !loading && !accountId && accounts.length === 0;

    let isNameEditable =
      accountId &&
      accountId !== 'budgeted' &&
      accountId !== 'offbudget' &&
      accountId !== 'uncategorized';

    let balanceQuery = this.getBalanceQuery(account, accountId);

    return (
      <AllTransactions
        transactions={transactions}
        filtered={transactionsFiltered}
      >
        {allTransactions =>
          allTransactions == null ? null : (
            <SelectedProviderWithItems
              name="transactions"
              items={allTransactions}
              fetchAllIds={this.fetchAllIds}
              registerDispatch={dispatch => (this.dispatchSelected = dispatch)}
            >
              <View style={[styles.page, { backgroundColor: colors.n11 }]}>
                <AccountHeader
                  tableRef={this.table}
                  editingName={editingName}
                  isNameEditable={isNameEditable}
                  workingHard={workingHard}
                  account={account}
                  accountName={accountName}
                  accountsSyncing={accountsSyncing}
                  accounts={accounts}
                  transactions={transactions}
                  showBalances={showBalances}
                  showExtraBalances={showExtraBalances}
                  showCleared={showCleared}
                  showEmptyMessage={showEmptyMessage}
                  balanceQuery={balanceQuery}
                  syncEnabled={syncEnabled}
                  canCalculateBalance={this.canCalculateBalance}
                  reconcileAmount={reconcileAmount}
                  search={this.state.search}
                  filters={this.state.filters}
                  savePrefs={this.props.savePrefs}
                  onSearch={this.onSearch}
                  onShowTransactions={this.onShowTransactions}
                  onMenuSelect={this.onMenuSelect}
                  onAddTransaction={this.onAddTransaction}
                  onToggleExtraBalances={this.onToggleExtraBalances}
                  onSaveName={this.onSaveName}
                  onExposeName={this.onExposeName}
                  onReconcile={this.onReconcile}
                  onDoneReconciling={this.onDoneReconciling}
                  onCreateReconciliationTransaction={
                    this.onCreateReconciliationTransaction
                  }
                  onSync={this.onSync}
                  onImport={this.onImport}
                  onBatchDelete={this.onBatchDelete}
                  onBatchDuplicate={this.onBatchDuplicate}
                  onBatchEdit={this.onBatchEdit}
                  onBatchUnlink={this.onBatchUnlink}
                  onUpdateFilter={this.onUpdateFilter}
                  onDeleteFilter={this.onDeleteFilter}
                  onApplyFilter={this.onApplyFilter}
                  onScheduleAction={this.onScheduleAction}
                />

                <View style={{ flex: 1 }}>
                  <TransactionList
                    tableRef={this.table}
                    account={account}
                    transactions={transactions}
                    allTransactions={allTransactions}
                    animated={this.animated}
                    loadMoreTransactions={() =>
                      this.paged && this.paged.fetchNext()
                    }
                    accounts={accounts}
                    categoryGroups={categoryGroups}
                    payees={payees}
                    balances={
                      showBalances && this.canCalculateBalance()
                        ? balances
                        : null
                    }
                    showCleared={showCleared}
                    showAccount={
                      !accountId ||
                      accountId === 'offbudget' ||
                      accountId === 'budgeted' ||
                      accountId === 'uncategorized'
                    }
                    isAdding={this.state.isAdding}
                    isNew={this.isNew}
                    isMatched={this.isMatched}
                    isFiltered={
                      this.state.search !== '' || this.state.filters.length > 0
                    }
                    dateFormat={dateFormat}
                    addNotification={addNotification}
                    renderEmpty={() =>
                      showEmptyMessage ? (
                        <EmptyMessage
                          onAdd={() =>
                            replaceModal(
                              syncEnabled ? 'add-account' : 'add-local-account',
                            )
                          }
                        />
                      ) : !loading ? (
                        <View
                          style={{
                            marginTop: 20,
                            textAlign: 'center',
                            fontStyle: 'italic',
                          }}
                        >
                          No transactions
                        </View>
                      ) : null
                    }
                    onChange={this.onTransactionsChange}
                    onRefetch={this.refetchTransactions}
                    onRefetchUpToRow={row =>
                      this.paged.refetchUpToRow(row, {
                        field: 'date',
                        order: 'desc',
                      })
                    }
                    onCloseAddTransaction={() =>
                      this.setState({ isAdding: false })
                    }
                    onCreatePayee={this.onCreatePayee}
                  />
                </View>
              </View>
            </SelectedProviderWithItems>
          )
        }
      </AllTransactions>
    );
  }
}

function AccountHack(props) {
  let { dispatch: splitsExpandedDispatch } = useSplitsExpanded();
  return (
    <AccountInternal
      {...props}
      splitsExpandedDispatch={splitsExpandedDispatch}
    />
  );
}

export default function Account(props) {
  let state = useSelector(state => ({
    newTransactions: state.queries.newTransactions,
    matchedTransactions: state.queries.matchedTransactions,
    accounts: state.queries.accounts,
    failedAccounts: state.account.failedAccounts,
    categoryGroups: state.queries.categories.grouped,
    syncEnabled: state.prefs.local['flags.syncAccount'],
    dateFormat: state.prefs.local.dateFormat || 'MM/dd/yyyy',
    expandSplits: props.match && state.prefs.local['expand-splits'],
    showBalances:
      props.match &&
      state.prefs.local['show-balances-' + props.match.params.id],
    showCleared:
      props.match &&
      !state.prefs.local['hide-cleared-' + props.match.params.id],
    showExtraBalances:
      props.match &&
      state.prefs.local['show-extra-balances-' + props.match.params.id],
    payees: state.queries.payees,
    modalShowing: state.modals.modalStack.length > 0,
    accountsSyncing: state.account.accountsSyncing,
    lastUndoState: state.app.lastUndoState,
    tutorialStage: state.tutorial.stage,
  }));

  let dispatch = useDispatch();
  let actionCreators = useMemo(
    () => bindActionCreators(actions, dispatch),
    [dispatch],
  );

  let params = useParams();
  let location = useLocation();
  let activeLocation = useActiveLocation();

  let transform = useMemo(() => {
    let filter = queries.getAccountFilter(params.id, '_account');

    // Never show schedules on these pages
    if (
      (location.state && location.state.filter) ||
      params.id === 'uncategorized'
    ) {
      filter = { id: null };
    }

    return q => {
      q = q.filter({ $and: [filter, { '_account.closed': false }] });
      return q.orderBy({ next_date: 'desc' });
    };
  }, [params.id]);

  return (
    <SchedulesProvider transform={transform}>
      <SplitsExpandedProvider
        initialMode={state.expandSplits ? 'collapse' : 'expand'}
      >
        <AccountHack
          {...state}
          {...actionCreators}
          modalShowing={
            state.modalShowing ||
            !!(activeLocation.state && activeLocation.state.locationPtr)
          }
          accountId={params.id}
          location={props.location}
        />
      </SplitsExpandedProvider>
    </SchedulesProvider>
  );
}