import { v4 as uuidv4 } from 'uuid';

import {
  type TransactionEntity,
  type NewTransactionEntity,
} from '../types/models';

import { last, diffItems, applyChanges } from './util';

interface TransactionEntityWithError extends TransactionEntity {
  error: ReturnType<typeof SplitTransactionError> | null;
  _deleted?: boolean;
}

export function isTemporaryId(id: string) {
  return id.indexOf('temp') !== -1;
}

export function isPreviewId(id: string) {
  return id.indexOf('preview/') !== -1;
}

// The amount might be null when adding a new transaction
function num(n: number | null | undefined) {
  return typeof n === 'number' ? n : 0;
}

function SplitTransactionError(total: number, parent: TransactionEntity) {
  const difference = num(parent.amount) - total;

  return {
    type: 'SplitTransactionError',
    version: 1,
    difference,
  };
}

type GenericTransactionEntity =
  | NewTransactionEntity
  | TransactionEntity
  | TransactionEntityWithError;

export function makeChild<T extends GenericTransactionEntity>(
  parent: T,
  data: object = {},
) {
  const prefix = parent.id === 'temp' ? 'temp' : '';

  return {
    amount: 0,
    ...data,
    category: 'category' in data ? data.category : parent.category,
    payee: 'payee' in data ? data.payee : parent.payee,
    id: 'id' in data ? data.id : prefix + uuidv4(),
    account: parent.account,
    date: parent.date,
    cleared: parent.cleared != null ? parent.cleared : null,
    reconciled: 'reconciled' in data ? data.reconciled : parent.reconciled,
    starting_balance_flag:
      parent.starting_balance_flag != null
        ? parent.starting_balance_flag
        : null,
    is_child: true,
    parent_id: parent.id,
    error: null,
  } as unknown as T;
}

function makeNonChild<T extends GenericTransactionEntity>(
  parent: T,
  data: object,
) {
  return {
    amount: 0,
    ...data,
    cleared: parent.cleared != null ? parent.cleared : null,
    reconciled: parent.reconciled != null ? parent.reconciled : null,
    sort_order: parent.sort_order || null,
    starting_balance_flag: null,
    is_child: false,
    parent_id: null,
  } as unknown as T;
}

export function recalculateSplit(trans: TransactionEntity) {
  // Calculate the new total of split transactions and make sure
  // that it equals the parent amount
  const total = (trans.subtransactions || []).reduce(
    (acc, t) => acc + num(t.amount),
    0,
  );
  return {
    ...trans,
    error:
      total === num(trans.amount) ? null : SplitTransactionError(total, trans),
  } as TransactionEntityWithError;
}

function findParentIndex(transactions: TransactionEntity[], idx: number) {
  // This relies on transactions being sorted in a way where parents
  // are always before children, which is enforced in the db layer.
  // Walk backwards and find the last parent;
  while (idx >= 0) {
    const trans = transactions[idx];
    if (trans.is_parent) {
      return idx;
    }
    idx--;
  }
  return null;
}

function getSplit(transactions: TransactionEntity[], parentIndex: number) {
  const split = [transactions[parentIndex]];
  let curr = parentIndex + 1;
  while (curr < transactions.length && transactions[curr].is_child) {
    split.push(transactions[curr]);
    curr++;
  }
  return split;
}

export function ungroupTransactions(transactions: TransactionEntity[]) {
  return transactions.reduce<TransactionEntity[]>((list, parent) => {
    const { subtransactions, ...trans } = parent;
    const _subtransactions = subtransactions || [];

    list.push(trans);

    for (let i = 0; i < _subtransactions.length; i++) {
      list.push(_subtransactions[i]);
    }
    return list;
  }, []);
}

export function groupTransaction(split: TransactionEntity[]) {
  return { ...split[0], subtransactions: split.slice(1) } as TransactionEntity;
}

export function ungroupTransaction(split: TransactionEntity | null) {
  if (split == null) {
    return null;
  }
  return ungroupTransactions([split]);
}

export function applyTransactionDiff(
  groupedTrans: Parameters<typeof ungroupTransaction>[0],
  diff: Parameters<typeof applyChanges>[0],
) {
  return groupTransaction(
    applyChanges(
      diff,
      ungroupTransaction(groupedTrans) || [],
    ) as TransactionEntity[],
  );
}

function replaceTransactions(
  transactions: TransactionEntity[],
  id: string,
  func: (
    transaction: TransactionEntity,
  ) => TransactionEntity | TransactionEntityWithError | null,
) {
  const idx = transactions.findIndex(t => t.id === id);
  const trans = transactions[idx];
  const transactionsCopy = [...transactions];

  if (idx === -1) {
    throw new Error('Tried to edit unknown transaction id: ' + id);
  }

  if (trans.is_parent || trans.is_child) {
    const parentIndex = findParentIndex(transactions, idx);
    if (parentIndex == null) {
      console.log('Cannot find parent index');
      return { data: [], diff: { deleted: [], updated: [] } };
    }

    const split = getSplit(transactions, parentIndex);
    let grouped = func(groupTransaction(split));
    const newSplit = ungroupTransaction(grouped);

    let diff;
    if (newSplit == null) {
      // If everything was deleted, just delete the parent which will
      // delete everything
      diff = { deleted: [{ id: split[0].id }], updated: [] };
      grouped = { ...split[0], _deleted: true };
      transactionsCopy.splice(parentIndex, split.length);
    } else {
      diff = diffItems(split, newSplit);
      transactionsCopy.splice(parentIndex, split.length, ...newSplit);
    }

    return { data: transactionsCopy, newTransaction: grouped, diff };
  } else {
    const grouped = func(trans);
    const newTrans = ungroupTransaction(grouped) || [];
    if (grouped) {
      grouped.subtransactions = grouped.subtransactions || [];
    }
    transactionsCopy.splice(idx, 1, ...newTrans);

    return {
      data: transactionsCopy,
      newTransaction: grouped || {
        ...trans,
        _deleted: true,
      },
      diff: diffItems([trans], newTrans),
    };
  }
}

