import {
  differenceInCalendarMonths,
  addMonths,
  addWeeks,
  format,
} from 'date-fns';

import * as monthUtils from '../../shared/months';
import {
  extractScheduleConds,
  getScheduledAmount,
} from '../../shared/schedules';
import { amountToInteger, integerToAmount } from '../../shared/util';
import * as db from '../db';
import { getRuleForSchedule, getNextDate } from '../schedules/app';

import { setBudget, getSheetValue } from './actions';
import { parse } from './goal-template.pegjs';

export function applyTemplate({ month }) {
  return processTemplate(month, false);
}

export function overwriteTemplate({ month }) {
  return processTemplate(month, true);
}

function checkScheduleTemplates(template) {
  let lowPriority = template[0].priority;
  let errorNotice = false;
  for (let l = 1; l < template.length; l++) {
    if (template[l].priority !== lowPriority) {
      lowPriority = Math.min(lowPriority, template[l].priority);
      errorNotice = true;
    }
  }
  return { lowPriority, errorNotice };
}

async function processTemplate(month, force) {
  let num_applied = 0;
  let errors = [];
  let category_templates = await getCategoryTemplates();
  let lowestPriority = 0;
  let originalCategoryBalance = [];

  let categories = await db.all(
    'SELECT * FROM v_categories WHERE tombstone = 0',
  );

  //clears templated categories
  for (let c = 0; c < categories.length; c++) {
    let category = categories[c];
    let budgeted = await getSheetValue(
      monthUtils.sheetForMonth(month),
      `budget-${category.id}`,
    );
    if (budgeted) {
      originalCategoryBalance.push({ cat: category, amount: budgeted });
    }
    let template = category_templates[category.id];
    if (template) {
      for (let l = 0; l < template.length; l++) {
        lowestPriority =
          template[l].priority > lowestPriority
            ? template[l].priority
            : lowestPriority;
      }
      await setBudget({
        category: category.id,
        month,
        amount: 0,
      });
    }
  }

  for (let priority = 0; priority <= lowestPriority; priority++) {
    for (let c = 0; c < categories.length; c++) {
      let category = categories[c];
      let template = category_templates[category.id];
      if (template) {
        //check that all schedule and by lines have the same priority level
        let skipSchedule = false;
        let isScheduleOrBy = false;
        let priorityCheck = 0;
        if (
          template.filter(t => t.type === 'schedule' || t.type === 'by')
            .length > 0
        ) {
          let { lowPriority, errorNotice } = await checkScheduleTemplates(
            template,
          );
          priorityCheck = lowPriority;
          skipSchedule = priorityCheck !== priority ? true : false;
          isScheduleOrBy = true;
          if (!skipSchedule && errorNotice) {
            errors.push(
              category.name +
                ': Schedules and By templates should all have the same priority.  Using priority ' +
                priorityCheck,
            );
          }
        }
        if (!skipSchedule) {
          if (!isScheduleOrBy) {
            template = template.filter(t => t.priority === priority);
          }
          if (template.length > 0) {
            errors = errors.concat(
              template
                .filter(t => t.type === 'error')
                .map(({ line, error }) =>
                  [
                    category.name + ': ' + error.message,
                    line,
                    ' '.repeat(
                      TEMPLATE_PREFIX.length + error.location.start.offset,
                    ) + '^',
                  ].join('\n'),
                ),
            );
            let { amount: to_budget, errors: applyErrors } =
              await applyCategoryTemplate(
                category,
                template,
                month,
                priority,
                force,
              );
            if (to_budget != null) {
              num_applied++;
              await setBudget({
                category: category.id,
                month,
                amount: to_budget,
              });
            }
            if (applyErrors != null) {
              errors = errors.concat(
                applyErrors.map(error => `${category.name}: ${error}`),
              );
            }
          }
        }
      }
    }
  }
  if (!force) {
    //if overwrite is not preferred, set cell to original value
    for (let l = 0; l < originalCategoryBalance.length; l++) {
      await setBudget({
        category: originalCategoryBalance[l].cat.id,
        month,
        amount: originalCategoryBalance[l].amount,
      });
      //if overwrite is not preferred, remove template errors for category
      let j = errors.length;
      for (let k = 0; k < j; k++) {
        if (errors[k].includes(originalCategoryBalance[l].cat.name)) {
          errors.splice(k, 1);
          j--;
        }
      }
    }
  }

  if (num_applied === 0) {
    if (errors.length) {
      return {
        type: 'error',
        sticky: true,
        message: `There were errors interpreting some templates:`,
        pre: errors.join('\n\n'),
      };
    } else {
      return { type: 'message', message: 'All categories were up to date.' };
    }
  } else {
    let applied = `Successfully applied templates to ${num_applied} ${
      num_applied === 1 ? 'category' : 'categories'
    }.`;
    if (errors.length) {
      return {
        sticky: true,
        message: `${applied} There were errors interpreting some templates:`,
        pre: errors.join('\n\n'),
      };
    } else {
      return {
        type: 'message',
        message: applied,
      };
    }
  }
}

