import { Map } from 'immutable';
import { dataFns } from './utils/data_utils';
const { get, set } = dataFns(['sync']);
import * as l from './core/index';

import { getEntity, observe, read, swap, updateEntity } from './store/index';

export default (m, key, opts) => {
  if (get(m, key) !== undefined) return m;

  const status = opts.waitFn
    ? 'waiting'
    : !opts.conditionFn || opts.conditionFn(m)
    ? 'pending'
    : 'no';

  return set(
    m,
    key,
    Map({
      conditionFn: opts.conditionFn,
      errorFn: opts.errorFn,
      recoverResult: opts.recoverResult,
      syncStatus: status,
      successFn: opts.successFn,
      syncFn: opts.syncFn,
      timeout: opts.timeout || 6000,
      waitFn: opts.waitFn
    })
  );
};

const syncStatusKey = key => (window.Array.isArray(key) ? key : [key]).concat(['syncStatus']);
const getStatus = (m, key) => get(m, syncStatusKey(key));
const setStatus = (m, key, str) => set(m, syncStatusKey(key), str);
const getProp = (m, key, name) => get(m, key).get(name);

const findKeys = m => {
  return m.reduce((r, v, k) => {
    const current = Map.isMap(v) && v.has('syncStatus') ? [k] : [];
    const nested = Map.isMap(v) ? findKeys(v).map(x => [k].concat(x)) : [];
    return r.concat(current, ...[nested]);
  }, []);
};

function removeKeys(m, keys) {
  return keys.reduce((r, k) => r.deleteIn(syncStatusKey(k)), m);
}

const process = (m, id) => {
  const keys = findKeys(get(m, [], Map()));
  // TODO timeout
  return keys.reduce((r, k) => {
    if (typeof getProp(r, k, 'syncFn') != 'function') return r;
    if (getStatus(r, k) === 'pending') {
      r = setStatus(r, k, 'loading');
      let called = false;
      getProp(
        r,
        k,
        'syncFn'
      )(r, (error, result) => {
        if (called) return;
        called = true;
        setTimeout(() => {
          swap(updateEntity, 'lock', id, m => {
            const errorFn = getProp(r, k, 'errorFn');

            if (error && typeof errorFn === 'function') {
              setTimeout(() => errorFn(m, error), 0);
            }

            const recoverResult = getProp(m, k, 'recoverResult');

            if (error && recoverResult === undefined) {
              return handleError(m, k, error);
            } else {
              m = setStatus(m, k, 'ok');
              return getProp(m, k, 'successFn')(m, error ? recoverResult : result);
            }
          });
        }, 0);
      });
    } else if (getStatus(r, k) === 'waiting') {
      if (getProp(r, k, 'waitFn')(r)) {
        const conditionFn = getProp(r, k, 'conditionFn');
        r = setStatus(r, k, !conditionFn || conditionFn(r) ? 'pending' : 'no');
      }
    }

    return r;
  }, m);
};

export const go = id => {
  observe('sync', id, m => {
    setTimeout(() => swap(updateEntity, 'lock', id, process, id), 0);
  });
};

export function isSuccess(m, key) {
  return getStatus(m, key) === 'ok';
}

export function isDone(m) {
  const keys = findKeys(get(m, [], Map()));
  return keys.length > 0 && keys.reduce((r, k) => r && !isLoading(m, k), true);
}

export function hasError(m, excludeKeys = []) {
  const keys = findKeys(removeKeys(get(m, [], Map()), excludeKeys));
  return keys.length > 0 && keys.reduce((r, k) => r || getStatus(m, k) === 'error', false);
}

function isLoading(m, key) {
  return ['loading', 'pending', 'waiting'].indexOf(getStatus(m, key)) > -1;
}

function handleError(m, key, error) {
  let result = setStatus(m, key, 'error');

  // TODO: this should be configurable for each sync
  if (key !== 'sso') {
    const stopError = new Error(
      `An error occurred when fetching ${key} data for Lock: ${error.message}`
    );
    stopError.code = 'sync';
    stopError.origin = error;
    result = l.stop(result, stopError);
  }

  return result;
}
