import express from 'express';
import { query, oneOf, validationResult } from 'express-validator';
import moment from 'moment';
import { pick } from 'lodash';
import JournalPoster from '@/services/Accounting/JournalPoster';
import { dateRangeCollection } from '@/utils';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import { formatNumberClosure } from './FinancialStatementMixin';
import DependencyGraph from '@/lib/DependencyGraph';

export default {
  /**
   * Router constructor.
   */
  router() {
    const router = express.Router();

    router.get('/', 
      this.profitLossSheet.validation,
      asyncMiddleware(this.profitLossSheet.handler));

    return router;
  },

  /**
   * Retrieve profit/loss financial statement.
   */
  profitLossSheet: {
    validation: [
      query('basis').optional(),
      query('from_date').optional().isISO8601(),
      query('to_date').optional().isISO8601(),
      query('number_format.no_cents').optional().isBoolean(),
      query('number_format.divide_1000').optional().isBoolean(),
      query('basis').optional(),
      query('none_zero').optional().isBoolean().toBoolean(),
      query('account_ids').isArray().optional(),
      query('account_ids.*').isNumeric().toInt(),
      query('display_columns_type').optional().isIn([
        'total', 'date_periods',
      ]),
      query('display_columns_by').optional({ nullable: true, checkFalsy: true })
        .isIn(['year', 'month', 'week', 'day', 'quarter']),
    ],
    async handler(req, res) {
      const validationErrors = validationResult(req);

      if (!validationErrors.isEmpty()) {
        return res.boom.badData(null, {
          code: 'validation_error', ...validationErrors,
        });
      }
      const { Account, AccountType } = req.models;
      const filter = {
        from_date: moment().startOf('year').format('YYYY-MM-DD'),
        to_date: moment().endOf('year').format('YYYY-MM-DD'),
        number_format: {
          no_cents: false,
          divide_1000: false,
        },
        basis: 'accural',
        none_zero: false,
        display_columns_type: 'total',
        display_columns_by: 'month',
        account_ids: [],
        ...req.query,
      };
      if (!Array.isArray(filter.account_ids)) {
        filter.account_ids = [filter.account_ids];
      }
      const incomeStatementTypes = await AccountType.query().where('income_sheet', true);

      // Fetch all income accounts from storage.
      const accounts = await Account.query()
        // .remember('profit_loss_accounts')
        .modify('filterAccounts', filter.account_ids)
        .whereIn('account_type_id', incomeStatementTypes.map((t) => t.id))
        .withGraphFetched('type')
        .withGraphFetched('transactions');

      // Accounts dependency graph.
      const accountsGraph = DependencyGraph.fromArray(
        accounts, { itemId: 'id', parentItemId: 'parentAccountId' }
      );

      // Filter all none zero accounts if it was enabled.
      const filteredAccounts = accounts.filter((account) => (
        account.transactions.length > 0 || !filter.none_zero
      ));
      const journalEntriesCollected = Account.collectJournalEntries(accounts);
      const journalEntries = new JournalPoster(accountsGraph);
      journalEntries.loadEntries(journalEntriesCollected);

      // Account balance formmatter based on the given query.
      const numberFormatter = formatNumberClosure(filter.number_format);
      const comparatorDateType = filter.display_columns_type === 'total'
        ? 'day' : filter.display_columns_by;

      // Gets the date range set from start to end date.
      const dateRangeSet = dateRangeCollection(
        filter.from_date,
        filter.to_date,
        comparatorDateType,
      );

      const accountsMapper = (incomeExpenseAccounts) => (
        incomeExpenseAccounts.map((account) => ({
          ...pick(account, ['id', 'index', 'name', 'code', 'parentAccountId']),

          // Total closing balance of the account.
          ...(filter.display_columns_type === 'total') && {
            total: (() => {
              const amount = journalEntries.getAccountBalance(account.id, filter.to_date);
              return { amount, date: filter.to_date, formatted_amount: numberFormatter(amount) };
            })(),
          },
          // Date periods when display columns type `periods`.
          ...(filter.display_columns_type === 'date_periods') && {
            periods: dateRangeSet.map((date) => {
              const type = comparatorDateType;
              const amount = journalEntries.getAccountBalance(account.id, date, type);

              return { date, amount, formatted_amount: numberFormatter(amount) };
            }),
          },
        })));

      const totalAccountsReducer = (incomeExpenseAccounts) => (
        incomeExpenseAccounts.reduce((acc, account) => {
          const amount = (account) ? account.total.amount : 0;
          return amount + acc;
        }, 0));

      const accountsIncome = Account.toNestedArray(accountsMapper(filteredAccounts
        .filter((account) => account.type.normal === 'credit')));

      const accountsExpenses = Account.toNestedArray(accountsMapper(filteredAccounts
        .filter((account) => account.type.normal === 'debit')));

      // @return {Array}
      const totalPeriodsMapper = (incomeExpenseAccounts) => (
        Object.values(dateRangeSet.reduce((acc, date, index) => {
          let amount = 0;

          incomeExpenseAccounts.forEach((account) => {
            const currentDate = account.periods[index];
            amount += currentDate.amount || 0;
          });
          acc[date] = { date, amount, formatted_amount: numberFormatter(amount) };
          return acc;
        }, {})));

      // Total income(date) - Total expenses(date) = Net income(date)
      // @return {Array}
      const netIncomePeriodsMapper = (totalIncomeAcocunts, totalExpenseAccounts) => (
        dateRangeSet.map((date, index) => {
          const totalIncome = totalIncomeAcocunts[index];
          const totalExpenses = totalExpenseAccounts[index];

          let amount = totalIncome.amount || 0;
          amount -= totalExpenses.amount || 0;
          return { date, amount, formatted_amount: numberFormatter(amount) };
        }));

      // @return {Object}
      const netIncomeTotal = (totalIncome, totalExpenses) => {
        const netIncomeAmount = totalIncome.amount - totalExpenses.amount;
        return { amount: netIncomeAmount, formatted_amount: netIncomeAmount, date: filter.to_date };
      };

      const incomeResponse = {
        entry_normal: 'credit',
        accounts: accountsIncome,
        ...(filter.display_columns_type === 'total') && (() => {
          const totalIncomeAccounts = totalAccountsReducer(accountsIncome);
          return {
            total: {
              amount: totalIncomeAccounts,
              date: filter.to_date,
              formatted_amount: numberFormatter(totalIncomeAccounts),
            },
          };
        })(),
        ...(filter.display_columns_type === 'date_periods') && {
          total_periods: [
            ...totalPeriodsMapper(accountsIncome),
          ],
        },
      };
      const expenseResponse = {
        entry_normal: 'debit',
        accounts: accountsExpenses,
        ...(filter.display_columns_type === 'total') && (() => {
          const totalExpensesAccounts = totalAccountsReducer(accountsExpenses);
          return {
            total: {
              amount: totalExpensesAccounts,
              date: filter.to_date,
              formatted_amount: numberFormatter(totalExpensesAccounts),
            },
          };
        })(),
        ...(filter.display_columns_type === 'date_periods') && {
          total_periods: [
            ...totalPeriodsMapper(accountsExpenses),
          ],
        },
      };
      const netIncomeResponse = {
        ...(filter.display_columns_type === 'total') && {
          total: {
            ...netIncomeTotal(incomeResponse.total, expenseResponse.total),
          },
        },
        ...(filter.display_columns_type === 'date_periods') && {
          total_periods: [
            ...netIncomePeriodsMapper(
              incomeResponse.total_periods,
              expenseResponse.total_periods,
            ),
          ],
        },
      };
      return res.status(200).send({
        query: { ...filter },
        columns: [...dateRangeSet],
        profitLoss: {
          income: incomeResponse,
          expenses: expenseResponse,
          net_income: netIncomeResponse,
        },        
      });
    },
  },
}