const TEMPLATE_PREFIX = '#template';
async function getCategoryTemplates() {
  let templates = {};

  let notes = await db.all(
    `SELECT * FROM notes WHERE lower(note) like '%${TEMPLATE_PREFIX}%'`,
  );

  for (let n = 0; n < notes.length; n++) {
    let lines = notes[n].note.split('\n');
    let template_lines = [];
    for (let l = 0; l < lines.length; l++) {
      let line = lines[l].trim();
      if (!line.toLowerCase().startsWith(TEMPLATE_PREFIX)) continue;
      let expression = line.slice(TEMPLATE_PREFIX.length);
      try {
        let parsed = parse(expression);
        template_lines.push(parsed);
      } catch (e) {
        template_lines.push({ type: 'error', line, error: e });
      }
    }
    if (template_lines.length) {
      templates[notes[n].id] = template_lines;
    }
  }
  return templates;
}

async function applyCategoryTemplate(
  category,
  template_lines,
  month,
  priority,
  force,
) {
  let current_month = new Date(`${month}-01`);
  let errors = [];
  let all_schedule_names = await db.all(
    'SELECT name from schedules WHERE name NOT NULL AND tombstone = 0',
  );
  all_schedule_names = all_schedule_names.map(v => v.name);

  // remove lines for past dates, calculate repeating dates
  template_lines = template_lines.filter(template => {
    switch (template.type) {
      case 'by':
      case 'spend':
        let target_month = new Date(`${template.month}-01`);
        let num_months = differenceInCalendarMonths(
          target_month,
          current_month,
        );
        let repeat = template.annual
          ? (template.repeat || 1) * 12
          : template.repeat;

        let spend_from;
        if (template.type === 'spend') {
          spend_from = new Date(`${template.from}-01`);
        }
        while (num_months < 0 && repeat) {
          target_month = addMonths(target_month, repeat);
          if (spend_from) {
            spend_from = addMonths(spend_from, repeat);
          }
          num_months = differenceInCalendarMonths(target_month, current_month);
        }
        if (num_months < 0) {
          errors.push(`${template.month} is in the past.`);
          return false;
        }
        template.month = format(target_month, 'yyyy-MM');
        if (spend_from) {
          template.from = format(spend_from, 'yyyy-MM');
        }
        break;
      case 'schedule':
        if (!all_schedule_names.includes(template.name)) {
          errors.push(`Schedule ${template.name} does not exist`);
          return null;
        }
        break;
      default:
    }
    return true;
  });

  if (template_lines.length > 1) {
    template_lines = template_lines.sort((a, b) => {
      if (a.type === 'by' && !a.annual) {
        return differenceInCalendarMonths(
          new Date(`${a.month}-01`),
          new Date(`${b.month}-01`),
        );
      } else {
        return a.type.localeCompare(b.type);
      }
    });
  }

  let sheetName = monthUtils.sheetForMonth(month);
  let budgeted = await getSheetValue(sheetName, `budget-${category.id}`);
  let spent = await getSheetValue(sheetName, `sum-amount-${category.id}`);
  let balance = await getSheetValue(sheetName, `leftover-${category.id}`);
  let budgetAvailable = await getSheetValue(sheetName, `to-budget`);
  let to_budget = budgeted;
  let limit;
  let hold;
  let last_month_balance = balance - spent - budgeted;
  let remainder = 0;
  for (let l = 0; l < template_lines.length; l++) {
    let template = template_lines[l];
    switch (template.type) {
      case 'simple': {
        // simple has 'monthly' and/or 'limit' params
        if (template.limit != null) {
          if (limit != null) {
            errors.push(`More than one “up to” limit found.`);
            return { errors };
          } else {
            limit = amountToInteger(template.limit.amount);
            hold = template.limit.hold;
          }
        }
        let increment = 0;
        if (template.monthly != null) {
          let monthly = amountToInteger(template.monthly);
          increment = monthly;
        } else {
          increment = limit;
        }
        if (to_budget + increment < budgetAvailable || !priority) {
          to_budget += increment;
        } else {
          if (budgetAvailable > 0) to_budget += budgetAvailable;
          errors.push(`Insufficient funds.`);
        }
        break;
      }
      case 'by': {
        // by has 'amount' and 'month' params
        let target = 0;
        let target_month = new Date(`${template_lines[l].month}-01`);
        let num_months = differenceInCalendarMonths(
          target_month,
          current_month,
        );
        let repeat =
          template.type === 'by'
            ? template.repeat
            : (template.repeat || 1) * 12;
        while (num_months < 0 && repeat) {
          target_month = addMonths(target_month, repeat);
          num_months = differenceInCalendarMonths(
            template_lines[l],
            current_month,
          );
        }
        if (l === 0) remainder = last_month_balance;
        remainder = amountToInteger(template_lines[l].amount) - remainder;
        if (remainder >= 0) {
          target = remainder;
          remainder = 0;
        } else {
          target = 0;
          remainder = Math.abs(remainder);
        }
        let diff = num_months > -1 ? Math.round(target / (num_months + 1)) : 0;
        if (diff >= 0) {
          if (to_budget + diff < budgetAvailable || !priority) {
            to_budget += diff;
          } else {
            if (budgetAvailable > 0) to_budget += budgetAvailable;
            errors.push(`Insufficient funds.`);
          }
        }
        break;
      }
      case 'week': {
        // week has 'amount', 'starting', 'weeks' and optional 'limit' params
        let amount = amountToInteger(template.amount);
        let weeks = template.weeks != null ? Math.round(template.weeks) : 1;
        if (template.limit != null) {
          if (limit != null) {
            errors.push(`More than one “up to” limit found.`);
            return { errors };
          } else {
            limit = amountToInteger(template.limit.amount);
            hold = template.limit.hold;
          }
        }
        let w = new Date(template.starting);

        let next_month = addMonths(current_month, 1);

        while (w.getTime() < next_month.getTime()) {
          if (w.getTime() >= current_month.getTime()) {
            if (to_budget + amount < budgetAvailable || !priority) {
              to_budget += amount;
            } else {
              if (budgetAvailable > 0) to_budget += budgetAvailable;
              errors.push(`Insufficient funds.`);
            }
            w = addWeeks(w, weeks);
          }
        }
        break;
      }
      case 'spend': {
        // spend has 'amount' and 'from' and 'month' params
        let from_month = new Date(`${template.from}-01`);
        let to_month = new Date(`${template.month}-01`);
        let already_budgeted = last_month_balance;
        let first_month = true;
        for (
          let m = from_month;
          differenceInCalendarMonths(current_month, m) > 0;
          m = addMonths(m, 1)
        ) {
          let sheetName = monthUtils.sheetForMonth(format(m, 'yyyy-MM'));

          if (first_month) {
            let spent = await getSheetValue(
              sheetName,
              `sum-amount-${category.id}`,
            );
            let balance = await getSheetValue(
              sheetName,
              `leftover-${category.id}`,
            );
            already_budgeted = balance - spent;
            first_month = false;
          } else {
            let budgeted = await getSheetValue(
              sheetName,
              `budget-${category.id}`,
            );
            already_budgeted += budgeted;
          }
        }
        let num_months = differenceInCalendarMonths(to_month, current_month);
        let target = amountToInteger(template.amount);

        let increment = 0;
        if (num_months < 0) {
          errors.push(`${template.month} is in the past.`);
          return { errors };
        } else if (num_months === 0) {
          increment = target - already_budgeted;
        } else {
          increment = Math.round(
            (target - already_budgeted) / (num_months + 1),
          );
        }
        if (increment < budgetAvailable || !priority) {
          to_budget = increment;
        } else {
          if (budgetAvailable > 0) to_budget = budgetAvailable;
          errors.push(`Insufficient funds.`);
        }
        break;
      }
      case 'percentage': {
        let percent = template.percent;
        let monthlyIncome = 0;
        if (template.category.toLowerCase() === 'all income') {
          monthlyIncome = await getSheetValue(sheetName, `total-income`);
        } else {
          let income_category = (await db.getCategories()).find(
            c =>
              c.is_income &&
              c.name.toLowerCase() === template.category.toLowerCase(),
          );
          if (!income_category) {
            errors.push(`Could not find category “${template.category}”`);
            return { errors };
          }
          monthlyIncome = await getSheetValue(
            sheetName,
            `sum-amount-${income_category.id}`,
          );
        }
        let increment = Math.max(
          0,
          Math.round(monthlyIncome * (percent / 100)),
        );
        if (increment < budgetAvailable || !priority) {
          to_budget = increment;
        } else {
          if (budgetAvailable > 0) to_budget = budgetAvailable;
          errors.push(`Insufficient funds.`);
        }
        break;
      }
      case 'error':
        return { errors };
      default:
      case 'schedule': {
        let { id: schedule_id } = await db.first(
          'SELECT id FROM schedules WHERE name = ?',
          [template.name],
        );
        let rule = await getRuleForSchedule(schedule_id);
        let conditions = rule.serialize().conditions;
        let { date: dateCond, amount: amountCond } =
          extractScheduleConds(conditions);
        let next_date_string = getNextDate(dateCond, current_month);
        let num_months = differenceInCalendarMonths(
          new Date(next_date_string),
          current_month,
        );
        if (l === 0) remainder = last_month_balance;
        remainder = -getScheduledAmount(amountCond.value) - remainder;
        let target = 0;
        if (remainder >= 0) {
          target = remainder;
          remainder = 0;
        } else {
          target = 0;
          remainder = Math.abs(remainder);
        }
        let diff = num_months > 0 ? Math.round(target / num_months) : 0;
        if (num_months < 0) {
          errors.push(
            `Non-repeating schedule ${template.name} was due on ${next_date_string}, which is in the past.`,
          );
          return { errors };
        } else if (num_months > 0) {
          if (
            (diff >= 0 &&
              num_months > -1 &&
              to_budget + diff < budgetAvailable) ||
            !priority
          ) {
            to_budget += diff;
          } else {
            if (budgetAvailable > 0) to_budget = budgetAvailable;
            errors.push(`Insufficient funds.`);
          }
        }
        break;
      }
    }
  }

  if (limit != null) {
    if (hold && balance > limit) {
      to_budget = 0;
    } else if (to_budget + last_month_balance > limit) {
      to_budget = limit - last_month_balance;
    }
  }
  if (
    ((category.budgeted != null && category.budgeted !== 0) ||
      to_budget === 0) &&
    !force
  ) {
    return { errors };
  } else if (category.budgeted === to_budget) {
    return null;
  } else {
    let str = category.name + ': ' + integerToAmount(last_month_balance);
    str +=
      ' + ' +
      integerToAmount(to_budget) +
      ' = ' +
      integerToAmount(last_month_balance + to_budget);
    str += ' ' + template_lines.map(x => x.line).join('\n');
    console.log(str);
    return { amount: to_budget, errors };
  }
}