export function addSplitTransaction(
  transactions: TransactionEntity[],
  id: string,
) {
  return replaceTransactions(transactions, id, trans => {
    if (!trans.is_parent) {
      return trans;
    }
    const prevSub = last(trans.subtransactions || []);
    trans.subtransactions?.push(
      makeChild(trans, {
        amount: 0,
        sort_order: num(prevSub && prevSub.sort_order) - 1,
      }),
    );
    return trans;
  });
}

export function updateTransaction(
  transactions: TransactionEntity[],
  transaction: TransactionEntity,
) {
  return replaceTransactions(transactions, transaction.id, trans => {
    if (trans.is_parent) {
      const parent = trans.id === transaction.id ? transaction : trans;
      const originalSubtransactions =
        parent.subtransactions ?? trans.subtransactions;
      const sub = originalSubtransactions?.map(t => {
        // Make sure to update the children to reflect the updated
        // properties (if the parent updated)

        let child = t;
        if (trans.id === transaction.id) {
          child = {
            ...t,
            payee: t.payee === trans.payee ? transaction.payee : t.payee,
          };
        } else if (t.id === transaction.id) {
          child = transaction;
        }

        return makeChild(parent, child);
      });

      return recalculateSplit({ ...parent, subtransactions: sub });
    } else {
      return transaction;
    }
  });
}

export function deleteTransaction(
  transactions: TransactionEntity[],
  id: string,
) {
  return replaceTransactions(transactions, id, trans => {
    if (trans.is_parent) {
      if (trans.id === id) {
        return null;
      } else if (trans.subtransactions?.length === 1) {
        return {
          ...trans,
          subtransactions: undefined,
          is_parent: false,
          error: null,
        } as TransactionEntityWithError;
      } else {
        const sub = trans.subtransactions?.filter(t => t.id !== id);
        return recalculateSplit({ ...trans, subtransactions: sub });
      }
    } else {
      return null;
    }
  });
}

export function splitTransaction(
  transactions: TransactionEntity[],
  id: string,
  createSubtransactions?: (
    parentTransaction: TransactionEntity,
  ) => TransactionEntity[],
) {
  return replaceTransactions(transactions, id, trans => {
    if (trans.is_parent || trans.is_child) {
      return trans;
    }

    const subtransactions = createSubtransactions?.(trans) || [
      makeChild(trans),
    ];

    return {
      ...trans,
      is_parent: true,
      error: num(trans.amount) === 0 ? null : SplitTransactionError(0, trans),
      subtransactions: subtransactions.map(t => ({
        ...t,
        sort_order: t.sort_order || -1,
      })),
    } as TransactionEntityWithError;
  });
}

export function realizeTempTransactions(transactions: TransactionEntity[]) {
  const parent = { ...transactions.find(t => !t.is_child), id: uuidv4() };
  const children = transactions.filter(t => t.is_child);
  return [
    parent,
    ...children.map(child => ({
      ...child,
      id: uuidv4(),
      parent_id: parent.id,
    })),
  ];
}

export function makeAsNonChildTransactions(
  childTransactionsToUpdate: TransactionEntity[],
  transactions: TransactionEntity[],
) {
  const [parentTransaction, ...childTransactions] = transactions;
  const newNonChildTransactions = childTransactionsToUpdate.map(t =>
    makeNonChild(parentTransaction, t),
  );

  const remainingChildTransactions = childTransactions.filter(
    t =>
      !newNonChildTransactions.some(updatedTrans => updatedTrans.id === t.id),
  );

  const nonChildTransactionsToUpdate =
    remainingChildTransactions.length === 1
      ? [
          ...newNonChildTransactions,
          makeNonChild(parentTransaction, remainingChildTransactions[0]),
        ]
      : newNonChildTransactions;

  const deleteParentTransaction = remainingChildTransactions.length <= 1;

  const updatedParentTransaction = {
    ...parentTransaction,
    ...(!deleteParentTransaction
      ? {
          amount: remainingChildTransactions
            .map(t => t.amount)
            .reduce((total, amount) => total + amount, 0),
        }
      : {}),
  };

  return {
    updated: [
      ...(!deleteParentTransaction ? [updatedParentTransaction] : []),
      ...nonChildTransactionsToUpdate,
    ],
    deleted: [...(deleteParentTransaction ? [updatedParentTransaction] : [])],
  };
}